@corbits/mcp 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/README.md +120 -26
  2. package/dist/auth/callback.d.ts +14 -0
  3. package/dist/auth/callback.js +79 -0
  4. package/dist/auth/challenge.d.ts +9 -0
  5. package/dist/auth/challenge.js +52 -0
  6. package/dist/auth/contracts.d.ts +18 -0
  7. package/dist/auth/contracts.js +19 -0
  8. package/dist/auth/http.d.ts +2 -0
  9. package/dist/auth/http.js +2 -0
  10. package/dist/auth/index.d.ts +6 -0
  11. package/dist/auth/index.js +6 -0
  12. package/dist/auth/login.d.ts +62 -0
  13. package/dist/auth/login.js +175 -0
  14. package/dist/auth/oauth-http.d.ts +6 -0
  15. package/dist/auth/oauth-http.js +32 -0
  16. package/dist/auth/resource-server.d.ts +25 -0
  17. package/dist/auth/resource-server.js +94 -0
  18. package/dist/auth/url-policy.d.ts +10 -0
  19. package/dist/auth/url-policy.js +35 -0
  20. package/dist/client/resource-budget.d.ts +1 -0
  21. package/dist/client/resource-budget.js +1 -0
  22. package/dist/client/transport.d.ts +10 -0
  23. package/dist/client/transport.js +1 -0
  24. package/dist/client.d.ts +208 -26
  25. package/dist/client.js +1128 -90
  26. package/dist/extensions/apps.d.ts +58 -0
  27. package/dist/extensions/apps.js +55 -0
  28. package/dist/extensions/elicitation.d.ts +18 -0
  29. package/dist/extensions/elicitation.js +55 -0
  30. package/dist/extensions/index.d.ts +5 -0
  31. package/dist/extensions/index.js +5 -0
  32. package/dist/extensions/maturity.d.ts +44 -0
  33. package/dist/extensions/maturity.js +24 -0
  34. package/dist/extensions/skills/contracts.d.ts +53 -0
  35. package/dist/extensions/skills/contracts.js +1 -0
  36. package/dist/extensions/skills/index.d.ts +52 -0
  37. package/dist/extensions/skills/index.js +402 -0
  38. package/dist/extensions/tasks/contracts.d.ts +39 -0
  39. package/dist/extensions/tasks/contracts.js +1 -0
  40. package/dist/extensions/tasks/file-store.d.ts +12 -0
  41. package/dist/extensions/tasks/file-store.js +152 -0
  42. package/dist/extensions/tasks/index.d.ts +82 -0
  43. package/dist/extensions/tasks/index.js +221 -0
  44. package/dist/extensions/tasks/record.d.ts +3 -0
  45. package/dist/extensions/tasks/record.js +37 -0
  46. package/dist/hub/discover.d.ts +5 -1
  47. package/dist/hub/discover.js +36 -13
  48. package/dist/index.d.ts +3 -2
  49. package/dist/index.js +4 -2
  50. package/dist/interchange/client/capabilities.d.ts +4 -0
  51. package/dist/interchange/client/capabilities.js +53 -0
  52. package/dist/interchange/client/catalog.d.ts +14 -0
  53. package/dist/interchange/client/catalog.js +52 -0
  54. package/dist/interchange/client/contracts.d.ts +47 -0
  55. package/dist/interchange/client/contracts.js +1 -0
  56. package/dist/interchange/client/projection.d.ts +6 -0
  57. package/dist/interchange/client/projection.js +92 -0
  58. package/dist/interchange/client/skills.d.ts +16 -0
  59. package/dist/interchange/client/skills.js +87 -0
  60. package/dist/interchange/client.d.ts +8 -0
  61. package/dist/interchange/client.js +6 -0
  62. package/dist/interchange/server/contracts.d.ts +65 -0
  63. package/dist/interchange/server/contracts.js +1 -0
  64. package/dist/interchange/server.d.ts +5 -0
  65. package/dist/interchange/server.js +172 -0
  66. package/dist/naming.d.ts +2 -0
  67. package/dist/naming.js +7 -0
  68. package/dist/oauth-discovery.d.ts +107 -1
  69. package/dist/oauth-discovery.js +292 -56
  70. package/dist/protocol/content.d.ts +35 -0
  71. package/dist/protocol/content.js +64 -0
  72. package/dist/protocol/envelopes.d.ts +14 -0
  73. package/dist/protocol/envelopes.js +22 -0
  74. package/dist/protocol/errors.d.ts +26 -0
  75. package/dist/protocol/errors.js +31 -0
  76. package/dist/protocol/extensions.d.ts +34 -0
  77. package/dist/protocol/extensions.js +86 -0
  78. package/dist/protocol/headers.d.ts +12 -0
  79. package/dist/protocol/headers.js +128 -0
  80. package/dist/protocol/index.d.ts +11 -0
  81. package/dist/protocol/index.js +11 -0
  82. package/dist/protocol/json-schema.d.ts +8 -0
  83. package/dist/protocol/json-schema.js +152 -0
  84. package/dist/protocol/legacy.d.ts +12 -0
  85. package/dist/protocol/legacy.js +18 -0
  86. package/dist/protocol/methods.d.ts +116 -0
  87. package/dist/protocol/methods.js +78 -0
  88. package/dist/protocol/resource-budget.d.ts +9 -0
  89. package/dist/protocol/resource-budget.js +206 -0
  90. package/dist/protocol/schemas.d.ts +115 -0
  91. package/dist/protocol/schemas.js +553 -0
  92. package/dist/protocol/types.d.ts +295 -0
  93. package/dist/protocol/types.js +3 -0
  94. package/dist/protocol/ui.d.ts +3 -0
  95. package/dist/protocol/ui.js +18 -0
  96. package/dist/server/contracts.d.ts +67 -0
  97. package/dist/server/contracts.js +1 -0
  98. package/dist/server/errors.d.ts +6 -0
  99. package/dist/server/errors.js +21 -0
  100. package/dist/server/extensions.d.ts +7 -0
  101. package/dist/server/extensions.js +61 -0
  102. package/dist/server/fixtures.d.ts +2 -0
  103. package/dist/server/fixtures.js +34 -0
  104. package/dist/server/legacy.d.ts +9 -0
  105. package/dist/server/legacy.js +435 -0
  106. package/dist/server/tools.d.ts +17 -0
  107. package/dist/server/tools.js +93 -0
  108. package/dist/server.d.ts +14 -0
  109. package/dist/server.js +406 -0
  110. package/dist/sidecar-bundle.d.ts +7 -7
  111. package/dist/sidecar-bundle.js +36 -29
  112. package/dist/test-server.js +12 -3
  113. package/dist/tool.d.ts +8 -0
  114. package/dist/tool.js +35 -19
  115. package/dist/url.js +8 -3
  116. package/docs/conformance.md +135 -0
  117. package/docs/integration-status.md +98 -0
  118. package/docs/legacy-server.md +58 -0
  119. package/docs/migration.md +143 -0
  120. package/docs/public-api.md +106 -0
  121. package/examples/README.md +28 -0
  122. package/examples/standalone-client.ts +14 -0
  123. package/examples/standalone-server.ts +37 -0
  124. package/package.json +50 -15
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @corbits/mcp
2
2
 
