Skip to content

Commit 885b3db

Browse files
authored
Feature/http streamable support (#364)
* add HTTP Streamable support * docs update * Add check script to package.json - Add npm run check script that runs the core tests - Simplifies CI/CD testing by providing standard check command * Revert "Add check script to package.json" This reverts commit be3aca9. * updated README and example to use HTTP streaming * updated docs * prettier fix * removed client tests, fixed readme
1 parent 579b228 commit 885b3db

File tree

14 files changed

+1081
-763
lines changed

14 files changed

+1081
-763
lines changed

.changeset/modern-geckos-check.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"agents": patch
3+
---
4+
5+
add HTTP Streamable support

examples/mcp-client/README.md

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
# MCP Client Demo Using Agents
22

3-
A minimal example showing an `Agent` as an MCP client.
3+
A minimal example showing an `Agent` as an MCP client with support for both SSE and HTTP Streamable transports.
4+
5+
## Transport Options
6+
7+
The MCP client supports two transport types:
8+
9+
- **HTTP Streamable** (recommended): Uses HTTP POST + SSE for better performance and reliability
10+
- **SSE (Server-Sent Events)**
411

512
## Instructions
613

@@ -15,8 +22,34 @@ Then, follow the steps below to setup the client:
1522

1623
Tap "O + enter" to open the front end. It should list out all the tools, prompts, and resources available for each server added.
1724

18-
## Troubleshooting
19-
20-
### TypeError: Cannot read properties of undefined (reading 'connectionState')
21-
22-
Clear the Local Storage cookies in your browser, then restart the client.
25+
## Transport Configuration
26+
27+
The MCP client defaults to HTTP Streamable transport for better performance. You can specify transport type explicitly:
28+
29+
```typescript
30+
// Using MCPClientManager directly
31+
const mcpClient = new MCPClientManager("my-app", "1.0.0");
32+
33+
// HTTP Streamable transport (default, recommended)
34+
await mcpClient.connect(serverUrl, {
35+
transport: {
36+
type: "streamable-http",
37+
authProvider: myAuthProvider
38+
}
39+
});
40+
41+
// SSE transport (legacy compatibility)
42+
await mcpClient.connect(serverUrl, {
43+
transport: {
44+
type: "sse",
45+
authProvider: myAuthProvider
46+
}
47+
});
48+
49+
// Or using Agent.addMcpServer() (as shown in the example)
50+
export class MyAgent extends Agent<Env, never> {
51+
async addServer(name: string, url: string, callbackHost: string) {
52+
await this.addMcpServer(name, url, callbackHost);
53+
}
54+
}
55+
```

examples/mcp/src/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,6 @@ export class MyMCP extends McpAgent<Env, State, {}> {
4949
}
5050
}
5151

