@procedures/mcp-server 0.2.3 → 0.3.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.
- package/CHANGELOG.md +49 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/procedures/mcp/list-tools.d.ts.map +1 -1
- package/dist/procedures/mcp/list-tools.js +2 -1
- package/dist/procedures/mcp/list-tools.js.map +1 -1
- package/dist/procedures/mcp/serve.d.ts.map +1 -1
- package/dist/procedures/mcp/serve.js +2 -1
- package/dist/procedures/mcp/serve.js.map +1 -1
- package/dist/transport/http.d.ts +28 -0
- package/dist/transport/http.d.ts.map +1 -0
- package/dist/transport/http.js +114 -0
- package/dist/transport/http.js.map +1 -0
- package/dist/transport/mcp-transport.d.ts +12 -0
- package/dist/transport/mcp-transport.d.ts.map +1 -1
- package/dist/transport/mcp-transport.js +75 -12
- package/dist/transport/mcp-transport.js.map +1 -1
- package/dist/types.d.ts +35 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/procedures/mcp/list-tools.ts +2 -1
- package/src/procedures/mcp/serve.ts +2 -1
- package/src/transport/http.ts +129 -0
- package/src/transport/mcp-transport.ts +75 -15
- package/src/types.ts +35 -3
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@procedures/mcp-server",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"description": "Model Context Protocol server transport: serve typed procedures as MCP tools to Claude and other MCP clients, with streaming progress, list_changed and an expose policy.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"mcp",
|
|
@@ -60,17 +60,17 @@
|
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
63
|
-
"@procedures/mcp": "0.
|
|
63
|
+
"@procedures/mcp": "0.3.0"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
66
|
-
"@procedures/client": "^0.
|
|
66
|
+
"@procedures/client": "^0.3.0"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"typescript": "^7.0.2",
|
|
70
70
|
"@types/node": "^22.0.0",
|
|
71
71
|
"@mark1russell7/cue": "^1.0.0",
|
|
72
72
|
"vitest": "^5.0.3",
|
|
73
|
-
"@procedures/client": "0.
|
|
73
|
+
"@procedures/client": "0.3.0",
|
|
74
74
|
"zod": "^4.6.5"
|
|
75
75
|
},
|
|
76
76
|
"scripts": {
|
|
@@ -67,7 +67,8 @@ export const mcpListToolsProcedure: AnyProcedure = defineProcedure({
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
// The registry of the caller: a host with its own registry lists its own tools
|
|
70
|
-
|
|
70
|
+
// Only the procedures that the host exposes (roadmap R17)
|
|
71
|
+
const procedures = (ctx?.registry ?? PROCEDURE_REGISTRY).getAll().filter((procedure) => !ctx?.expose || ctx.expose(procedure.path));
|
|
71
72
|
const mcpTools = proceduresToMcpTools(procedures, filter);
|
|
72
73
|
|
|
73
74
|
const tools: McpToolInfo[] = mcpTools.map((tool) => ({
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
import { McpServerTransport } from "../../transport/mcp-transport.js";
|
|
26
26
|
import type { McpServerTransportOptions } from "../../types.js";
|
|
27
27
|
import { exposeRule, importBundles, mergeSurfaces } from "../../surface.js";
|
|
28
|
+
import { ms } from "@procedures/client";
|
|
28
29
|
|
|
29
30
|
/** The default bundle of the tool surface. */
|
|
30
31
|
export const DEFAULT_SERVE_BUNDLE = "@procedures/bundle-mcp";
|
|
@@ -129,7 +130,7 @@ export async function prepareServe(
|
|
|
129
130
|
registry,
|
|
130
131
|
expose,
|
|
131
132
|
strictCalls: input.strictCalls ?? true,
|
|
132
|
-
interceptors: [timeoutInterceptor({
|
|
133
|
+
interceptors: [timeoutInterceptor({ limit: ms(input.toolTimeoutMs ?? 600_000) })],
|
|
133
134
|
});
|
|
134
135
|
return warning === undefined ? { server, expose } : { server, expose, warning };
|
|
135
136
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MCP transport over Streamable HTTP (roadmap R26), the HTTP transport of the current MCP
|
|
3
|
+
* specification. Before, this package had only stdio, and its SSE transport only threw.
|
|
4
|
+
*
|
|
5
|
+
* Each client gets a session: its `initialize` request makes a transport and an MCP server of its
|
|
6
|
+
* own, and the later requests of the client carry the `mcp-session-id` header of that session.
|
|
7
|
+
* A session gets the progress of a tool call and the `list_changed` notifications. The checks of
|
|
8
|
+
* a local server apply: the server listens on loopback by default, it refuses a web page of
|
|
9
|
+
* another host or origin (DNS rebinding, cross-site requests), and with a token it refuses a
|
|
10
|
+
* request without the token.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
14
|
+
import { createServer, type IncomingMessage, type Server as HttpServer, type ServerResponse } from "node:http";
|
|
15
|
+
import type { AddressInfo } from "node:net";
|
|
16
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
17
|
+
import { bearerOf, checkBrowserRequest, tokenMatches } from "@procedures/client";
|
|
18
|
+
import type { HttpTransportOptions } from "../types.js";
|
|
19
|
+
|
|
20
|
+
/** The HTTP endpoint of an MCP server, with a session for each client. */
|
|
21
|
+
export interface McpHttpTransport {
|
|
22
|
+
/** This function starts to listen, when the endpoint has a server of its own. It gives the URL. */
|
|
23
|
+
listen(): Promise<string>;
|
|
24
|
+
/** The number of open sessions. */
|
|
25
|
+
readonly sessions: number;
|
|
26
|
+
/** This function closes each session, and the server when the endpoint made one. */
|
|
27
|
+
close(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* This function makes the Streamable HTTP endpoint of an MCP server. `connect` connects the
|
|
32
|
+
* transport of a new session to an MCP server of its own.
|
|
33
|
+
*/
|
|
34
|
+
export function createHttpTransport(
|
|
35
|
+
options: HttpTransportOptions,
|
|
36
|
+
connect: (transport: StreamableHTTPServerTransport) => Promise<void>,
|
|
37
|
+
): McpHttpTransport {
|
|
38
|
+
const path = options.path ?? "/mcp";
|
|
39
|
+
const host = options.host ?? "127.0.0.1";
|
|
40
|
+
const owned = options.server === undefined;
|
|
41
|
+
const server: HttpServer = options.server ?? createServer();
|
|
42
|
+
const sessions = new Map<string, StreamableHTTPServerTransport>();
|
|
43
|
+
|
|
44
|
+
const refuse = (res: ServerResponse, status: number, code: string, message: string): void => {
|
|
45
|
+
res.statusCode = status;
|
|
46
|
+
res.setHeader("content-type", "application/json");
|
|
47
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32001, message, data: { code } }, id: null }));
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** The transport of a request: the one of its session, or a new one for an `initialize`. */
|
|
51
|
+
const transportOf = async (req: IncomingMessage): Promise<StreamableHTTPServerTransport | undefined> => {
|
|
52
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
53
|
+
if (typeof sessionId === "string") return sessions.get(sessionId);
|
|
54
|
+
if (req.method !== "POST") return undefined;
|
|
55
|
+
const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
|
|
56
|
+
sessionIdGenerator: () => randomUUID(),
|
|
57
|
+
onsessioninitialized: (id) => {
|
|
58
|
+
sessions.set(id, transport);
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
transport.onclose = () => {
|
|
62
|
+
if (transport.sessionId) sessions.delete(transport.sessionId);
|
|
63
|
+
};
|
|
64
|
+
await connect(transport);
|
|
65
|
+
return transport;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const onRequest = (req: IncomingMessage, res: ServerResponse): void => {
|
|
69
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
70
|
+
if (url.pathname !== path) {
|
|
71
|
+
if (owned) refuse(res, 404, "NOT_FOUND", `No MCP endpoint at ${url.pathname}`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// A web page must not reach the tools: the host and the origin of the request
|
|
75
|
+
const guard = checkBrowserRequest(req, { ...(options.allowedOrigins ? { allowedOrigins: options.allowedOrigins } : {}) });
|
|
76
|
+
if (!guard.ok) {
|
|
77
|
+
refuse(res, guard.status, guard.code, guard.message);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (options.token !== undefined && !tokenMatches(options.token, bearerOf(req.headers["authorization"]))) {
|
|
81
|
+
refuse(res, 401, "UNAUTHORIZED", "Unauthorized: this server requires its token");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
void transportOf(req)
|
|
85
|
+
.then((transport) => {
|
|
86
|
+
if (!transport) {
|
|
87
|
+
refuse(res, 404, "SESSION_NOT_FOUND", "No session: start one with an initialize request");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
return transport.handleRequest(req, res);
|
|
91
|
+
})
|
|
92
|
+
.catch((error: unknown) => {
|
|
93
|
+
if (!res.headersSent) refuse(res, 500, "INTERNAL_ERROR", error instanceof Error ? error.message : String(error));
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
server.on("request", onRequest);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
listen: async () => {
|
|
100
|
+
if (owned && !server.listening) {
|
|
101
|
+
await new Promise<void>((resolve, reject) => {
|
|
102
|
+
server.once("error", reject);
|
|
103
|
+
server.listen(options.port ?? 3003, host, () => {
|
|
104
|
+
server.off("error", reject);
|
|
105
|
+
resolve();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const address = server.address() as AddressInfo | null;
|
|
110
|
+
const shown = host === "0.0.0.0" ? "localhost" : host;
|
|
111
|
+
return `http://${shown}:${address?.port ?? options.port ?? 3003}${path}`;
|
|
112
|
+
},
|
|
113
|
+
get sessions() {
|
|
114
|
+
return sessions.size;
|
|
115
|
+
},
|
|
116
|
+
close: async () => {
|
|
117
|
+
server.off("request", onRequest);
|
|
118
|
+
const open = [...sessions.values()];
|
|
119
|
+
sessions.clear();
|
|
120
|
+
await Promise.all(open.map((transport) => transport.close().catch(() => undefined)));
|
|
121
|
+
if (owned && server.listening) {
|
|
122
|
+
await new Promise<void>((resolve) => {
|
|
123
|
+
server.close(() => resolve());
|
|
124
|
+
server.closeAllConnections();
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -23,6 +23,8 @@ import {
|
|
|
23
23
|
import type { McpServerTransportOptions, McpServerTransportState } from "../types.js";
|
|
24
24
|
import { createStdioTransport } from "./stdio.js";
|
|
25
25
|
import { createSseTransport } from "./sse.js";
|
|
26
|
+
import { createHttpTransport, type McpHttpTransport } from "./http.js";
|
|
27
|
+
import { optionalMs } from "@procedures/client";
|
|
26
28
|
|
|
27
29
|
/**
|
|
28
30
|
* Generate a unique request ID.
|
|
@@ -84,8 +86,15 @@ export class McpServerTransport implements ServerTransport {
|
|
|
84
86
|
private tools: Map<string, McpTool> = new Map();
|
|
85
87
|
private registerListener: ((procedure: unknown) => void) | undefined;
|
|
86
88
|
private unregisterListener: ((procedure: unknown) => void) | undefined;
|
|
89
|
+
/** The Streamable HTTP transport and its URL, when the transport is "http" (R26) */
|
|
90
|
+
private http: McpHttpTransport | undefined;
|
|
91
|
+
/** The MCP servers of the sessions of the Streamable HTTP transport */
|
|
92
|
+
private readonly sessionServers = new Set<McpServer>();
|
|
93
|
+
private httpUrl: string | undefined;
|
|
87
94
|
/** A pending list_changed notification: a burst of registrations sends one */
|
|
88
95
|
private listChangedTimer: ReturnType<typeof setTimeout> | undefined;
|
|
96
|
+
/** The limits of a stream that can run without an end, the time in milliseconds (R15) */
|
|
97
|
+
private readonly streamLimits: { items: number; timeMs: number };
|
|
89
98
|
|
|
90
99
|
constructor(server: Server, options: McpServerTransportOptions = {}) {
|
|
91
100
|
this.server = server;
|
|
@@ -97,6 +106,10 @@ export class McpServerTransport implements ServerTransport {
|
|
|
97
106
|
},
|
|
98
107
|
...options,
|
|
99
108
|
};
|
|
109
|
+
this.streamLimits = {
|
|
110
|
+
items: Math.max(1, options.streamLimits?.items ?? 100),
|
|
111
|
+
timeMs: optionalMs(options.streamLimits?.time, "streamLimits.time") ?? 30_000,
|
|
112
|
+
};
|
|
100
113
|
// The registry of the server, when it has one (a ProcedureServer with an explicit registry):
|
|
101
114
|
// before, the tools came from the global registry, also when the server served another one
|
|
102
115
|
this.registry =
|
|
@@ -106,25 +119,32 @@ export class McpServerTransport implements ServerTransport {
|
|
|
106
119
|
|
|
107
120
|
// Create MCP server with capabilities. The tool list follows the registry: when it changes,
|
|
108
121
|
// the server sends notifications/tools/list_changed (deep dive roadmap 0.4)
|
|
109
|
-
this.mcpServer =
|
|
122
|
+
this.mcpServer = this.createSdkServer();
|
|
123
|
+
|
|
124
|
+
// The registry listeners are attached in start() and removed in stop(): attaching them here
|
|
125
|
+
// leaked them for a transport that never started or failed to start (BUGS-2026-07 L28)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* An MCP server of the SDK with the handlers of this transport. The stdio transport has one, and
|
|
130
|
+
* each session of the Streamable HTTP transport has one of its own (roadmap R26).
|
|
131
|
+
*/
|
|
132
|
+
private createSdkServer(): McpServer {
|
|
133
|
+
const server = new McpServer(this.options.serverInfo, {
|
|
110
134
|
capabilities: {
|
|
111
135
|
tools: { listChanged: true },
|
|
112
136
|
},
|
|
113
137
|
});
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
this.setupHandlers();
|
|
117
|
-
|
|
118
|
-
// The registry listeners are attached in start() and removed in stop(): attaching them here
|
|
119
|
-
// leaked them for a transport that never started or failed to start (BUGS-2026-07 L28)
|
|
138
|
+
this.setupHandlers(server);
|
|
139
|
+
return server;
|
|
120
140
|
}
|
|
121
141
|
|
|
122
142
|
/**
|
|
123
143
|
* Setup MCP request handlers.
|
|
124
144
|
*/
|
|
125
|
-
private setupHandlers(): void {
|
|
145
|
+
private setupHandlers(server: McpServer): void {
|
|
126
146
|
// ListToolsRequestSchema -> List all procedures as tools
|
|
127
|
-
|
|
147
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
128
148
|
this.log("ListToolsRequest received");
|
|
129
149
|
|
|
130
150
|
const tools = Array.from(this.tools.values()).map(toToolDefinition);
|
|
@@ -135,7 +155,7 @@ export class McpServerTransport implements ServerTransport {
|
|
|
135
155
|
});
|
|
136
156
|
|
|
137
157
|
// CallToolRequestSchema -> Execute procedure
|
|
138
|
-
|
|
158
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
139
159
|
const { name, arguments: args } = request.params;
|
|
140
160
|
|
|
141
161
|
this.log(`CallToolRequest: ${name}`);
|
|
@@ -175,7 +195,8 @@ export class McpServerTransport implements ServerTransport {
|
|
|
175
195
|
|
|
176
196
|
// Convert to MCP result
|
|
177
197
|
if (response.status.type === "success" && response.stream) {
|
|
178
|
-
|
|
198
|
+
const unbounded = response.metadata?.["stream"] === "unbounded";
|
|
199
|
+
return await this.streamResult(response.stream, extra, unbounded);
|
|
179
200
|
}
|
|
180
201
|
if (response.status.type === "success") {
|
|
181
202
|
const structured = tool.outputSchema ? structuredContentOf(response.payload) : undefined;
|
|
@@ -186,14 +207,17 @@ export class McpServerTransport implements ServerTransport {
|
|
|
186
207
|
...(structured ? { structuredContent: structured } : {}),
|
|
187
208
|
};
|
|
188
209
|
} else {
|
|
210
|
+
// The code and the reason of the error travel on each face (roadmap R25)
|
|
211
|
+
const reason = typeof response.metadata?.["reason"] === "string" ? response.metadata["reason"] : undefined;
|
|
189
212
|
return {
|
|
190
213
|
content: [
|
|
191
214
|
{
|
|
192
215
|
type: "text" as const,
|
|
193
|
-
text: response.status.message ?? "Unknown error"
|
|
216
|
+
text: `${response.status.message ?? "Unknown error"}${reason ? ` (reason: ${reason})` : ""}`,
|
|
194
217
|
},
|
|
195
218
|
],
|
|
196
219
|
isError: true,
|
|
220
|
+
_meta: { code: response.status.code, ...(reason ? { reason } : {}) },
|
|
197
221
|
};
|
|
198
222
|
}
|
|
199
223
|
} catch (error) {
|
|
@@ -222,14 +246,25 @@ export class McpServerTransport implements ServerTransport {
|
|
|
222
246
|
extra: {
|
|
223
247
|
_meta?: { progressToken?: string | number | undefined } | undefined;
|
|
224
248
|
sendNotification: (notification: ServerNotification) => Promise<void>;
|
|
225
|
-
}
|
|
249
|
+
},
|
|
250
|
+
unbounded = false
|
|
226
251
|
): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
|
|
227
252
|
const progressToken = extra._meta?.progressToken;
|
|
228
253
|
const blocks: Array<{ type: "text"; text: string }> = [];
|
|
229
254
|
let count = 0;
|
|
255
|
+
// A stream that can run without an end stops at the first limit (roadmap R15: before, a
|
|
256
|
+
// docker.logs with follow ended only at the time limit of the tool call)
|
|
257
|
+
const limits = this.streamLimits;
|
|
258
|
+
const started = Date.now();
|
|
259
|
+
let stoppedBy: string | undefined;
|
|
230
260
|
try {
|
|
231
261
|
for await (const item of stream) {
|
|
232
262
|
count++;
|
|
263
|
+
if (unbounded && (count > limits.items || Date.now() - started > limits.timeMs)) {
|
|
264
|
+
stoppedBy = count > limits.items ? `${limits.items} items` : `${Math.round(limits.timeMs / 1000)} seconds`;
|
|
265
|
+
count--;
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
233
268
|
const text = toText(item);
|
|
234
269
|
blocks.push({ type: "text", text });
|
|
235
270
|
if (blocks.length > MAX_STREAM_BLOCKS) blocks.shift();
|
|
@@ -249,6 +284,9 @@ export class McpServerTransport implements ServerTransport {
|
|
|
249
284
|
if (count > MAX_STREAM_BLOCKS) {
|
|
250
285
|
blocks.unshift({ type: "text", text: `(${count} items: the first ${count - MAX_STREAM_BLOCKS} are not shown)` });
|
|
251
286
|
}
|
|
287
|
+
if (stoppedBy) {
|
|
288
|
+
blocks.push({ type: "text", text: `(The stream can run without an end, so the tool stopped it after ${stoppedBy}.)` });
|
|
289
|
+
}
|
|
252
290
|
if (count === 0) blocks.push({ type: "text", text: "(no items)" });
|
|
253
291
|
return { content: blocks };
|
|
254
292
|
}
|
|
@@ -313,6 +351,7 @@ export class McpServerTransport implements ServerTransport {
|
|
|
313
351
|
this.listChangedTimer = setTimeout(() => {
|
|
314
352
|
this.listChangedTimer = undefined;
|
|
315
353
|
if (!this.running) return;
|
|
354
|
+
for (const sessionServer of this.sessionServers) sessionServer.sendToolListChanged().catch(() => undefined);
|
|
316
355
|
this.mcpServer.sendToolListChanged().catch((error: unknown) => {
|
|
317
356
|
this.log(`list_changed failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
318
357
|
});
|
|
@@ -346,13 +385,22 @@ export class McpServerTransport implements ServerTransport {
|
|
|
346
385
|
|
|
347
386
|
try {
|
|
348
387
|
// Create appropriate transport
|
|
349
|
-
let transport: SdkTransport;
|
|
388
|
+
let transport: SdkTransport | undefined;
|
|
350
389
|
if (this.options.sdkTransport) {
|
|
351
390
|
transport = this.options.sdkTransport;
|
|
352
391
|
this.log("Using the given transport");
|
|
353
392
|
} else if (this.options.transport === "stdio") {
|
|
354
393
|
transport = createStdioTransport();
|
|
355
394
|
this.log("Using stdio transport");
|
|
395
|
+
} else if (this.options.transport === "http") {
|
|
396
|
+
// Streamable HTTP (roadmap R26): an MCP server of its own for each session
|
|
397
|
+
this.http = createHttpTransport(this.options.httpOptions ?? {}, async (sessionTransport) => {
|
|
398
|
+
const sessionServer = this.createSdkServer();
|
|
399
|
+
this.sessionServers.add(sessionServer);
|
|
400
|
+
sessionServer.onclose = () => this.sessionServers.delete(sessionServer);
|
|
401
|
+
await sessionServer.connect(sessionTransport as unknown as SdkTransport);
|
|
402
|
+
});
|
|
403
|
+
transport = undefined;
|
|
356
404
|
} else if (this.options.transport === "sse") {
|
|
357
405
|
transport = createSseTransport(this.options.sseOptions);
|
|
358
406
|
this.log(`Using SSE transport on ${this.options.sseOptions?.path ?? "/mcp/sse"}`);
|
|
@@ -361,9 +409,15 @@ export class McpServerTransport implements ServerTransport {
|
|
|
361
409
|
}
|
|
362
410
|
|
|
363
411
|
// Connect MCP server to transport
|
|
364
|
-
await this.mcpServer.connect(transport);
|
|
412
|
+
if (transport) await this.mcpServer.connect(transport);
|
|
413
|
+
if (this.http) {
|
|
414
|
+
this.httpUrl = await this.http.listen();
|
|
415
|
+
this.log(`Using Streamable HTTP at ${this.httpUrl}`);
|
|
416
|
+
}
|
|
365
417
|
} catch (error) {
|
|
366
418
|
this.removeRegistryListeners();
|
|
419
|
+
await this.http?.close().catch(() => undefined);
|
|
420
|
+
this.http = undefined;
|
|
367
421
|
throw error;
|
|
368
422
|
}
|
|
369
423
|
|
|
@@ -387,6 +441,11 @@ export class McpServerTransport implements ServerTransport {
|
|
|
387
441
|
}
|
|
388
442
|
|
|
389
443
|
await this.mcpServer.close();
|
|
444
|
+
await this.http?.close();
|
|
445
|
+
for (const sessionServer of this.sessionServers) await sessionServer.close().catch(() => undefined);
|
|
446
|
+
this.sessionServers.clear();
|
|
447
|
+
this.http = undefined;
|
|
448
|
+
this.httpUrl = undefined;
|
|
390
449
|
this.running = false;
|
|
391
450
|
this.log("MCP server stopped");
|
|
392
451
|
}
|
|
@@ -406,6 +465,7 @@ export class McpServerTransport implements ServerTransport {
|
|
|
406
465
|
running: this.running,
|
|
407
466
|
toolCount: this.tools.size,
|
|
408
467
|
transport: this.options.transport,
|
|
468
|
+
...(this.httpUrl ? { url: this.httpUrl } : {}),
|
|
409
469
|
};
|
|
410
470
|
}
|
|
411
471
|
|
package/src/types.ts
CHANGED
|
@@ -7,10 +7,27 @@
|
|
|
7
7
|
import type { Transport as SdkTransport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
8
8
|
import type { ProcedureRegistry } from "@procedures/client";
|
|
9
9
|
import type { McpServerInfo, McpToolFilter } from "@procedures/mcp";
|
|
10
|
+
import type { Duration } from "@procedures/client";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* SSE transport configuration options.
|
|
13
14
|
*/
|
|
15
|
+
/** The options of the Streamable HTTP transport (roadmap R26). */
|
|
16
|
+
export interface HttpTransportOptions {
|
|
17
|
+
/** Port to listen on (default: 3003) */
|
|
18
|
+
port?: number | undefined;
|
|
19
|
+
/** Host to bind to (default: "127.0.0.1": only this computer can reach the tools) */
|
|
20
|
+
host?: string | undefined;
|
|
21
|
+
/** The path of the MCP endpoint (default: "/mcp") */
|
|
22
|
+
path?: string | undefined;
|
|
23
|
+
/** A secret that each request must send as `Authorization: Bearer <token>` */
|
|
24
|
+
token?: string | undefined;
|
|
25
|
+
/** Browser origins that can call the server, besides loopback origins */
|
|
26
|
+
allowedOrigins?: string[] | undefined;
|
|
27
|
+
/** An HTTP server to answer on, in place of a server of the transport (it does not listen then) */
|
|
28
|
+
server?: import("node:http").Server | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
14
31
|
export interface SseTransportOptions {
|
|
15
32
|
/** Port to listen on (default: 3002) */
|
|
16
33
|
port?: number;
|
|
@@ -26,8 +43,14 @@ export interface SseTransportOptions {
|
|
|
26
43
|
* MCP Server Transport configuration options.
|
|
27
44
|
*/
|
|
28
45
|
export interface McpServerTransportOptions {
|
|
29
|
-
/**
|
|
30
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Transport type (default: "stdio"). "http" is Streamable HTTP, the HTTP transport of the
|
|
48
|
+
* current MCP specification (roadmap R26). "sse" is not implemented.
|
|
49
|
+
*/
|
|
50
|
+
transport?: "stdio" | "sse" | "http";
|
|
51
|
+
|
|
52
|
+
/** The options of the Streamable HTTP transport. */
|
|
53
|
+
httpOptions?: HttpTransportOptions | undefined;
|
|
31
54
|
|
|
32
55
|
/** Procedure registry to use (defaults to PROCEDURE_REGISTRY) */
|
|
33
56
|
registry?: ProcedureRegistry;
|
|
@@ -49,6 +72,13 @@ export interface McpServerTransportOptions {
|
|
|
49
72
|
|
|
50
73
|
/** Enable debug logging */
|
|
51
74
|
debug?: boolean;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The limits of a stream that can run without an end (`stream: { unbounded: true }` of its
|
|
78
|
+
* procedure): the tool stops it at the first one and gives the items that it has (roadmap R15).
|
|
79
|
+
* @default { items: 100, time: seconds(30) }
|
|
80
|
+
*/
|
|
81
|
+
streamLimits?: { items?: number; time?: Duration } | undefined;
|
|
52
82
|
}
|
|
53
83
|
|
|
54
84
|
/**
|
|
@@ -60,5 +90,7 @@ export interface McpServerTransportState {
|
|
|
60
90
|
/** Number of registered tools */
|
|
61
91
|
toolCount: number;
|
|
62
92
|
/** Transport type in use */
|
|
63
|
-
transport: "stdio" | "sse";
|
|
93
|
+
transport: "stdio" | "sse" | "http";
|
|
94
|
+
/** The URL of the Streamable HTTP endpoint, when the transport is "http" */
|
|
95
|
+
url?: string;
|
|
64
96
|
}
|