3
- A Corbits tool pack for the Interchange sidecar that turns each tool on a remote Model Context Protocol server into its own `@intx/agent` tool, over streamable HTTP (2025-03-26 spec). It also ships a hub route for catalog discovery and OAuth discovery helpers, and the client works standalone.
3
+ A Corbits tool pack for the Interchange sidecar that turns each tool on a remote Model Context Protocol server into its own `@intx/agent` tool, over streamable HTTP. The client speaks the 2026-07-28 revision and falls back to the 2025 `initialize` handshake when the server does not. It also ships a hub route for catalog discovery and OAuth discovery helpers, and the client works standalone.
4
4
 
5
5
  ## Why @corbits/mcp?
6
6
 
@@ -8,16 +8,63 @@ A Corbits tool pack for the Interchange sidecar that turns each tool on a remote
8
8
  2. **The agent never holds the server's token.** Calls go through a mediated credential: a fetch pinned to the server's origin that adds the bearer per request.
9
9
  3. **Destructive tools stay behind approval.** Every generated tool starts `ask`-marked, and a tool the server flags `destructiveHint: true` cannot be lowered to `allow`.
10
10
 
11
- It supports `initialize`, `tools/list` and `tools/call` only, against stateless and stateful (`Mcp-Session-Id`) servers.
11
+ `mcpConnect` probes `server/discover`. A 2026-07-28 server is stateless: later calls send `MCP-Protocol-Version`, `Mcp-Method`, and `Mcp-Name`, and carry `io.modelcontextprotocol/protocolVersion`, `clientInfo`, and `clientCapabilities` in `_meta`. They do not send `initialize`, `notifications/initialized`, or `Mcp-Session-Id`. A call made with no session speaks this stateless path directly; a session handed over from `mcpConnect` is stateless too. If `server/discover` is missing, times out, or the server only speaks a 2025 (or earlier) version, the client keeps the `initialize` plus `Mcp-Session-Id` path.
12
+
13
+ Core methods, paginated where the spec uses a cursor: `tools/list`, `tools/call`, `prompts/list`, `prompts/get`, `resources/list`, `resources/read`, `resources/templates/list`, `completion/complete`. A missing `resultType` is `complete`. `input_required` is returned to the caller (method, `inputRequests`, `requestState`) and retried with `mcpContinue`. `subscriptions/listen` is a long-lived POST; `notifications/progress` stays on the call that asked for it; modern HTTP cancellation closes the response stream (it does not send `notifications/cancelled`). Legacy transport cancellation is separately negotiated.
14
+
15
+ Extensions are off unless named in `extensions`: `io.modelcontextprotocol/tasks` (`tasks/get`, `tasks/update`, `tasks/cancel`, and a `task` result), `io.modelcontextprotocol/ui` (capability advertisement and passthrough of tool-result UI metadata; no widget renderer), and `io.modelcontextprotocol/skills` (`skills/list`, `skills/get`, with `resources/directory/read` only when the server declares `directoryRead`). Roots, sampling, logging, modern `ping`, HTTP+SSE, and SSE resumption are not implemented; the opt-in legacy shim supports revision-scoped `ping`. Prompts, resources, and subscriptions are not projected into agent tools. Tools remain the only grantable unit. Catalog entries keep `outputSchema`, `annotations.title`, the four boolean hints, `ttlMs`, and `cacheScope`. JSON Schema is passed through, including unknown 2020-12 keywords.
16
+
17
+ ## Public entries
18
+
19
+ | Entry | Role | Host dependencies |
20
+ | --------------------------------- | ------------------------------------------------------------ | ------------------------------------------ |
21
+ | `@corbits/mcp/client` | Standalone MCP client | No Interchange/hub runtime required |
22
+ | `@corbits/mcp/server` | Fetch-compatible MCP server | No mandatory Interchange or OAuth provider |
23
+ | `@corbits/mcp/interchange/client` | Remote MCP into selected Interchange tools and host services | Agent/harness peers |
24
+ | `@corbits/mcp/interchange/server` | Explicit selected Interchange bindings exposed as MCP | Host-bound identity and grants |
25
+
26
+ The root, `./sidecar-bundle`, and `./hub` remain compatibility facades. Import
27
+ standalone roles directly to avoid the root facade's historical host dependencies.
28
+ See [migration and deployment](docs/migration.md), the
29
+ [revision/maturity and requirement matrix](docs/conformance.md), and
30
+ [examples](examples/README.md), and the [public API handoff](docs/public-api.md). These are scoped APIs, not a claim of full MCP
31
+ conformance or browser Apps-host support.
32
+
33
+ ## Server compatibility and Apps hosting
34
+
35
+ `createMcpServer` is stateless by default. Its explicit `legacy` option enables
36
+ 2025-11-25, 2025-06-18 and 2025-03-26 Streamable HTTP sessions. Defaults are 1,024
37
+ in-memory sessions with a 30-minute absolute TTL; hosts need session affinity,
38
+ and restart loses sessions. Sessions permit 10,000 unique request IDs. Private
39
+ initialization requires `mcp:server` / `initialize` authorization. Only March 2025
40
+ accepts supported batches; GET returns 405, with no HTTP+SSE or resumption. See
41
+ [legacy deployment and limits](docs/legacy-server.md).
42
+
43
+ Apps capability advertisement requires explicit Apps host support and bridge,
44
+ sandbox/CSP/authenticated-bridge commitments, and presentation. Metadata-only
45
+ handling never advertises Apps. This package does not supply a browser renderer
46
+ or certify the host's sandbox. See [scope and evidence](docs/conformance.md).
12
47
 
