Skip to content

Commit 873f4f6

Browse files
committed
feat(hub)!: pin discovery to the credential's origin via @corbits/credential-http
Replace @corbits/credential-mcp with @corbits/credential-http. Discovery that sends a secret now pins to the credential's provider apiBaseUrl origin, and a host allows extra origins per pinned origin with `extraOrigins`. No origin is allowed by default, which drops the built-in vendor allowance credential-mcp carried. A keyless credential pins to the URL's origin. OAuth discovery's fallback now requires the resource-served metadata to name the resource's own issuer (RFC 8414 §3.3). BREAKING CHANGE: readCredentialSecret is now readCredential and returns { secret, origin? }; discoverMcpServer takes `credential` instead of `secret`; a credential with a secret whose provider has no apiBaseUrl, or a URL off its origin without `extraOrigins`, is refused.
1 parent 7460de4 commit 873f4f6

10 files changed

Lines changed: 316 additions & 90 deletions

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ never hold a server's bearer.
3232
network work at construction: the names and the `ask` marks come from the
3333
stored catalog.
3434
4. **At run time the agent only ever sees a mediated handle.** Each server's
35-
credential resolves to an `http` handle (a fetch pinned to that server's
35+
credential resolves to an `http` handle (a fetch pinned to the credential's
3636
origin that injects the bearer per request), so the token never reaches
3737
agent code, and a relative path is all the bundle ever asks for.
3838

‎README.md‎

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Runs on Node.js 24+ and Bun 1.2+.
2121
For the `@corbits/mcp/hub` routes, also install:
2222

2323
```bash
24-
bun add @corbits/credential-mcp @intx/authz @intx/crypto @intx/db @intx/hub-api drizzle-orm hono
24+
bun add @corbits/credential-http @intx/authz @intx/crypto @intx/db @intx/hub-api drizzle-orm hono
2525
```
2626

2727
## Quickstart
@@ -46,25 +46,25 @@ Every client call takes `{ fetch }` as its last argument, for example a fetch th
4646

4747
- **Hub:** `@corbits/mcp/hub` mounts a discovery route on an [`@intx/hub-api`](https://github.com/faremeter/interchange/tree/main/packages/hub-api) app and reads tenant credentials from [`@intx/db`](https://github.com/faremeter/interchange/tree/main/packages/db).
4848
- **Sidecar:** `@corbits/mcp/sidecar-bundle` builds [`@intx/agent`](https://github.com/faremeter/interchange/tree/main/packages/agent) tools from a stored catalog.
49-
- **Pairs with:** [`@corbits/oauth-core`](https://github.com/corbitsdev/corbits-oauth-core) for the login flow and [`@corbits/credential-mcp`](https://github.com/corbitsdev/corbits-credential-mcp) for the origin-pinned credential provider.
49+
- **Pairs with:** [`@corbits/oauth-core`](https://github.com/corbitsdev/corbits-oauth-core) for the login flow and [`@corbits/credential-http`](https://github.com/corbitsdev/credential-http) for the origin-pinned credential provider.
5050

5151
## Reference
5252

53-
| Export | Entry | Purpose |
54-
| ------------------------------------------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------- |
55-
| `mcpInitialize(url, opts?)` | `@corbits/mcp` | Send `initialize`; returns `serverInfo` and `protocolVersion`. |
56-
| `mcpListTools(url, opts?)` | `@corbits/mcp` | Send `tools/list`; returns `McpTool[]`. |
57-
| `mcpCallTool(url, name, args, opts?)` | `@corbits/mcp` | Send `tools/call`; returns `McpToolResult`. |
58-
| `mcpTools(options, env?)` | `@corbits/mcp` | Discover each server live, then build tools. For hosts that can await; servers use `name` and `credentialHandle`. |
59-
| `qualifiedName`, `toolDescription`, `isAskExempt` | `@corbits/mcp` | Naming and `ask`-floor rules the tool builders use. |
60-
| `discoverMcpLoginEntry({ resourceUrl })` | `@corbits/mcp` | Resolve the authorization server (RFC 9728, then RFC 8414). |
61-
| `registerMcpClient(opts)` | `@corbits/mcp` | Register a loopback public client (RFC 7591). |
62-
| `mcpClientConfig(entry, opts)` | `@corbits/mcp` | Build the `OAuthClientConfig` oauth-core's login helpers take. |
63-
| `selectMcpScopes(opts)` | `@corbits/mcp` | Pick scopes in the MCP spec's selection order. |
64-
| `mcpServers(config)` | `@corbits/mcp/sidecar-bundle` | Build tools from stored catalogs over mediated credentials. |
65-
| `shapeMcpContent(content)`, `SIDECAR_BUNDLE_ID` | `@corbits/mcp/sidecar-bundle` | Result shaping and the bundle's tool id. |
66-
| `mountMcpDiscovery(app, opts)` | `@corbits/mcp/hub` | Mount `POST /mcp/discover`. |
67-
| `discoverMcpServer(args)`, `readCredentialSecret(opts)` | `@corbits/mcp/hub` | The discovery and credential reads the route uses. |
53+
| Export | Entry | Purpose |
54+
| ------------------------------------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------- |
55+
| `mcpInitialize(url, opts?)` | `@corbits/mcp` | Send `initialize`; returns `serverInfo` and `protocolVersion`. |
56+
| `mcpListTools(url, opts?)` | `@corbits/mcp` | Send `tools/list`; returns `McpTool[]`. |
57+
| `mcpCallTool(url, name, args, opts?)` | `@corbits/mcp` | Send `tools/call`; returns `McpToolResult`. |
58+
| `mcpTools(options, env?)` | `@corbits/mcp` | Discover each server live, then build tools. For hosts that can await; servers use `name` and `credentialHandle`. |
59+
| `qualifiedName`, `toolDescription`, `isAskExempt` | `@corbits/mcp` | Naming and `ask`-floor rules the tool builders use. |
60+
| `discoverMcpLoginEntry({ resourceUrl })` | `@corbits/mcp` | Resolve the authorization server (RFC 9728, then RFC 8414). |
61+
| `registerMcpClient(opts)` | `@corbits/mcp` | Register a loopback public client (RFC 7591). |
62+
| `mcpClientConfig(entry, opts)` | `@corbits/mcp` | Build the `OAuthClientConfig` oauth-core's login helpers take. |
63+
| `selectMcpScopes(opts)` | `@corbits/mcp` | Pick scopes in the MCP spec's selection order. |
64+
| `mcpServers(config)` | `@corbits/mcp/sidecar-bundle` | Build tools from stored catalogs over mediated credentials. |
65+
| `shapeMcpContent(content)`, `SIDECAR_BUNDLE_ID` | `@corbits/mcp/sidecar-bundle` | Result shaping and the bundle's tool id. |
66+
| `mountMcpDiscovery(app, opts)` | `@corbits/mcp/hub` | Mount `POST /mcp/discover`. |
67+
| `discoverMcpServer(args)`, `readCredential(opts)` | `@corbits/mcp/hub` | The discovery and credential reads the route uses. |
6868

6969
`McpToolSchema` validates a catalog entry (`McpTool`). Transport and protocol failures throw `McpError`. OAuth discovery failures throw `OAuthDiscoveryError` from `@corbits/oauth-core`.
7070

@@ -112,7 +112,32 @@ mountMcpDiscovery(mcpRoutes, {
112112

113113
Mount `mcpRoutes` on the hub app under `/api/tenants/:tenantId`, behind the hub's auth and tenant middleware.
114114

115-
`POST /api/tenants/:tenantId/mcp/discover` with `{ url, credentialId? }` returns `{ data: { serverInfo, tools } }`. `url` must be https (http only on loopback). `credentialId` names a tenant credential whose secret is sent as a bearer; a credential holding `MCP_NO_TOKEN_SENTINEL` from `@corbits/credential-mcp` sends no `authorization` header. Errors: 400 for a bad body or URL, 404 for an unknown credential, 422 when the server fails discovery. `requireGrant` is the host's own grant middleware for this route. The fetch is pinned to the server's origin and refuses redirects, so the secret never leaves that origin.
115+
`POST /api/tenants/:tenantId/mcp/discover` with `{ url, credentialId? }` returns `{ data: { serverInfo, tools } }`. `url` must be https (http only on loopback). `credentialId` names a tenant credential whose secret is sent as a bearer; a credential holding `MCP_NO_TOKEN_SENTINEL` from `@corbits/credential-http` sends no `authorization` header. Errors: 400 for a bad body or URL, 404 for an unknown credential, 422 when the server fails discovery or the request would leave the credential's origin. `requireGrant` is the host's own grant middleware for this route.
116+
117+
When a secret is sent, the fetch is pinned to the origin of the credential's provider `apiBaseUrl`; with no credential or a keyless one, to the URL's origin. Redirects are always refused, so the secret never leaves that origin.
118+
119+
#### Extra origins
120+
121+
Some servers serve MCP on a different origin from the one their credential is issued for. Nothing is allowed off the pinned origin by default. The host lists extra origins per pinned origin in `extraOrigins`, which is passed through to `@corbits/credential-http`:
122+
123+
```ts
124+
mountMcpDiscovery(mcpRoutes, {
125+
db,
126+
cipher,
127+
requireGrant: requireGrant("credential:*", "read"),
128+
extraOrigins: {
129+
"https://api.example.com": ["https://mcp.example.net"],
130+
},
131+
});
132+
```
133+
134+
| Option | Type | Default |
135+
| -------------- | --------------------------------------------- | ------- |
136+
| `extraOrigins` | `Readonly<Record<string, readonly string[]>>` | `{}` |
137+
138+
A credential pinned to `https://api.example.com` may then be sent to `https://mcp.example.net`; no other credential can.
139+
140+
`extraOrigins` is host-wide, not per tenant: an entry applies to every tenant's credential pinned to that origin, so list only origins you trust with all of them. `discoverMcpServer` takes the same option. For the sidecar bundle, configure the same allowance on the credential provider the host registers.
116141

117142
### Load the sidecar bundle
118143

@@ -136,7 +161,7 @@ export const tools = mcpServers({
136161

137162
Each catalog entry becomes a tool named `<handle>.<tool>`, with the remote `inputSchema` passed through and the call proxied to `tools/call`. Text content comes back as text and anything else as JSON; `isError` passes through. A handle that does not resolve, or a server that fails `initialize`, fails only that server's calls.
138163

139-
`handle` is the credential handle the host binds this server's token to. The bundle resolves it from the runtime `credentials` capability and expects an `http` credential, such as the one `@corbits/credential-mcp`'s provider returns. The package manifest declares the `mcp-server` credential for this.
164+
`handle` is the credential handle the host binds this server's token to. The bundle resolves it from the runtime `credentials` capability and expects an `http` credential, such as the one `@corbits/credential-http`'s MCP provider returns. The package manifest declares the `mcp-server` credential for this.
140165

141166
### Grant access
142167

@@ -146,7 +171,9 @@ Grant the agent's principal `tool:deepwiki.*` for the whole server, or `tool:dee
146171

147172
- Existing server configs, stored catalogs and the `mcp-server` credential keep working unchanged.
148173
- `@intx/harness` is a required peer. Import `FetchLike` from it; `@corbits/mcp` no longer exports that type.
149-
- `@intx/db`, `@intx/hub-api`, `drizzle-orm`, `hono` and `@corbits/credential-mcp` are optional peers. Install them if you use `@corbits/mcp/hub`.
174+
- `@intx/db`, `@intx/hub-api`, `drizzle-orm`, `hono` and `@corbits/credential-http` are optional peers. Install them if you use `@corbits/mcp/hub`. `@corbits/credential-http` replaces `@corbits/credential-mcp`.
175+
- Discovery with a credential pins to the credential's provider `apiBaseUrl` origin, not to the requested URL's origin. A credential with a secret whose provider has no `apiBaseUrl` is refused, and a URL on another origin needs `extraOrigins`. No origin is allowed by default.
176+
- `readCredentialSecret` is now `readCredential`, which returns `{ secret, origin? }`. `discoverMcpServer` takes `credential: { secret, origin? }` instead of `secret`.
150177
- `@intx/agent` and `@intx/harness` peers are `^0.4.0`.
151178
- Discovery rejects authorization-server metadata whose `issuer` differs from the one the protected resource names.
152179

‎bun.lock‎

Lines changed: 6 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,12 @@
5454
"check": "bun run typecheck && bun run lint && bun run format:check && bun run test"
5555
},
5656
"dependencies": {
57-
"@corbits/oauth-core": "^0.1.0",
57+
"@corbits/oauth-core": "^0.2.0",
5858
"@intx/types": "^0.4.0",
5959
"arktype": "^2.2.3"
6060
},
6161
"devDependencies": {
62-
"@corbits/credential-mcp": "^0.1.0",
62+
"@corbits/credential-http": "0.1.0",
6363
"@intx/agent": "0.4.0",
6464
"@intx/authz": "0.4.0",
6565
"@intx/db": "0.4.0",
@@ -73,7 +73,7 @@
7373
"typescript": "5.9.3"
7474
},
7575
"peerDependencies": {
76-
"@corbits/credential-mcp": "^0.1.0",
76+
"@corbits/credential-http": "^0.1.0",
7777
"@intx/agent": "^0.4.0",
7878
"@intx/db": "^0.4.0",
7979
"@intx/harness": "^0.4.0",
@@ -82,7 +82,7 @@
8282
"hono": "^4.11.9"
8383
},
8484
"peerDependenciesMeta": {
85-
"@corbits/credential-mcp": {
85+
"@corbits/credential-http": {
8686
"optional": true
8787
},
8888
"@intx/db": {

0 commit comments

Comments
 (0)