Skip to content

Commit da55f6b

Browse files
committed
fix: mega typing
1 parent 7268b17 commit da55f6b

File tree

6 files changed

+210
-81
lines changed

6 files changed

+210
-81
lines changed

docs/mcp-transports.md

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,14 +306,50 @@ The RPC transport is minimal by design (~350 lines) and fully supports:
306306
- Notifications (messages without `id` field)
307307
- Automatic reconnection after Durable Object hibernation
308308

309+
### Configuring RPC Transport Server Timeout
310+
311+
The RPC transport has a configurable timeout for waiting for tool responses. By default, the server will wait **60 seconds** for a tool handler to call `send()`. You can customize this by overriding the `getRpcTransportOptions()` method in your `McpAgent`:
312+
313+
```typescript
314+
export class MyMCP extends McpAgent<Env, State, {}> {
315+
server = new McpServer({
316+
name: "MyMCP",
317+
version: "1.0.0"
318+
});
319+
320+
// Configure RPC transport timeout
321+
protected getRpcTransportOptions() {
322+
return {
323+
timeout: 120000 // 2 minutes (default is 60000)
324+
};
325+
}
326+
327+
async init() {
328+
this.server.tool(
329+
"long-running-task",
330+
"A tool that takes a while to complete",
331+
{ input: z.string() },
332+
async ({ input }) => {
333+
// This tool has up to 2 minutes to complete
334+
await longRunningOperation(input);
335+
return {
336+
content: [{ type: "text", text: "Task completed" }]
337+
};
338+
}
339+
);
340+
}
341+
}
342+
```
343+
344+
The timeout ensures that if a tool handler fails to respond (e.g., due to an infinite loop or forgotten `send()` call), the request will fail with a clear timeout error rather than hanging indefinitely.
345+
309346
### Advanced: Custom RPC function names
310347

311348
By default, the RPC transport calls the `handleMcpMessage` function. You can customize this:
312349

313350
```typescript
314351
await this.addMcpServer("my-server", "MyMCP", {
315-
transport: { type: "rpc" },
316-
functionName: "customHandler"
352+
transport: { type: "rpc", functionName: "customHandler" }
317353
});
318354
```
319355

packages/agents/src/index.ts

Lines changed: 14 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ import type {
3737
import { genericObservability, type Observability } from "./observability";
3838
import { DisposableStore } from "./core/events";
3939
import { MessageType } from "./ai-types";
40-
import type { McpAgent } from "./mcp";
40+
import type {
41+
McpAgent,
42+
RpcConnectionOptions,
43+
HttpConnectionOptions
44+
} from "./mcp";
4145

4246
export type { Connection, ConnectionContext, WSMessage } from "partyserver";
4347

@@ -1470,12 +1474,7 @@ export class Agent<
14701474
>(
14711475
serverName: string,
14721476
binding: DurableObjectNamespace<T> | string,
1473-
options: {
1474-
transport: { type: "rpc" };
1475-
functionName?: string;
1476-
client?: ConstructorParameters<typeof Client>[1];
1477-
props?: T extends McpAgent<unknown, unknown, infer Props> ? Props : never;
1478-
}
1477+
options: RpcConnectionOptions<T>
14791478
): Promise<{ id: string; authUrl: undefined }>;
14801479

14811480
// Overload for HTTP/SSE transport
@@ -1484,38 +1483,17 @@ export class Agent<
14841483
url: string,
14851484
callbackHost?: string,
14861485
agentsPrefix?: string,
1487-
options?: {
1488-
client?: ConstructorParameters<typeof Client>[1];
1489-
transport?: {
1490-
headers?: HeadersInit;
1491-
type?: Exclude<TransportType, "rpc">;
1492-
};
1493-
}
1486+
options?: HttpConnectionOptions
14941487
): Promise<{ id: string; authUrl: string | undefined }>;
14951488