13
48
  ## Install
14
49
 
15
50
  ```bash
16
- bun add @corbits/mcp @intx/agent @intx/harness
51
+ bun add @corbits/mcp
52
+ # For Interchange consumption:
53
+ bun add @intx/agent @intx/harness
54
+ # Historical root / guarded OAuth login also requires OAuth-core:
55
+ bun add @corbits/oauth-core@0.3.0
17
56
  ```
18
57
 
19
58
  Runs on Node.js 24+ and Bun 1.2+.
20
59
 
60
+ Standalone client/server entries do not require OAuth-core or Interchange host
61
+ peers. OAuth-core is now an **optional peer**, not a mandatory dependency: this
62
+ is a breaking installation change for the historical root and OAuth login users,
63
+ who must explicitly install it as shown above. Installing OAuth-core still brings
64
+ its own DB/hub/Hono dependencies; that host installation is separate from the
65
+ neutral package. See [public API dependency notes](docs/public-api.md) for the
66
+ credential-http/harness/types peer conflict affecting the full hub setup.
67
+
21
68
  For the `@corbits/mcp/hub` routes, also install:
22
69
 
23
70
  ```bash
@@ -29,16 +76,16 @@ bun add @corbits/credential-http @intx/authz @intx/crypto @intx/db @intx/hub-api
29
76
  Lists the tools on a public MCP server:
30
77
 
31
78
  ```ts
32
- import { mcpInitialize, mcpListTools } from "@corbits/mcp";
79
+ import { mcpConnect, mcpListTools } from "@corbits/mcp/client";
33
80
 