52-
export default MyMCP.mount("/sse", {
52+
export default MyMCP.serve("/mcp", {
5353
binding: "MyMCP"
5454
});

packages/agents/README.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,111 @@ This creates:
393393
- Intuitive input handling
394394
- Easy conversation reset
395395

396+
### 🔗 MCP (Model Context Protocol) Integration
397+
398+
Agents can seamlessly integrate with the Model Context Protocol, allowing them to act as both MCP servers (providing tools to AI assistants) and MCP clients (using tools from other services).
399+
400+
#### Creating an MCP Server
401+
402+
```typescript
403+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
404+
import { McpAgent } from "agents/mcp";
405+
import { z } from "zod";
406+
407+
type Env = {
408+
MyMCP: DurableObjectNamespace<MyMCP>;
409+
};
410+
411+
type State = { counter: number };
412+
413+
export class MyMCP extends McpAgent<Env, State, {}> {
414+
server = new McpServer({
415+
name: "Demo",
416+
version: "1.0.0"
417+
});
418+
419+
initialState: State = {
420+
counter: 1
421+
};
422+
423+
async init() {
424+
this.server.resource("counter", "mcp://resource/counter", (uri) => {
425+
return {
426+
contents: [{ text: String(this.state.counter), uri: uri.href }]
427+
};
428+
});
429+
430+
this.server.tool(
431+
"add",
432+
"Add to the counter, stored in the MCP",
433+
{ a: z.number() },
434+
async ({ a }) => {
435+
this.setState({ ...this.state, counter: this.state.counter + a });
436+
437+
return {
438+
content: [
439+
{
440+
text: String(`Added ${a}, total is now ${this.state.counter}`),
441+
type: "text"
442+
}
443+
]
444+
};
445+
}
446+
);
447+
}
448+
449+
onStateUpdate(state: State) {
450+
console.log({ stateUpdate: state });
451+
}
452+
}
453+
454+
// HTTP Streamable transport (recommended)
455+
export default MyMCP.serve("/mcp", {
456+
binding: "MyMCP"
457+
});
458+
459+
// Or SSE transport for legacy compatibility
460+
// export default MyMCP.serveSSE("/mcp", { binding: "MyMCP" });
461+
```
462+
463+
#### Using MCP Tools
464+
465+
```typescript
466+
import { MCPClientManager } from "agents/mcp";
467+
468+
const client = new MCPClientManager("my-app", "1.0.0");
469+
470+
// Connect to an MCP server
471+
await client.connect("https://weather-service.com/mcp", {
472+
transport: { type: "streamable-http" }
473+
});
474+
475+
// Use tools from the server
476+
const weather = await client.callTool({
477+
serverId: "weather-service",
478+
name: "getWeather",
479+
arguments: { location: "San Francisco" }
480+
});
481+
```
482+
483+
#### AI SDK Integration
484+
485+
```typescript
486+
import { generateText } from "ai";
487+
488+
// Convert MCP tools for AI use
489+
const result = await generateText({
490+
model: openai("gpt-4"),
491+
tools: client.unstable_getAITools(),
492+
prompt: "What's the weather in Tokyo?"
493+
});
494+
```
495+
496+
**Transport Options:**
497+
498+
- **HTTP Streamable**: Best performance, batch requests, session management
499+
- **SSE**: Simple setup, legacy compatibility
500+
396501
### 💬 The Path Forward
397502

398503
We're developing new dimensions of agent capability:

packages/agents/src/mcp/client-connection.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
22
import type { SSEClientTransportOptions } from "@modelcontextprotocol/sdk/client/sse.js";
3+
import type { StreamableHTTPClientTransportOptions } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
34
import {
45
// type ClientCapabilities,
56
type ListPromptsResult,
@@ -18,6 +19,15 @@ import {
1819
} from "@modelcontextprotocol/sdk/types.js";
1920
import type { AgentsOAuthProvider } from "./do-oauth-client-provider";
2021
import { SSEEdgeClientTransport } from "./sse-edge";
22+
import { StreamableHTTPEdgeClientTransport } from "./streamable-http-edge";
23+
24+
export type MCPTransportOptions = (
25+
| SSEClientTransportOptions
26+
| StreamableHTTPClientTransportOptions
27+
) & {
28+
authProvider?: AgentsOAuthProvider;
29+
type?: "sse" | "streamable-http";
30+
};
2131

2232
export class MCPClientConnection {
2333
client: Client;
@@ -38,9 +48,7 @@ export class MCPClientConnection {
3848
public url: URL,
3949
info: ConstructorParameters<typeof Client>[0],
4050
public options: {
41-
transport: SSEClientTransportOptions & {
42-
authProvider?: AgentsOAuthProvider;
43-
};
51+
transport: MCPTransportOptions;
4452
client: ConstructorParameters<typeof Client>[1];
4553
} = { client: {}, transport: {} }
4654
) {
@@ -55,10 +63,17 @@ export class MCPClientConnection {
5563
*/
5664
async init(code?: string) {
5765
try {
58-
const transport = new SSEEdgeClientTransport(
59-
this.url,
60-
this.options.transport
61-
);
66+
const transportType = this.options.transport.type || "streamable-http";
67+
const transport =
68+
transportType === "streamable-http"
69+
? new StreamableHTTPEdgeClientTransport(
70+
this.url,
71+
this.options.transport as StreamableHTTPClientTransportOptions
72+
)
73+
: new SSEEdgeClientTransport(
74+
this.url,
75+
this.options.transport as SSEClientTransportOptions
76+
);
6277

6378
if (code) {
6479
await transport.finishAuth(code);

packages/agents/src/mcp/client.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
2-
import type { SSEClientTransportOptions } from "@modelcontextprotocol/sdk/client/sse.js";
32
import type { RequestOptions } from "@modelcontextprotocol/sdk/shared/protocol.js";
43
import type {
54
CallToolRequest,
@@ -14,8 +13,10 @@ import type {
1413
} from "@modelcontextprotocol/sdk/types.js";
1514
import { type ToolSet, jsonSchema } from "ai";
1615
import { nanoid } from "nanoid";
17-
import { MCPClientConnection } from "./client-connection";
18-
import type { AgentsOAuthProvider } from "./do-oauth-client-provider";
16+
import {
17+
MCPClientConnection,
18+
type MCPTransportOptions
19+
} from "./client-connection";
1920

2021
/**
2122
* Utility class that aggregates multiple MCP clients into one
@@ -52,9 +53,7 @@ export class MCPClientManager {
5253
oauthCode?: string;
5354
};
5455
// we're overriding authProvider here because we want to be able to access the auth URL
55-
transport?: SSEClientTransportOptions & {
56-
authProvider?: AgentsOAuthProvider;
57-
};
56+
transport?: MCPTransportOptions;
5857
client?: ConstructorParameters<typeof Client>[1];
5958
} = {}
6059
): Promise<{

packages/agents/src/mcp/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
isJSONRPCResponse
1313
} from "@modelcontextprotocol/sdk/types.js";
1414
import type { Connection, WSMessage } from "../";
15-
import { Agent } from "../";
15+
import { Agent } from "../index";
1616

1717
const MAXIMUM_MESSAGE_SIZE_BYTES = 4 * 1024 * 1024; // 4MB
1818

@@ -1149,3 +1149,7 @@ export abstract class McpAgent<
11491149
};
11501150
}
11511151
}
1152+
1153+
// Export client transport classes
1154+
export { SSEEdgeClientTransport } from "./sse-edge";
1155+
export { StreamableHTTPEdgeClientTransport } from "./streamable-http-edge";
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
2+
import {
3+
StreamableHTTPClientTransport,
4+
type StreamableHTTPClientTransportOptions
5+
} from "@modelcontextprotocol/sdk/client/streamableHttp.js";
6+
7+
export class StreamableHTTPEdgeClientTransport extends StreamableHTTPClientTransport {
8+
private authProvider: OAuthClientProvider | undefined;
9+
10+
/**
11+
* Creates a new StreamableHTTPEdgeClientTransport, which overrides fetch to be compatible with the CF workers environment
12+
*/
13+
constructor(url: URL, options: StreamableHTTPClientTransportOptions) {
14+
const fetchOverride: typeof fetch = async (
15+
fetchUrl: RequestInfo | URL,
16+
fetchInit: RequestInit = {}
17+
) => {
18+
// add auth headers
19+
const headers = await this.authHeaders();
20+
const workerOptions = {
21+
...fetchInit,
22+
headers: {
23+
...options.requestInit?.headers,
24+
...fetchInit?.headers,
25+
...headers
26+
}
27+
};
28+
29+
// Remove unsupported properties
30+
delete workerOptions.mode;
31+
32+
// Call the original fetch with fixed options
33+
return (
34+
// @ts-expect-error Custom fetch function for Cloudflare Workers compatibility
35+
(options.requestInit?.fetch?.(
36+
fetchUrl as URL | string,
37+
workerOptions
38+
) as Promise<Response>) || fetch(fetchUrl, workerOptions)
39+
);
40+
};
41+
42+
super(url, {
43+
...options,
44+
requestInit: {
45+
...options.requestInit,
46+
// @ts-expect-error Custom fetch override for Cloudflare Workers
47+
fetch: fetchOverride
48+
}
49+
});
50+
this.authProvider = options.authProvider;
51+
}
52+
53+
async authHeaders() {
54+
if (this.authProvider) {
55+
const tokens = await this.authProvider.tokens();
56+
if (tokens) {
57+
return {
58+
Authorization: `Bearer ${tokens.access_token}`
59+
};
60+
}
61+
}
62+
}
63+
}

0 commit comments

Comments
 (0)