14961489
async addMcpServer<
14971490
T extends McpAgent<unknown, unknown, Record<string, unknown>> = McpAgent
14981491
>(
14991492
serverName: string,
15001493
urlOrBinding: string | DurableObjectNamespace<T>,
1501-
callbackHostOrOptions?:
1502-
| string
1503-
| {
1504-
transport: { type: "rpc" };
1505-
functionName?: string;
1506-
client?: ConstructorParameters<typeof Client>[1];
1507-
props?: T extends McpAgent<unknown, unknown, infer Props>
1508-
? Props
1509-
: never;
1510-
},
1494+
callbackHostOrOptions?: string | RpcConnectionOptions<T>,
15111495
agentsPrefix = "agents",
1512-
options?: {
1513-
client?: ConstructorParameters<typeof Client>[1];
1514-
transport?: {
1515-
headers?: HeadersInit;
1516-
type?: TransportType;
1517-
};
1518-
}
1496+
options?: HttpConnectionOptions
15191497
): Promise<{ id: string; authUrl: string | undefined }> {
15201498
// Determine if this is RPC or HTTP based on parameters
15211499
const isRpc =
@@ -1524,14 +1502,7 @@ export class Agent<
15241502

15251503
if (isRpc) {
15261504
// RPC transport path
1527-
const rpcOptions = callbackHostOrOptions as {
1528-
transport: { type: "rpc" };
1529-
functionName?: string;
1530-
client?: ConstructorParameters<typeof Client>[1];
1531-
props?: T extends McpAgent<unknown, unknown, infer Props>
1532-
? Props
1533-
: never;
1534-
};
1505+
const rpcOptions = callbackHostOrOptions as RpcConnectionOptions<T>;
15351506

15361507
const namespace = this._resolveRpcBinding<T>(
15371508
urlOrBinding as DurableObjectNamespace<T> | string
@@ -1551,11 +1522,7 @@ export class Agent<
15511522
serverName,
15521523
namespace,
15531524
url,
1554-
options: {
1555-
functionName: rpcOptions.functionName,
1556-
client: rpcOptions.client,
1557-
props: rpcOptions.props
1558-
},
1525+
options: rpcOptions,
15591526
reconnect
15601527
});
15611528

@@ -1770,14 +1737,14 @@ export class Agent<
17701737

17711738
await stub.setName(`rpc:${serverName}`);
17721739

1773-
if (options?.props) {
1774-
await stub.updateProps(options.props);
1740+
if (options?.transport?.props) {
1741+
await stub.updateProps(options.transport.props);
17751742
}
17761743

17771744
return {
17781745
type: "rpc",
17791746
stub,
1780-
functionName: options?.functionName
1747+
functionName: options?.transport?.functionName
17811748
};
17821749
}
17831750

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

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,12 @@ import {
3434
import { SSEEdgeClientTransport } from "./sse-edge";
3535
import { StreamableHTTPEdgeClientTransport } from "./streamable-http-edge";
3636
import { RPCClientTransport, type RPCClientTransportOptions } from "./rpc";
37-
import type { BaseTransportType, TransportType } from "./types";
37+
import type {
38+
BaseTransportType,
39+
TransportType,
40+
McpClientOptions,
41+
HttpTransportType
42+
} from "./types";
3843

3944
/**
4045
* Connection state for MCP client connections
@@ -46,12 +51,18 @@ export type MCPConnectionState =
4651
| "discovering"
4752
| "failed";
4853

54+
/**
55+
* Transport options for MCP client connections.
56+
* Combines transport-specific options with auth provider and type selection.
57+
*/
4958
export type MCPTransportOptions = (
5059
| SSEClientTransportOptions
5160
| StreamableHTTPClientTransportOptions
5261
| RPCClientTransportOptions
5362
) & {
63+
/** Optional OAuth provider for authenticating with the MCP server */
5464
authProvider?: AgentsOAuthProvider;
65+
/** The transport type to use. "auto" will try streamable-http, then fall back to SSE */
5566
type?: TransportType;
5667
};
5768

@@ -75,7 +86,7 @@ export class MCPClientConnection {
7586
info: ConstructorParameters<typeof Client>[0],
7687
public options: {
7788
transport: MCPTransportOptions;
78-
client: ConstructorParameters<typeof Client>[1];
89+
client: McpClientOptions;
7990
} = { client: {}, transport: {} }
8091
) {
8192
const clientOptions = {
@@ -144,10 +155,7 @@ export class MCPClientConnection {
144155
throw new Error("Transport type must be specified");
145156
}
146157

147-
const finishAuth = async (base: BaseTransportType) => {
148-
if (base === "rpc") {
149-
throw new Error("RPC transport does not support authentication");
150-
}
158+
const finishAuth = async (base: HttpTransportType) => {
151159
const transport = this.getTransport(base);
152160
if (
153161
"finishAuth" in transport &&

packages/agents/src/mcp/index.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
MCP_MESSAGE_HEADER
2121
} from "./utils";
2222
import { McpSSETransport, StreamableHTTPServerTransport } from "./transport";
23-
import { RPCServerTransport } from "./rpc";
23+
import { RPCServerTransport, type RPCServerTransportOptions } from "./rpc";
2424

2525
export abstract class McpAgent<
2626
Env = unknown,
@@ -88,6 +88,23 @@ export abstract class McpAgent<
8888
return websockets[0];
8989
}
9090

91+
/**
92+
* Returns options for configuring the RPC server transport.
93+
* Override this method to customize RPC transport behavior (e.g., timeout).
94+
*
95+
* @example
96+
* ```typescript
97+
* class MyMCP extends McpAgent {
98+
* protected getRpcTransportOptions() {
99+
* return { timeout: 120000 }; // 2 minutes
100+
* }
101+
* }
102+
* ```
103+
*/
104+
protected getRpcTransportOptions(): RPCServerTransportOptions {
105+
return {};
106+
}
107+
91108
/** Returns a new transport matching the type of the Agent. */
92109
private initTransport() {
93110
switch (this.getTransportType()) {
@@ -98,7 +115,7 @@ export abstract class McpAgent<
98115
return new StreamableHTTPServerTransport({});
99116
}
100117
case "rpc": {
101-
return new RPCServerTransport();
118+
return new RPCServerTransport(this.getRpcTransportOptions());
102119
}
103120
}
104121
}
@@ -372,7 +389,7 @@ export abstract class McpAgent<
372389
): Promise<JSONRPCMessage | JSONRPCMessage[] | undefined> {
373390
if (!this._transport) {
374391
const server = await this.server;
375-
this._transport = new RPCServerTransport();
392+
this._transport = new RPCServerTransport(this.getRpcTransportOptions());
376393
await server.connect(this._transport);
377394
}
378395

@@ -489,3 +506,12 @@ export type {
489506
MCPClientOAuthResult,
490507
MCPClientOAuthCallbackConfig
491508
} from "./client";
509+
510+
// Export connection configuration types
511+
export type {
512+
RpcConnectionOptions,
513+
HttpConnectionOptions,
514+
McpClientOptions,
515+
RpcTransportOptions,
516+
HttpTransportOptions
517+
} from "./types";

packages/agents/src/mcp/rpc.ts

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,21 @@ export interface RPCServerTransportOptions {
319319
* ```
320320
*/
321321
onsessionclosed?: (sessionId: string) => void | Promise<void>;
322+
323+
/**
324+
* Timeout in milliseconds for waiting for a response from the onmessage handler.
325+
* If the handler doesn't call send() within this time, the request will fail with a timeout error.
326+
*
327+
* @default 60000 (60 seconds)
328+
*
329+
* @example
330+
* ```typescript
331+
* const transport = new RPCServerTransport({
332+
* timeout: 30000 // 30 seconds
333+
* });
334+
* ```
335+
*/
336+
timeout?: number;
322337
}
323338

324339
export class RPCServerTransport implements Transport {
@@ -331,6 +346,7 @@ export class RPCServerTransport implements Transport {
331346
private _initialized = false;
332347
private _onsessioninitialized?: (sessionId: string) => void | Promise<void>;
333348
private _onsessionclosed?: (sessionId: string) => void | Promise<void>;
349+
private _timeout: number;
334350

335351
sessionId?: string;
336352
onclose?: () => void;
@@ -341,6 +357,7 @@ export class RPCServerTransport implements Transport {
341357
this._sessionIdGenerator = options?.sessionIdGenerator;
342358
this._onsessioninitialized = options?.onsessioninitialized;
343359
this._onsessionclosed = options?.onsessionclosed;
360+
this._timeout = options?.timeout ?? 60000; // Default 60 seconds
344361
}
345362

346363
setProtocolVersion(version: string): void {
@@ -402,12 +419,19 @@ export class RPCServerTransport implements Transport {
402419
this._pendingResponse = [this._pendingResponse, message];
403420
}
404421

405-
// Resolve the promise on the next tick to allow multiple sends to accumulate
422+
// Resolve the promise on the next tick to allow multiple send() calls to accumulate
423+
// Note: Only the first send() triggers resolution; subsequent sends just accumulate
424+
// until the microtask executes and handle() returns all responses
406425
if (this._responseResolver) {
407426
const resolver = this._responseResolver;
408427
this._responseResolver = null;
409428
// Use queueMicrotask to allow additional send() calls to accumulate
410429
queueMicrotask(() => resolver());
430+
} else if (this._currentRequestId !== null) {
431+
// This shouldn't happen - send() called after promise already resolved
432+
console.warn(
433+
`send() called after response already resolved for request ${this._currentRequestId}`
434+
);
411435
}
412436
}
413437

@@ -516,14 +540,40 @@ export class RPCServerTransport implements Transport {
516540
this._currentRequestId = (message as { id: string | number | null }).id;
517541

518542
// Set up the promise before calling onmessage to handle race conditions
519-
const responsePromise = new Promise<void>((resolve) => {
520-
this._responseResolver = resolve;
543+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
544+
const responsePromise = new Promise<void>((resolve, reject) => {
545+
// Set up timeout
546+
timeoutId = setTimeout(() => {
547+
this._responseResolver = null;
548+
reject(
549+
new Error(
550+
`Request timeout: No response received within ${this._timeout}ms for request ID ${this._currentRequestId}`
551+
)
552+
);
553+
}, this._timeout);
554+
555+
// Wrap the resolver to clear timeout when response is received
556+
this._responseResolver = () => {
557+
if (timeoutId) {
558+
clearTimeout(timeoutId);
559+
timeoutId = null;
560+
}
561+
resolve();
562+
};
521563
});
522564

523565
this.onmessage?.(message);
524566

525567
// Wait for a response using a promise that resolves when send() is called
526-
await responsePromise;
568+
try {
569+
await responsePromise;
570+
} catch (error) {
571+
// Clean up on timeout
572+
this._pendingResponse = null;
573+
this._currentRequestId = null;
574+
this._responseResolver = null;
575+
throw error;
576+
}
527577

528578
const response = this._pendingResponse;
529579
this._pendingResponse = null;

0 commit comments

Comments
 (0)