34
81
  const url = "https://mcp.deepwiki.com/mcp";
35
- const session = await mcpInitialize(url);
82
+ const session = await mcpConnect(url);
36
83
  for (const tool of await mcpListTools(url, { session })) {
37
84
  console.log(`${tool.name}: ${tool.description ?? ""}`);
38
85
  }
39
86
  ```
40
87
 
41
- Every client call takes `{ fetch, timeoutMs, signal, session }` as its last argument, for example a fetch that adds an `Authorization` header. `timeoutMs` covers the request and reading its body, and `signal` cancels both. `mcpTools` and `mcpServers` take a `timeoutMs` option, 60 seconds by default, and pass each tool call's abort signal through, so a stalling server fails that call instead of hanging it. A response body or event-stream frame over 4 MiB is refused and the stream cancelled.
88
+ Every client call takes `{ fetch, timeoutMs, signal, session }` as its last argument, for example a fetch that adds an `Authorization` header. `timeoutMs` covers the request and reading its body, and `signal` cancels both. `mcpTools` and `mcpServers` take a `timeoutMs` option, 60 seconds by default, and pass each tool call's abort signal through, so a stalling server fails that call instead of hanging it. By default a response body or event-stream frame over 4 MiB is refused and the stream cancelled. Explicit `resources/read` requests with `maxDecodedBytes` (including Skills consumer reads) support up to 16 MiB aggregate decoded text/blob bytes, with a finite JSON/base64 encoding allowance and a separate 64 KiB metadata/envelope cap. Other methods retain the 4 MiB default. Servers explicitly configured with the Skills extension apply the same bounded resource-emission policy; ordinary resource providers retain the default validation limit.
42
89
 
43
90
  ## Where it fits
44
91
 
@@ -50,23 +97,28 @@ Every client call takes `{ fetch, timeoutMs, signal, session }` as its last argu
50
97
 
51
98
  ## Reference
52
99
 
53
- | Export | Entry | Purpose |
54
- | ------------------------------------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------- |
55
- | `mcpInitialize(url, opts?)` | `@corbits/mcp` | Send `initialize` and `notifications/initialized`; returns the `McpSession` to pass to later calls. |
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. |
68
-
69
- `McpToolSchema` validates a catalog entry (`McpTool`). Transport and protocol failures throw `McpError`. OAuth discovery failures throw `OAuthDiscoveryError` from `@corbits/oauth-core`.
100
+ | Export | Entry | Purpose |
101
+ | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
102
+ | `mcpConnect(url, opts?)` | `@corbits/mcp` | Probe `server/discover`, or fall back to `initialize`; returns the `McpSession` for later calls. |
103
+ | `mcpInitialize(url, opts?)` | `@corbits/mcp` | Explicit legacy `initialize` and `notifications/initialized`. |
104
+ | `mcpListTools(url, opts?)` | `@corbits/mcp` | Send `tools/list`; returns `McpTool[]` with `ttlMs` and `cacheScope` when the server sent them. |
105
+ | `mcpCallTool(url, name, args, opts?)` | `@corbits/mcp` | Send `tools/call`. A complete result, `input_required`, or (when tasks are enabled) a task handle. |
106
+ | `mcpContinue(url, pending, inputResponses, opts?)` | `@corbits/mcp` | Retry an `input_required` result, echoing `requestState`. |
107
+ | `mcpListPrompts`, `mcpGetPrompt`, `mcpListResources`, `mcpReadResource`, `mcpListResourceTemplates`, `mcpComplete`, `mcpListen` | `@corbits/mcp` | The rest of the core client. Prompts, resources, and subscriptions are not agent tools. |
108
+ | `mcpTools(options, env?)` | `@corbits/mcp` | Discover each server live, then build tools. For hosts that can await; servers use `name` and `credentialHandle`. |
109
+ | `qualifiedName`, `toolDescription`, `isAskExempt` | `@corbits/mcp` | Naming and `ask`-floor rules the tool builders use. |
110
+ | `discoverMcpLoginEntry({ resourceUrl })` | `@corbits/mcp` | Resolve the authorization server (RFC 9728, then RFC 8414). |
111
+ | `registerMcpClient(opts)` | `@corbits/mcp` | Register a public client (RFC 7591), `application_type` `"native"` unless set. |
112
+ | `preferredMcpClientRegistration`, `mcpClientIdMetadataDocument`, `resolveStoredMcpClient` | `@corbits/mcp` | Pre-registered, then Client ID Metadata Documents, then dynamic registration. Dynamic and pre-registered credentials are keyed by issuer. |
113
+ | `assertAuthorizationIssuer`, `readAuthorizationCallback` | `@corbits/mcp` | Check RFC 9207 `iss` against the recorded issuer before the code is read. |
114
+ | `mcpClientConfig(entry, opts)` | `@corbits/mcp` | Build the `OAuthClientConfig` oauth-core's login helpers take. |
115
+ | `selectMcpScopes(opts)` | `@corbits/mcp` | Pick scopes in the MCP spec's selection order. |
116
+ | `mcpServers(config)` | `@corbits/mcp/sidecar-bundle` | Build tools from stored catalogs over mediated credentials. |
117
+ | `shapeMcpContent(content)`, `SIDECAR_BUNDLE_ID` | `@corbits/mcp/sidecar-bundle` | Result shaping and the bundle's tool id. |
118
+ | `mountMcpDiscovery(app, opts)` | `@corbits/mcp/hub` | Mount `POST /mcp/discover`. |
119
+ | `discoverMcpServer(args)`, `readCredential(opts)` | `@corbits/mcp/hub` | The discovery and credential reads the route uses. |
120
+
121
+ `McpToolSchema` validates a catalog entry (`McpTool`). Transport and protocol failures throw `McpError`. OAuth-facing discovery failures throw `OAuthDiscoveryError` from `@corbits/oauth-core`. Neutral resource-auth URL-policy errors preserve the diagnostic name/message but are not instances of that optional peer class; see [public API](docs/public-api.md).
70
122
 
71
123
  ### Grants
72
124
 
@@ -159,7 +211,7 @@ export const tools = mcpServers({
159
211
  });
160
212
  ```
