Skip to content

Commit e13d7b3

Browse files
committed
feat(client)!: support stateful streamable-HTTP servers
mcpInitialize now sends notifications/initialized and returns an McpSession whose Mcp-Session-Id and MCP-Protocol-Version later requests carry. Discovery, mcpTools and the sidecar bundle pass it on, and reopen it after a failed call. A protocolVersion that is not printable ASCII is refused before it is echoed into a header, and a timed-out notification is an McpError. Non-JSON data frames are skipped, and only a message with result or error and no method counts as the response. e2e covers the official SDK's stateless and stateful server transports; the SDK is a devDependency only. BREAKING CHANGE: mcpInitialize returns McpSession instead of McpServerInfo.
1 parent 9b3600e commit e13d7b3

14 files changed

Lines changed: 589 additions & 72 deletions

‎.github/workflows/ci.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ jobs:
1717
- uses: oven-sh/setup-bun@v2
1818
- run: bun install --frozen-lockfile
1919
- run: bun run check
20+
- run: bun run test:e2e
2021
- name: node consumer smoke
2122
run: |
2223
set -euo pipefail

‎AGENTS.md‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,21 +40,22 @@ never hold a server's bearer.
4040

4141
The client in `src/client.ts` is hand-rolled on purpose. It only sends
4242
`initialize`, `tools/list` and `tools/call`, each as one request awaiting one
43-
reply; in the sidecar bundle that request goes through a fetch the mediated
43+
reply, plus the `notifications/initialized` notification; in the sidecar bundle that request goes through a fetch the mediated
4444
credential handle pins to the server's origin. That needs only JSON-RPC
4545
framing and reading the matching `data:` frame from an event-stream reply.
4646

4747
The SDK's streamable-HTTP client transport is built for long-lived sessions
4848
(resumable streams, server-initiated requests) and installs `zod`, `ajv`,
4949
`express` and more, none of which this package uses.
5050

51-
The cost: the client never sends `Mcp-Session-Id`, so a server that issues
52-
one on `initialize` and requires it afterwards rejects `tools/list` and
53-
`tools/call`. Notifications and server-to-client requests are not
54-
supported either.
51+
The session a stateful server issues on `initialize` is carried in
52+
`McpSession` and sent as `Mcp-Session-Id` with `MCP-Protocol-Version` on
53+
every later request. Resumable streams and server-to-client requests are not
54+
supported. `e2e/` runs discovery and tool calls against the SDK's stateless
55+
and stateful server transports; the SDK is a devDependency only.
5556

5657
## Local development
5758

5859
```sh
59-
bun install && bun run check
60+
bun install && bun run check && bun run test:e2e
6061
```

‎CONTRIBUTING.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
```sh
66
bun install
77
bun run check
8+
bun run test:e2e
89
```
910

10-
`bun run check` runs typecheck, lint, format check and unit tests. `bun run format` rewrites the tree.
11+
`bun run check` runs typecheck, lint, format check and unit tests. `bun run test:e2e` runs the client against the official SDK's server transports. `bun run format` rewrites the tree.
1112

1213
Contributors sign the [CLA](CLA.md) on their first PR; the CLA bot explains how.
1314

‎README.md‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ A Corbits tool pack for the Interchange sidecar that turns each tool on a remote
88
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.
99
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`.
1010

11-
It supports `initialize`, `tools/list` and `tools/call` only. It does not send `Mcp-Session-Id`, so servers that require a session are not supported.
11+
It supports `initialize`, `tools/list` and `tools/call` only, against stateless and stateful (`Mcp-Session-Id`) servers.
1212

1313
## Install
1414

@@ -32,13 +32,13 @@ Lists the tools on a public MCP server:
3232
import { mcpInitialize, mcpListTools } from "@corbits/mcp";
3333

3434
const url = "https://mcp.deepwiki.com/mcp";
35-
await mcpInitialize(url);
36-
for (const tool of await mcpListTools(url)) {
35+
const session = await mcpInitialize(url);
36+
for (const tool of await mcpListTools(url, { session })) {
3737
console.log(`${tool.name}: ${tool.description ?? ""}`);
3838
}
3939
```
4040

41-
Every client call takes `{ fetch, timeoutMs, signal }` 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.
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.
4242

4343
## Where it fits
4444

@@ -52,7 +52,7 @@ Every client call takes `{ fetch, timeoutMs, signal }` as its last argument, for
5252

5353
| Export | Entry | Purpose |
5454
| ------------------------------------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------- |
55-
| `mcpInitialize(url, opts?)` | `@corbits/mcp` | Send `initialize`; returns `serverInfo` and `protocolVersion`. |
55+
| `mcpInitialize(url, opts?)` | `@corbits/mcp` | Send `initialize` and `notifications/initialized`; returns the `McpSession` to pass to later calls. |
5656
| `mcpListTools(url, opts?)` | `@corbits/mcp` | Send `tools/list`; returns `McpTool[]`. |
5757
| `mcpCallTool(url, name, args, opts?)` | `@corbits/mcp` | Send `tools/call`; returns `McpToolResult`. |
5858
| `mcpTools(options, env?)` | `@corbits/mcp` | Discover each server live, then build tools. For hosts that can await; servers use `name` and `credentialHandle`. |
@@ -174,6 +174,7 @@ Grant the agent's principal `tool:deepwiki.*` for the whole server, or `tool:dee
174174
- `@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`.
175175
- 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.
176176
- `readCredentialSecret` is now `readCredential`, which returns `{ secret, origin? }`. `discoverMcpServer` takes `credential: { secret, origin? }` instead of `secret`.
177+
- `mcpInitialize` returns an `McpSession` (`protocolVersion`, `sessionId?`, `serverInfo?`) and sends `notifications/initialized`. Pass it as `{ session }` to `mcpListTools` and `mcpCallTool`; a stateful server needs it.
177178
- `@intx/agent` and `@intx/harness` peers are `^0.4.0`.
178179
- Discovery rejects authorization-server metadata whose `issuer` differs from the one the protected resource names.
179180

0 commit comments

Comments
 (0)