161
213
 
162
- 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.
214
+ 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; richer content and private metadata use explicit detail/host channels. `isError` passes through. `input_required` and task results require explicit host lifecycle services; absent services report unsupported rather than implicitly approving input or fabricating pending execution. A handle that does not resolve, or a server that fails the handshake, fails only that server's calls.
163
215
 
164
216
  `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.
165
217
 
@@ -171,16 +223,58 @@ A host records how a server authenticates in the credential's `metadata.mcp.auth
171
223
 
172
224
  Grant the agent's principal `tool:deepwiki.*` for the whole server, or `tool:deepwiki.<tool>` per tool, with `allow`, `ask` or `deny`.
173
225
 
226
+ ## Migrating to the 2026 client API
227
+
228
+ The 2026 client changes the public return contract: `mcpCallTool` returns
229
+ `McpToolCallResult`, not only a resolved `McpToolResult`. Callers must narrow
230
+ `resultType` before reading `content`, `isError`, or `structuredContent`:
231
+
232
+ ```ts
233
+ const result = await mcpCallTool(url, "echo", { text: "hi" }, { session });
234
+ if (result.resultType === "input_required") {
235
+ // Fulfill result.inputRequests, then pass this result and the collected
236
+ // inputResponses to mcpContinue(url, result, inputResponses, { session }).
237
+ } else if (result.resultType === "task") {
238
+ // Handle the task through the opted-in tasks extension.
239
+ } else {
240
+ console.log(result.content);
241
+ }
242
+ ```
243
+
244
+ `mcpContinue` can return another `input_required` result; always use the latest
245
+ pending result for the next retry. Prompt and resource reads also expose their
246
+ pending variants. Do not cast a union result to a resolved-only type. This is a
247
+ breaking API change: the release containing it must be the next minor release,
248
+ with a `!` marker in the breaking-change PR title as required by CONTRIBUTING.
249
+
250
+ ### Protocol regression coverage and limits
251
+
252
+ The focused regressions in `src/protocol-2026.test.ts` check these requirements:
253
+
254
+ - [Streamable HTTP backward compatibility](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#backward-compatibility):
255
+ an HTTP 400 discovery response with an empty, non-JSON, or legacy missing-session
256
+ error body falls back to `initialize`; later requests carry the legacy session.
257
+ Recognized modern errors do not take that fallback. Ordinary HTTP 401/403,
258
+ rate-limit, server, and malformed successful responses remain failures.
259
+ - [MRTR client requirements](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr#client-requirements-basic-workflow):
260
+ retries echo only the latest `requestState`, including an empty string, and
261
+ omit it when the latest response omits it. Tests also check fresh request IDs,
262
+ original tool arguments, and the latest input responses across multiple rounds.
263
+
264
+ These tests and the SDK transport e2e suite are targeted compatibility evidence,
265
+ not comprehensive MCP certification. The transport and extension limits listed
266
+ above still apply, including no deprecated HTTP+SSE transport or SSE resumption.
267
+
174
268
  ## Upgrading from 0.1
175
269
 
176
270
  - Existing server configs, stored catalogs and the `mcp-server` credential keep working unchanged.
177
- - `@intx/harness` is a required peer. Import `FetchLike` from it; `@corbits/mcp` no longer exports that type.
271
+ - For historical Interchange integrations, install `@intx/agent` and `@intx/harness`. They are optional package peers so standalone imports do not install a host runtime.
178
272
  - `@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`.
179
273
  - 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.
180
274
  - `readCredentialSecret` is now `readCredential`, which returns `{ secret, origin? }`. `discoverMcpServer` takes `credential: { secret, origin? }` instead of `secret`.
181
275
  - `mcpInitialize` returns an `McpSession` (`protocolVersion`, `sessionId?`, `serverInfo?`) and sends `notifications/initialized`. Pass it as `{ session }` to `mcpListTools` and `mcpCallTool`; a stateful server needs it.
182
276
  - The discovery route refuses plain-http loopback URLs unless `allowLoopback` is set. Handles and server names containing `.`, and catalogs repeating a tool name, are refused. `mcpTools` refuses an empty or repeated server name, and a non-https URL, before any request.
183
- - `@intx/agent` and `@intx/harness` peers are `^0.4.0`.
277
+ - The integration baseline is `@intx/agent` 0.5.0 and `@intx/harness` 0.5.0; see package peer ranges.
184
278
  - Discovery rejects authorization-server metadata whose `issuer` differs from the one the protected resource names.
185
279
 
186
280
  ## License
@@ -0,0 +1,14 @@
1
+ import type { CallbackServer } from "@corbits/oauth-core";
2
+ import { type AuthorizationIssuerOptions } from "../oauth-discovery.js";
3
+ /** Fetch-compatible receiver implementing oauth-core's injected CallbackServer.
4
+ * The host MUST bind exactly redirectUri before returning this adapter from
5
+ * startCallbackServer, and MUST stop that listener from closeListener.
6
+ * Unlike the stock callback server this checks iss before returning a code. */
7
+ export declare function createMcpCallbackAdapter(opts: {
8
+ expectedState: string;
9
+ redirectUri: string;
10
+ issuer: Omit<AuthorizationIssuerOptions, "iss">;
11
+ closeListener(): void;
12
+ }): CallbackServer & {
13
+ fetch(request: Request): Response;
14
+ };
@@ -0,0 +1,79 @@
1
+ import { OAuthDiscoveryError } from "@corbits/oauth-core";
2
+ import { assertAuthorizationCallbackIssuer, readAuthorizationCallback, } from "../oauth-discovery.js";
3
+ import { assertRedirectUri } from "./http.js";
4
+ /** Fetch-compatible receiver implementing oauth-core's injected CallbackServer.
5
+ * The host MUST bind exactly redirectUri before returning this adapter from
6
+ * startCallbackServer, and MUST stop that listener from closeListener.
7
+ * Unlike the stock callback server this checks iss before returning a code. */
8
+ export function createMcpCallbackAdapter(opts) {
9
+ const redirect = assertRedirectUri(opts.redirectUri);
10
+ let outcome;
11
+ let notify;
12
+ let closed = false;
13
+ return {
14
+ port: Number(redirect.port || (redirect.protocol === "https:" ? 443 : 80)),
15
+ fetch(request) {
16
+ const url = new URL(request.url);
17
+ if (request.method !== "GET" ||
18
+ url.origin !== redirect.origin ||
19
+ url.pathname !== redirect.pathname)
20
+ return new Response(null, { status: 404 });
21
+ if (closed || outcome !== undefined)
22
+ return new Response(null, { status: 409 });
23
+ if (url.searchParams.getAll("state").length !== 1 ||
24
+ url.searchParams.get("state") !== opts.expectedState)
25
+ return new Response("Invalid authorization response", { status: 400 });
26
+ try {
27
+ assertAuthorizationCallbackIssuer(url.searchParams, opts.issuer);
28
+ }
29
+ catch {
30
+ // Unauthenticated attempts must not terminate the legitimate login.
31
+ return new Response("Invalid authorization response", { status: 400 });
32
+ }
33
+ try {
34
+ outcome = readAuthorizationCallback(url.searchParams, opts.issuer);
35
+ notify?.();
36
+ return new Response("Authorization received. You may close this window.", {
37
+ headers: {
38
+ "cache-control": "no-store",
39
+ "content-type": "text/plain",
40
+ },
41
+ });
42
+ }
43
+ catch {
44
+ outcome = {
45
+ error: new OAuthDiscoveryError("Invalid authorization response"),
46
+ };
47
+ notify?.();
48
+ return new Response("Invalid authorization response", { status: 400 });
49
+ }
50
+ },
51
+ async waitForCode(signal) {
52
+ signal.throwIfAborted();
53
+ if (outcome === undefined)
54
+ await new Promise((resolve, reject) => {
55
+ const abort = () => {
56
+ notify = undefined;
57
+ reject(new OAuthDiscoveryError("Authorization cancelled"));
58
+ };
59
+ notify = () => {
60
+ signal.removeEventListener("abort", abort);
61
+ notify = undefined;
62
+ resolve();
63
+ };
64
+ signal.addEventListener("abort", abort, { once: true });
65
+ });
66
+ if (outcome === undefined || "error" in outcome)
67
+ throw (outcome?.error ?? new OAuthDiscoveryError("Authorization cancelled"));
68
+ return outcome.code;
69
+ },
70
+ close() {
71
+ if (closed)
72
+ return;
73
+ closed = true;
74
+ outcome ??= { error: new OAuthDiscoveryError("Authorization cancelled") };
75
+ notify?.();
76
+ opts.closeListener();
77
+ },
78
+ };
79
+ }
@@ -0,0 +1,9 @@
1
+ export type McpAuthChallenge = {
2
+ resourceMetadata?: string;
3
+ scope?: string;
4
+ error?: string;
5
+ };
6
+ /** RFC 9110 challenge lists: commas inside quoted strings are not separators. */
7
+ export declare function parseMcpAuthChallenge(header: string | null | undefined): McpAuthChallenge | undefined;
8
+ /** Step-up is an explicit host decision, never an automatic replay or grant widening. */
9
+ export declare function mcpStepUpScopes(challenge: McpAuthChallenge, granted: readonly string[]): string[];
@@ -0,0 +1,52 @@
1
+ import { OAuthDiscoveryError } from "@corbits/oauth-core";
2
+ /** RFC 9110 challenge lists: commas inside quoted strings are not separators. */
3
+ export function parseMcpAuthChallenge(header) {
4
+ if (!header)
5
+ return undefined;
6
+ if (header.length > 16384 || /[\r\n]/.test(header))
7
+ throw new OAuthDiscoveryError("Invalid authentication challenge");
8
+ const parts = header.match(/(?:[^,"]|"(?:[^"\\]|\\.)*")+/g) ?? [];
9
+ let bearer = false;
10
+ let found = false;
11
+ const params = new Map();
12
+ for (const part of parts) {
13
+ let text = part.trim();
14
+ const scheme = /^([!#$%&'*+.^_`|~\w-]+)(?:\s+|$)(.*)$/.exec(text);
15
+ if (scheme && !/^\s*=/.test(scheme[2] ?? "")) {
16
+ bearer = scheme[1]?.toLowerCase() === "bearer";
17
+ if (bearer && found)
18
+ throw new OAuthDiscoveryError("Ambiguous Bearer challenge");
19
+ if (bearer)
20
+ found = true;
21
+ text = scheme[2] ?? "";
22
+ }
23
+ if (!bearer || !text)
24
+ continue;
25
+ const pair = /^([!#$%&'*+.^_`|~\w-]+)\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([!#$%&'*+.^_`|~\w-]+))$/.exec(text);
26
+ if (!pair)
27
+ throw new OAuthDiscoveryError("Malformed Bearer challenge");
28
+ const key = pair[1].toLowerCase();
29
+ if (params.has(key))
30
+ throw new OAuthDiscoveryError("Duplicate Bearer challenge parameter");
31
+ params.set(key, (pair[2] ?? pair[3] ?? "").replace(/\\(.)/g, "$1"));
32
+ }
33
+ if (!found)
34
+ return undefined;
35
+ return {
36
+ ...(params.has("resource_metadata")
37
+ ? { resourceMetadata: params.get("resource_metadata") }
38
+ : {}),
39
+ ...(params.has("scope") ? { scope: params.get("scope") } : {}),
40
+ ...(params.has("error") ? { error: params.get("error") } : {}),
41
+ };
42
+ }
43
+ /** Step-up is an explicit host decision, never an automatic replay or grant widening. */
44
+ export function mcpStepUpScopes(challenge, granted) {
45
+ if (challenge.error !== "insufficient_scope" ||
46
+ challenge.scope === undefined) {
47
+ throw new OAuthDiscoveryError("No actionable insufficient_scope challenge");
48
+ }
49
+ return [
50
+ ...new Set([...granted, ...challenge.scope.split(" ").filter(Boolean)]),
51
+ ];
52
+ }
@@ -0,0 +1,18 @@
1
+ /** Host-authenticated identity. Never derive these fields from MCP params. */
2
+ export type McpIdentity = {
3
+ readonly tenantId: string;
4
+ readonly principalId: string;
5
+ readonly scopes: readonly string[];
6
+ /** Host-assigned cache partition, never a bearer token. */
7
+ readonly authorizationContextId: string;
8
+ };
9
+ export type McpAuthOperation = {
10
+ readonly resource: string;
11
+ readonly action: string;
12
+ };
13
+ export type McpAuthPolicy = {
14
+ authenticate(request: Request): McpIdentity | null | Promise<McpIdentity | null>;
15
+ authorize(identity: McpIdentity, operation: McpAuthOperation): boolean | Promise<boolean>;
16
+ };
17
+ /** Copies only validated identity fields; extra verifier claims cannot leak. */
18
+ export declare function validateMcpIdentity(value: unknown): McpIdentity;
@@ -0,0 +1,19 @@
1
+ import { type } from "arktype";
2
+ const Identity = type({
3
+ tenantId: "string > 0",
4
+ principalId: "string > 0",
5
+ scopes: "string[]",
6
+ authorizationContextId: "string > 0",
7
+ });
8
+ /** Copies only validated identity fields; extra verifier claims cannot leak. */
9
+ export function validateMcpIdentity(value) {
10
+ const identity = Identity(value);
11
+ if (identity instanceof type.errors)
12
+ throw new Error("Invalid authenticated MCP identity");
13
+ return Object.freeze({
14
+ tenantId: identity.tenantId,
15
+ principalId: identity.principalId,
16
+ scopes: Object.freeze([...identity.scopes]),
17
+ authorizationContextId: identity.authorizationContextId,
18
+ });
19
+ }
@@ -0,0 +1,2 @@
1
+ export { assertOAuthUrl, assertRedirectUri, secureOAuthFetch, } from "./oauth-http.js";
2
+ export type { FetchLike } from "../protocol/types.js";
@@ -0,0 +1,2 @@
1
+ // OAuth-only compatibility facade. Neutral server code uses url-policy directly.
2
+ export { assertOAuthUrl, assertRedirectUri, secureOAuthFetch, } from "./oauth-http.js";
@@ -0,0 +1,6 @@
1
+ export * from "./contracts.js";
2
+ export * from "./resource-server.js";
3
+ export * from "./challenge.js";
4
+ export * from "./login.js";
5
+ export * from "./callback.js";
6
+ export { secureOAuthFetch } from "./http.js";
@@ -0,0 +1,6 @@
1
+ export * from "./contracts.js";
2
+ export * from "./resource-server.js";
3
+ export * from "./challenge.js";
4
+ export * from "./login.js";
5
+ export * from "./callback.js";
6
+ export { secureOAuthFetch } from "./http.js";
@@ -0,0 +1,62 @@
1
+ import { type McpClientConfigOptions, type McpLoginEntry, type StoredMcpClient } from "../oauth-discovery.js";
2
+ import { type FetchLike } from "./http.js";
3
+ /** Host-only secret material. Never expose through MCP results or agent context. */
4
+ export type McpOAuthTokens = {
5
+ access: string;
6
+ refresh?: string;
7
+ expiresAt?: number;
8
+ scope?: string;
9
+ };
10
+ export type McpOAuthBinding = {
11
+ tenantId: string;
12
+ principalId: string;
13
+ issuer: string;
14
+ resource: string;
15
+ clientId: string;
16
+ };
17
+ /** Performs real DCR only when neither issuer-bound pre-registration nor CIMD applies. */
18
+ export declare function prepareMcpClient(opts: {
19
+ entry: McpLoginEntry;
20
+ storedClient?: StoredMcpClient;
21
+ clientMetadataUrl?: string;
22
+ clientName: string;
23
+ redirectUri: string;
24
+ /** Hosted web callbacks use "web"; native/loopback clients default to "native". */
25
+ applicationType?: "native" | "web";
26
+ fetchImpl?: FetchLike;
27
+ }): Promise<StoredMcpClient>;
28
+ /** A one-use callback session. The host mounts its registered callback route,
29
+ * passes the full URLSearchParams here, then persists secrets server-side.
30
+ * Unlike oauth-core BaseTokens this contract permits access-token-only grants.
31
+ * No automatic tool replay, persistence, browser opening or grant widening. */
32
+ export declare function createMcpAuthorizationSession(opts: {
33
+ entry: McpLoginEntry;
34
+ client: StoredMcpClient;
35
+ config: Omit<McpClientConfigOptions, "clientId">;
36
+ tenantId: string;
37
+ principalId: string;
38
+ persist(binding: McpOAuthBinding, tokens: McpOAuthTokens): Promise<void>;
39
+ fetchImpl?: FetchLike;
40
+ now?: () => number;
41
+ expiresInMs?: number;
42
+ }): {
43
+ authorizeUrl: string;
44
+ binding: Readonly<{
45
+ tenantId: string;
46
+ principalId: string;
47
+ issuer: string;
48
+ resource: string;
49
+ clientId: string;
50
+ }>;
51
+ cancel(): void;
52
+ complete(params: URLSearchParams): Promise<void>;
53
+ };
54
+ /** Refresh under the same recorded issuer/resource/client binding. */
55
+ export declare function refreshMcpOAuthTokens(opts: {
56
+ entry: McpLoginEntry;
57
+ client: StoredMcpClient;
58
+ binding: McpOAuthBinding;
59
+ tokens: McpOAuthTokens;
60
+ fetchImpl?: FetchLike;
61
+ now?: () => number;
62
+ }): Promise<McpOAuthTokens>;