tiny-http-mcp-server 0.1.16 → 0.1.18
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/dist/cli.js +14 -2
- package/dist/composition.json +1 -1
- package/dist/http-server.d.ts +7 -5
- package/dist/http-server.js +3 -11
- package/dist/http-transport.d.ts +6 -1
- package/dist/http-transport.js +196 -15
- package/dist/index.d.ts +2 -2
- package/dist/modern-headers.d.ts +3 -0
- package/dist/modern-headers.js +35 -0
- package/dist/parse-body.d.ts +1 -0
- package/dist/parse-body.js +9 -5
- package/dist/test-support.js +17 -0
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +3 -2
- package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +21 -4
- package/node_modules/mcp-oauth/dist/client/token-endpoint.d.ts +1 -1
- package/node_modules/mcp-oauth/dist/client/token-endpoint.js +14 -6
- package/node_modules/mcp-oauth/dist/http-fetch.d.ts +1 -0
- package/node_modules/mcp-oauth/dist/http-fetch.js +8 -0
- package/node_modules/mcp-oauth/dist/http-response.d.ts +1 -0
- package/node_modules/mcp-oauth/dist/http-response.js +50 -0
- package/node_modules/mcp-oauth/dist/index.d.ts +2 -0
- package/node_modules/mcp-oauth/dist/index.js +2 -0
- package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.js +21 -3
- package/node_modules/tiny-mcp-client/README.md +1 -0
- package/node_modules/tiny-mcp-client/dist/index.d.ts +178 -101
- package/node_modules/tiny-mcp-client/dist/index.js +4696 -291
- package/node_modules/tiny-mcp-client/package.json +4 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -29,6 +29,8 @@ const HELP_TEXT = [
|
|
|
29
29
|
" --allowed-origin <url> Allowed CORS Origin value (repeatable)",
|
|
30
30
|
" --max-request-bytes <bytes>",
|
|
31
31
|
" Maximum JSON request body size",
|
|
32
|
+
" --max-response-bytes <bytes>",
|
|
33
|
+
" Maximum JSON body or SSE frame size (default: 16777216)",
|
|
32
34
|
" --max-batch-size <count>",
|
|
33
35
|
" Maximum JSON-RPC batch member count",
|
|
34
36
|
" --max-sessions <count> Maximum active sessions (default: 128)",
|
|
@@ -38,13 +40,15 @@ const HELP_TEXT = [
|
|
|
38
40
|
" --max-streams-per-session <count>",
|
|
39
41
|
" Maximum concurrent GET SSE streams per session",
|
|
40
42
|
" --max-stream-buffer-bytes <bytes>",
|
|
41
|
-
" Maximum buffered bytes per
|
|
43
|
+
" Maximum buffered bytes per SSE stream (default: 1048576)",
|
|
42
44
|
" --max-sse-event-history <count>",
|
|
43
45
|
" Number of SSE events retained for Last-Event-ID replay",
|
|
44
46
|
" --sse-keep-alive-ms <ms>",
|
|
45
|
-
"
|
|
47
|
+
" SSE keepalive interval (default: 30000; 0 disables)",
|
|
46
48
|
" --max-concurrent-tool-calls <count>",
|
|
47
49
|
" Maximum concurrent tool calls across sessions (default: 4)",
|
|
50
|
+
" --max-active-requests <count>",
|
|
51
|
+
" Maximum active requests across connections (default: 128)",
|
|
48
52
|
" --max-queued-tool-calls <count>",
|
|
49
53
|
" Maximum waiting tool calls (default: 64; 0 disables waiting)",
|
|
50
54
|
" --trusted-proxy Trust X-Forwarded-Proto and X-Forwarded-Host",
|
|
@@ -194,6 +198,7 @@ function parseCliOptions(args) {
|
|
|
194
198
|
"allowed-host": { type: "string", multiple: true },
|
|
195
199
|
"allowed-origin": { type: "string", multiple: true },
|
|
196
200
|
"max-request-bytes": { type: "string" },
|
|
201
|
+
"max-response-bytes": { type: "string" },
|
|
197
202
|
"max-batch-size": { type: "string" },
|
|
198
203
|
"max-sessions": { type: "string" },
|
|
199
204
|
"max-sessions-per-subject": { type: "string" },
|
|
@@ -203,6 +208,7 @@ function parseCliOptions(args) {
|
|
|
203
208
|
"max-sse-event-history": { type: "string" },
|
|
204
209
|
"sse-keep-alive-ms": { type: "string" },
|
|
205
210
|
"max-concurrent-tool-calls": { type: "string" },
|
|
211
|
+
"max-active-requests": { type: "string" },
|
|
206
212
|
"max-queued-tool-calls": { type: "string" },
|
|
207
213
|
"trusted-proxy": { type: "boolean" },
|
|
208
214
|
"request-timeout-ms": { type: "string" },
|
|
@@ -222,6 +228,7 @@ function parseCliOptions(args) {
|
|
|
222
228
|
});
|
|
223
229
|
const maxRequestBytes = parseOptionalInteger(values["max-request-bytes"], "--max-request-bytes", 1);
|
|
224
230
|
const maxBatchSize = parseOptionalInteger(values["max-batch-size"], "--max-batch-size", 1);
|
|
231
|
+
const maxResponseBytes = parseOptionalInteger(values["max-response-bytes"], "--max-response-bytes", 1);
|
|
225
232
|
const maxSessions = parseOptionalInteger(values["max-sessions"], "--max-sessions", 1);
|
|
226
233
|
const maxSessionsPerSubject = parseOptionalInteger(values["max-sessions-per-subject"], "--max-sessions-per-subject", 1);
|
|
227
234
|
const sessionTtlMs = parseOptionalInteger(values["session-ttl-ms"], "--session-ttl-ms", 1);
|
|
@@ -231,6 +238,7 @@ function parseCliOptions(args) {
|
|
|
231
238
|
const sseKeepAliveMs = parseOptionalInteger(values["sse-keep-alive-ms"], "--sse-keep-alive-ms", 0);
|
|
232
239
|
const maxConcurrentToolCalls = parseOptionalInteger(values["max-concurrent-tool-calls"], "--max-concurrent-tool-calls", 1);
|
|
233
240
|
const maxQueuedToolCalls = parseOptionalInteger(values["max-queued-tool-calls"], "--max-queued-tool-calls", 0);
|
|
241
|
+
const maxActiveRequests = parseOptionalInteger(values["max-active-requests"], "--max-active-requests", 1);
|
|
234
242
|
const requestTimeoutMs = parseOptionalInteger(values["request-timeout-ms"], "--request-timeout-ms", 0);
|
|
235
243
|
const headersTimeoutMs = parseOptionalInteger(values["headers-timeout-ms"], "--headers-timeout-ms", 0);
|
|
236
244
|
const keepAliveTimeoutMs = parseOptionalInteger(values["keep-alive-timeout-ms"], "--keep-alive-timeout-ms", 0);
|
|
@@ -252,6 +260,7 @@ function parseCliOptions(args) {
|
|
|
252
260
|
}
|
|
253
261
|
: {}),
|
|
254
262
|
...(maxRequestBytes === undefined ? {} : { maxRequestBytes }),
|
|
263
|
+
...(maxResponseBytes === undefined ? {} : { maxResponseBytes }),
|
|
255
264
|
...(maxBatchSize === undefined ? {} : { maxBatchSize }),
|
|
256
265
|
...(maxSessions === undefined ? {} : { maxSessions }),
|
|
257
266
|
...(maxSessionsPerSubject === undefined ? {} : { maxSessionsPerSubject }),
|
|
@@ -261,6 +270,7 @@ function parseCliOptions(args) {
|
|
|
261
270
|
...(maxSseEventHistory === undefined ? {} : { maxSseEventHistory }),
|
|
262
271
|
...(sseKeepAliveMs === undefined ? {} : { sseKeepAliveMs }),
|
|
263
272
|
...(maxConcurrentToolCalls === undefined ? {} : { maxConcurrentToolCalls }),
|
|
273
|
+
...(maxActiveRequests === undefined ? {} : { maxActiveRequests }),
|
|
264
274
|
...(maxQueuedToolCalls === undefined ? {} : { maxQueuedToolCalls }),
|
|
265
275
|
trustedProxy: values["trusted-proxy"] ?? false,
|
|
266
276
|
...(requestTimeoutMs === undefined ? {} : { requestTimeoutMs }),
|
|
@@ -400,6 +410,7 @@ export async function runCli(args = process.argv.slice(2), dependencies = {}) {
|
|
|
400
410
|
...(options.maxRequestBytes === undefined
|
|
401
411
|
? {}
|
|
402
412
|
: { maxRequestBytes: options.maxRequestBytes }),
|
|
413
|
+
...(options.maxResponseBytes === undefined ? {} : { maxResponseBytes: options.maxResponseBytes }),
|
|
403
414
|
...(options.maxBatchSize === undefined ? {} : { maxBatchSize: options.maxBatchSize }),
|
|
404
415
|
...(options.maxSessions === undefined ? {} : { maxSessions: options.maxSessions }),
|
|
405
416
|
...(options.maxSessionsPerSubject === undefined ? {} : { maxSessionsPerSubject: options.maxSessionsPerSubject }),
|
|
@@ -418,6 +429,7 @@ export async function runCli(args = process.argv.slice(2), dependencies = {}) {
|
|
|
418
429
|
? {}
|
|
419
430
|
: { maxConcurrentToolCalls: options.maxConcurrentToolCalls }),
|
|
420
431
|
...(options.maxQueuedToolCalls === undefined ? {} : { maxQueuedToolCalls: options.maxQueuedToolCalls }),
|
|
432
|
+
...(options.maxActiveRequests === undefined ? {} : { maxActiveRequests: options.maxActiveRequests }),
|
|
421
433
|
...(options.trustedProxy ? { trustedProxy: true } : {}),
|
|
422
434
|
...(oauth === undefined ? {} : { oauth })
|
|
423
435
|
});
|
package/dist/composition.json
CHANGED
package/dist/http-server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type IncomingMessage, type ServerResponse } from "node:http";
|
|
2
|
-
import { type Server, type ServerOptions, type ToolDefinition, type CallToolResult, type ToolReturn, type TypedSchema } from "tiny-stdio-mcp-server";
|
|
2
|
+
import { type Server, type ServerOptions, type ToolDefinition, type CallToolResult, type InputRequiredResult, type HandlerRequestContext, type ToolReturn, type TypedSchema, type TypedOutputSchema } from "tiny-stdio-mcp-server";
|
|
3
3
|
import { type AuthenticatedIncomingMessage, type TokenVerifier, type VerifiedAccessToken, type RequestAuthInfo } from "./auth.js";
|
|
4
4
|
import { type HttpObservabilityEvent, type StreamableHttpTransportOptions } from "./http-transport.js";
|
|
5
5
|
export interface ProtectedResourceMetadataOptions {
|
|
@@ -33,18 +33,20 @@ export interface HttpServerHandle {
|
|
|
33
33
|
closeAllConnections(): void;
|
|
34
34
|
}
|
|
35
35
|
export interface HttpServer extends Omit<Server, "tool" | "registerTool"> {
|
|
36
|
-
tool<TIn, TOut = never>(name: string, description: string, inputSchema: TypedSchema<TIn>, handler: HttpToolHandler<TIn, TOut>, outputSchema?:
|
|
36
|
+
tool<TIn, TOut = never>(name: string, description: string, inputSchema: TypedSchema<TIn>, handler: HttpToolHandler<TIn, TOut>, outputSchema?: TypedOutputSchema<TOut>): HttpServer;
|
|
37
37
|
registerTool<TIn, TOut = never>(definition: Omit<ToolDefinition<TIn, TOut>, "handler">, handler: HttpToolHandler<TIn, TOut>): HttpServer;
|
|
38
38
|
listenHttp(options?: HttpListenOptions): Promise<HttpServerHandle>;
|
|
39
39
|
handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void>;
|
|
40
|
-
getRequestContext():
|
|
40
|
+
getRequestContext(): HttpRequestContext | undefined;
|
|
41
41
|
}
|
|
42
|
-
export interface
|
|
42
|
+
export interface HttpRequestContext {
|
|
43
43
|
request: AuthenticatedIncomingMessage;
|
|
44
44
|
sessionId?: string;
|
|
45
45
|
auth?: RequestAuthInfo;
|
|
46
46
|
}
|
|
47
|
-
export
|
|
47
|
+
export interface HttpToolContext extends HttpRequestContext, HandlerRequestContext {
|
|
48
|
+
}
|
|
49
|
+
export type HttpToolHandler<T = Record<string, unknown>, TOut = ToolReturn> = (args: T, context: HttpToolContext) => Promise<TOut | CallToolResult | InputRequiredResult> | TOut | CallToolResult | InputRequiredResult;
|
|
48
50
|
export declare function createProtectedResourceMetadataDocument(options: ProtectedResourceMetadataOptions): Record<string, unknown>;
|
|
49
51
|
export declare function createHttpServer(options: HttpTransportOptions): HttpServer;
|
|
50
52
|
export type { RequestAuthInfo, TokenVerifier, VerifiedAccessToken };
|
package/dist/http-server.js
CHANGED
|
@@ -87,12 +87,7 @@ export function createProtectedResourceMetadataDocument(options) {
|
|
|
87
87
|
}
|
|
88
88
|
export function createHttpServer(options) {
|
|
89
89
|
const requestContextStorage = new AsyncLocalStorage();
|
|
90
|
-
const
|
|
91
|
-
const server = createServer({
|
|
92
|
-
...options,
|
|
93
|
-
supportNotifications: supportsSessions,
|
|
94
|
-
supportResourceSubscriptions: supportsSessions
|
|
95
|
-
});
|
|
90
|
+
const server = createServer(options);
|
|
96
91
|
const transport = new StreamableHttpTransport(server, options, async (req, callback) => requestContextStorage.run({
|
|
97
92
|
request: req,
|
|
98
93
|
sessionId: Array.isArray(req.headers["mcp-session-id"])
|
|
@@ -144,11 +139,11 @@ export function createHttpServer(options) {
|
|
|
144
139
|
return true;
|
|
145
140
|
};
|
|
146
141
|
httpServer.tool = (name, description, inputSchema, handler, outputSchema) => {
|
|
147
|
-
registerTool(name, description, inputSchema, (args) => handler(args, requestContextStorage.getStore() ?? defaultContext), outputSchema);
|
|
142
|
+
registerTool(name, description, inputSchema, (args, context) => handler(args, { ...(requestContextStorage.getStore() ?? defaultContext), ...context }), outputSchema);
|
|
148
143
|
return httpServer;
|
|
149
144
|
};
|
|
150
145
|
httpServer.registerTool = (definition, handler) => {
|
|
151
|
-
registerRichTool(definition, (args) => handler(args, requestContextStorage.getStore() ?? defaultContext));
|
|
146
|
+
registerRichTool(definition, (args, context) => handler(args, { ...(requestContextStorage.getStore() ?? defaultContext), ...context }));
|
|
152
147
|
return httpServer;
|
|
153
148
|
};
|
|
154
149
|
httpServer.listenHttp = async (listenOptions = {}) => {
|
|
@@ -265,6 +260,3 @@ export function createHttpServer(options) {
|
|
|
265
260
|
httpServer.getRequestContext = () => requestContextStorage.getStore();
|
|
266
261
|
return httpServer;
|
|
267
262
|
}
|
|
268
|
-
function hasOwnProperty(value, key) {
|
|
269
|
-
return Object.prototype.hasOwnProperty.call(value, key);
|
|
270
|
-
}
|
package/dist/http-transport.d.ts
CHANGED
|
@@ -64,6 +64,7 @@ export interface StreamableHttpTransportOptions {
|
|
|
64
64
|
allowedOrigins?: readonly string[];
|
|
65
65
|
allowedHosts?: readonly string[];
|
|
66
66
|
maxRequestBytes?: number;
|
|
67
|
+
maxResponseBytes?: number;
|
|
67
68
|
maxBatchSize?: number;
|
|
68
69
|
maxSessions?: number;
|
|
69
70
|
/** Maximum sessions per authenticated subject or client ID; defaults to 16. */
|
|
@@ -88,6 +89,7 @@ export declare class StreamableHttpTransport {
|
|
|
88
89
|
private readonly allowedOrigins;
|
|
89
90
|
private readonly allowedHosts;
|
|
90
91
|
private readonly maxRequestBytes;
|
|
92
|
+
private readonly maxResponseBytes;
|
|
91
93
|
private readonly maxBatchSize;
|
|
92
94
|
private readonly maxSessions;
|
|
93
95
|
private readonly maxSessionsPerSubject;
|
|
@@ -108,6 +110,8 @@ export declare class StreamableHttpTransport {
|
|
|
108
110
|
private readonly responseRequestIds;
|
|
109
111
|
private readonly responseOrigins;
|
|
110
112
|
private readonly responseRejectionReasons;
|
|
113
|
+
private readonly modernRequests;
|
|
114
|
+
private readonly modernStreams;
|
|
111
115
|
private nextNotificationEventId;
|
|
112
116
|
private nextRequestId;
|
|
113
117
|
private activeToolCalls;
|
|
@@ -118,6 +122,7 @@ export declare class StreamableHttpTransport {
|
|
|
118
122
|
handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void>;
|
|
119
123
|
close(): Promise<void>;
|
|
120
124
|
private handlePost;
|
|
125
|
+
private handleModernPost;
|
|
121
126
|
private handleGet;
|
|
122
127
|
private handleDelete;
|
|
123
128
|
private handleOptions;
|
|
@@ -143,7 +148,7 @@ export declare class StreamableHttpTransport {
|
|
|
143
148
|
private stopSseKeepAliveIfIdle;
|
|
144
149
|
private stopSseKeepAlive;
|
|
145
150
|
private sendNotificationToSession;
|
|
146
|
-
private
|
|
151
|
+
private writeToLiveStream;
|
|
147
152
|
private recordSseEvent;
|
|
148
153
|
private replaySseEvents;
|
|
149
154
|
private isJsonRequest;
|
package/dist/http-transport.js
CHANGED
|
@@ -4,6 +4,7 @@ import { formatErrorResponse, formatSuccessResponse } from "tiny-stdio-mcp-serve
|
|
|
4
4
|
import { JsonRpcMessageError, readAndClassifyBody } from "./parse-body.js";
|
|
5
5
|
import { createSessionStore, defaultSessionIdGenerator } from "./session.js";
|
|
6
6
|
import { formatSseEvent, SSE_HEADERS } from "./sse.js";
|
|
7
|
+
import { validateModernHeaders } from "./modern-headers.js";
|
|
7
8
|
const ALLOWED_METHODS = "POST, GET, DELETE, OPTIONS";
|
|
8
9
|
const MCP_SESSION_ID_HEADER = "Mcp-Session-Id";
|
|
9
10
|
const LOCAL_HOSTS = ["localhost", "127.0.0.1", "::1"];
|
|
@@ -25,6 +26,7 @@ export class StreamableHttpTransport {
|
|
|
25
26
|
allowedOrigins;
|
|
26
27
|
allowedHosts;
|
|
27
28
|
maxRequestBytes;
|
|
29
|
+
maxResponseBytes;
|
|
28
30
|
maxBatchSize;
|
|
29
31
|
maxSessions;
|
|
30
32
|
maxSessionsPerSubject;
|
|
@@ -45,6 +47,8 @@ export class StreamableHttpTransport {
|
|
|
45
47
|
responseRequestIds = new WeakMap();
|
|
46
48
|
responseOrigins = new WeakMap();
|
|
47
49
|
responseRejectionReasons = new WeakMap();
|
|
50
|
+
modernRequests = new Map();
|
|
51
|
+
modernStreams = new Set();
|
|
48
52
|
nextNotificationEventId = 1;
|
|
49
53
|
nextRequestId = 1;
|
|
50
54
|
activeToolCalls = 0;
|
|
@@ -62,10 +66,13 @@ export class StreamableHttpTransport {
|
|
|
62
66
|
this.allowedHosts = new Set((options.allowedHosts ?? LOCAL_HOSTS).map((host) => this.normalizeHost(host)));
|
|
63
67
|
this.maxRequestBytes = validateOptionalIntegerOption("maxRequestBytes", options.maxRequestBytes, 1);
|
|
64
68
|
this.maxBatchSize = validateOptionalIntegerOption("maxBatchSize", options.maxBatchSize, 1);
|
|
69
|
+
this.maxResponseBytes = validateOptionalIntegerOption("maxResponseBytes", options.maxResponseBytes, 1) ?? 16 * 1024 * 1024;
|
|
65
70
|
this.maxSessions = validateOptionalIntegerOption("maxSessions", options.maxSessions, 1) ?? 128;
|
|
66
71
|
this.maxSessionsPerSubject =
|
|
67
|
-
validateOptionalIntegerOption("maxSessionsPerSubject", options.maxSessionsPerSubject, 1) ??
|
|
68
|
-
|
|
72
|
+
validateOptionalIntegerOption("maxSessionsPerSubject", options.maxSessionsPerSubject, 1) ??
|
|
73
|
+
16;
|
|
74
|
+
this.sessionTtlMs =
|
|
75
|
+
validateOptionalIntegerOption("sessionTtlMs", options.sessionTtlMs, 1) ?? 15 * 60_000;
|
|
69
76
|
this.maxStreamsPerSession =
|
|
70
77
|
validateOptionalIntegerOption("maxStreamsPerSession", options.maxStreamsPerSession, 1) ?? 1;
|
|
71
78
|
this.maxStreamBufferBytes =
|
|
@@ -120,6 +127,12 @@ export class StreamableHttpTransport {
|
|
|
120
127
|
this.respondWithRejection(res, 403, "origin_not_allowed", `Origin ${JSON.stringify(this.readOrigin(req) ?? "")} is not allowed; add it to allowedOrigins.`);
|
|
121
128
|
return;
|
|
122
129
|
}
|
|
130
|
+
if (req.headers["mcp-protocol-version"] === "2026-07-28" &&
|
|
131
|
+
req.method !== "POST" &&
|
|
132
|
+
req.method !== "OPTIONS") {
|
|
133
|
+
this.respondWithStatus(res, 405, undefined, { Allow: "POST, OPTIONS" });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
123
136
|
switch (req.method) {
|
|
124
137
|
case "POST":
|
|
125
138
|
await this.handlePost(req, res);
|
|
@@ -172,6 +185,12 @@ export class StreamableHttpTransport {
|
|
|
172
185
|
}
|
|
173
186
|
async close() {
|
|
174
187
|
this.closed = true;
|
|
188
|
+
for (const [response, controller] of this.modernRequests) {
|
|
189
|
+
controller.abort();
|
|
190
|
+
response.end();
|
|
191
|
+
}
|
|
192
|
+
this.modernRequests.clear();
|
|
193
|
+
this.modernStreams.clear();
|
|
175
194
|
if (this.sessionExpiryInterval !== undefined) {
|
|
176
195
|
clearInterval(this.sessionExpiryInterval);
|
|
177
196
|
this.sessionExpiryInterval = undefined;
|
|
@@ -221,6 +240,20 @@ export class StreamableHttpTransport {
|
|
|
221
240
|
this.respondWithJsonRpcError(res, 400, code, message, id);
|
|
222
241
|
return;
|
|
223
242
|
}
|
|
243
|
+
const modern = req.headers["mcp-protocol-version"] === "2026-07-28" ||
|
|
244
|
+
classified.messages.some((message) => {
|
|
245
|
+
if (!("method" in message))
|
|
246
|
+
return false;
|
|
247
|
+
const metadata = message.params?._meta;
|
|
248
|
+
return (message.method === "server/discover" ||
|
|
249
|
+
(typeof metadata === "object" &&
|
|
250
|
+
metadata !== null &&
|
|
251
|
+
Object.prototype.hasOwnProperty.call(metadata, "io.modelcontextprotocol/protocolVersion")));
|
|
252
|
+
});
|
|
253
|
+
if (modern) {
|
|
254
|
+
await this.handleModernPost(req, res, classified);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
224
257
|
const initMessage = classified.messages.find((message) => this.isRequest(message) && message.method === "initialize");
|
|
225
258
|
let sessionId;
|
|
226
259
|
let activeSession;
|
|
@@ -237,7 +270,8 @@ export class StreamableHttpTransport {
|
|
|
237
270
|
return;
|
|
238
271
|
}
|
|
239
272
|
const authSubject = this.readAuthSubject(req);
|
|
240
|
-
if (authSubject !== undefined &&
|
|
273
|
+
if (authSubject !== undefined &&
|
|
274
|
+
this.sessionCount(authSubject) >= this.maxSessionsPerSubject) {
|
|
241
275
|
this.respondWithRejection(res, 429, "subject_session_limit_reached", "The authenticated subject has reached its session limit; close a session or retry later.");
|
|
242
276
|
return;
|
|
243
277
|
}
|
|
@@ -283,6 +317,15 @@ export class StreamableHttpTransport {
|
|
|
283
317
|
if (!("method" in message)) {
|
|
284
318
|
continue;
|
|
285
319
|
}
|
|
320
|
+
if (this.sessionIdGenerator === undefined &&
|
|
321
|
+
(message.method === "resources/subscribe" || message.method === "resources/unsubscribe")) {
|
|
322
|
+
if (this.isRequest(message))
|
|
323
|
+
responses.push(formatErrorResponse(message.id, {
|
|
324
|
+
code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND,
|
|
325
|
+
message: "Method not found"
|
|
326
|
+
}));
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
286
329
|
const session = activeSession;
|
|
287
330
|
if (session !== undefined &&
|
|
288
331
|
message.method !== "initialize" &&
|
|
@@ -333,7 +376,23 @@ export class StreamableHttpTransport {
|
|
|
333
376
|
this.activeToolCalls -= 1;
|
|
334
377
|
}
|
|
335
378
|
}
|
|
336
|
-
const { error
|
|
379
|
+
const { error } = handled;
|
|
380
|
+
let { result } = handled;
|
|
381
|
+
if (error === undefined &&
|
|
382
|
+
message.method === "initialize" &&
|
|
383
|
+
result !== undefined &&
|
|
384
|
+
this.sessionIdGenerator === undefined) {
|
|
385
|
+
const initialization = result;
|
|
386
|
+
const capabilities = structuredClone(initialization.capabilities);
|
|
387
|
+
for (const category of ["tools", "prompts", "resources"]) {
|
|
388
|
+
const capability = capabilities[category];
|
|
389
|
+
if (capability !== undefined)
|
|
390
|
+
delete capability.listChanged;
|
|
391
|
+
}
|
|
392
|
+
if (capabilities.resources !== undefined)
|
|
393
|
+
delete capabilities.resources.subscribe;
|
|
394
|
+
result = { ...initialization, capabilities };
|
|
395
|
+
}
|
|
337
396
|
if (isToolCall) {
|
|
338
397
|
this.emit({
|
|
339
398
|
type: "tool.end",
|
|
@@ -380,12 +439,115 @@ export class StreamableHttpTransport {
|
|
|
380
439
|
this.respondWithStatus(res, 200, sessionId, { "Content-Type": "application/json" }, body);
|
|
381
440
|
return;
|
|
382
441
|
}
|
|
442
|
+
const frames = formattedResponses.map((data) => formatSseEvent({ data }));
|
|
443
|
+
if (frames.some((frame) => Buffer.byteLength(frame, "utf8") > this.maxResponseBytes)) {
|
|
444
|
+
res.destroy();
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
383
447
|
res.writeHead(200, this.withSessionHeader(SSE_HEADERS, sessionId, res));
|
|
384
|
-
for (const
|
|
385
|
-
|
|
448
|
+
for (const frame of frames) {
|
|
449
|
+
this.writeToLiveStream(res, frame);
|
|
386
450
|
}
|
|
387
451
|
res.end();
|
|
388
452
|
}
|
|
453
|
+
async handleModernPost(req, res, body) {
|
|
454
|
+
const message = body.messages[0];
|
|
455
|
+
if (body.isBatch ||
|
|
456
|
+
body.messages.length !== 1 ||
|
|
457
|
+
message === undefined ||
|
|
458
|
+
!("method" in message)) {
|
|
459
|
+
this.respondWithJsonRpcError(res, 400, JSON_RPC_ERROR_CODES.INVALID_REQUEST, "Modern HTTP requires a single request or notification");
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const id = this.isRequest(message) ? message.id : null;
|
|
463
|
+
if (!this.acceptsResponseType(req, "application/json") ||
|
|
464
|
+
!this.acceptsResponseType(req, "text/event-stream")) {
|
|
465
|
+
this.respondWithRejection(res, 406, "response_type_not_acceptable", "Accept must allow application/json and text/event-stream.");
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
const headerError = validateModernHeaders(req.headers, message);
|
|
469
|
+
if (headerError !== undefined) {
|
|
470
|
+
this.respondWithJsonRpcError(res, 400, headerError.code, headerError.message, id);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const controller = new AbortController();
|
|
474
|
+
const cancel = () => controller.abort();
|
|
475
|
+
res.once("close", cancel);
|
|
476
|
+
req.once("aborted", cancel);
|
|
477
|
+
this.modernRequests.set(res, controller);
|
|
478
|
+
let streaming = false;
|
|
479
|
+
const write = (data) => {
|
|
480
|
+
if (controller.signal.aborted || res.destroyed || res.writableEnded)
|
|
481
|
+
return;
|
|
482
|
+
const frame = formatSseEvent({ data });
|
|
483
|
+
if (Buffer.byteLength(frame, "utf8") > this.maxResponseBytes) {
|
|
484
|
+
controller.abort(new Error("Modern SSE response size limit exceeded"));
|
|
485
|
+
res.destroy();
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if ((res.writableLength ?? 0) > this.maxStreamBufferBytes) {
|
|
489
|
+
controller.abort(new Error("Modern SSE output buffer limit exceeded"));
|
|
490
|
+
res.destroy();
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (!streaming) {
|
|
494
|
+
res.writeHead(200, this.withSessionHeader({ ...SSE_HEADERS, "X-Accel-Buffering": "no" }, undefined, res));
|
|
495
|
+
streaming = true;
|
|
496
|
+
res.flushHeaders();
|
|
497
|
+
this.modernStreams.add(res);
|
|
498
|
+
this.startSseKeepAlive();
|
|
499
|
+
}
|
|
500
|
+
res.write(frame);
|
|
501
|
+
};
|
|
502
|
+
const session = this.server.createMessageSession((notification) => write(JSON.stringify(notification)));
|
|
503
|
+
try {
|
|
504
|
+
if (!this.isRequest(message) && !message.method.startsWith("notifications/")) {
|
|
505
|
+
this.respondWithJsonRpcError(res, 400, JSON_RPC_ERROR_CODES.INVALID_REQUEST, "A request ID is required");
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const handled = await this.runWithRequestContext(req, () => session.handleMessage(message.method, message.params, {
|
|
509
|
+
...(this.isRequest(message) ? { requestId: message.id } : {}),
|
|
510
|
+
signal: controller.signal,
|
|
511
|
+
parameterHeaders: req.headers
|
|
512
|
+
}));
|
|
513
|
+
if (controller.signal.aborted || res.destroyed || res.writableEnded)
|
|
514
|
+
return;
|
|
515
|
+
if (!this.isRequest(message) ||
|
|
516
|
+
(handled.result === undefined && handled.error === undefined)) {
|
|
517
|
+
if (streaming)
|
|
518
|
+
res.end();
|
|
519
|
+
else
|
|
520
|
+
this.respondWithStatus(res, 202);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const formatted = handled.error === undefined
|
|
524
|
+
? formatSuccessResponse(message.id, handled.result)
|
|
525
|
+
: formatErrorResponse(message.id, handled.error);
|
|
526
|
+
const status = handled.error?.code === JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND
|
|
527
|
+
? 404
|
|
528
|
+
: handled.error?.code === JSON_RPC_ERROR_CODES.INVALID_PARAMS ||
|
|
529
|
+
handled.error?.code === JSON_RPC_ERROR_CODES.UNSUPPORTED_PROTOCOL_VERSION ||
|
|
530
|
+
handled.error?.code === -32020 ||
|
|
531
|
+
handled.error?.code === -32021
|
|
532
|
+
? 400
|
|
533
|
+
: 200;
|
|
534
|
+
if (streaming || (!this.enableJsonResponse && status === 200)) {
|
|
535
|
+
write(formatted);
|
|
536
|
+
if (!res.destroyed)
|
|
537
|
+
res.end();
|
|
538
|
+
}
|
|
539
|
+
else
|
|
540
|
+
this.respondWithStatus(res, status, undefined, { "Content-Type": "application/json" }, formatted);
|
|
541
|
+
}
|
|
542
|
+
finally {
|
|
543
|
+
session.close();
|
|
544
|
+
res.off("close", cancel);
|
|
545
|
+
req.off("aborted", cancel);
|
|
546
|
+
this.modernRequests.delete(res);
|
|
547
|
+
this.modernStreams.delete(res);
|
|
548
|
+
this.stopSseKeepAliveIfIdle();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
389
551
|
async handleGet(req, res) {
|
|
390
552
|
if (this.sessionIdGenerator === undefined) {
|
|
391
553
|
this.respondWithStatus(res, 405, undefined, {
|
|
@@ -675,10 +837,14 @@ export class StreamableHttpTransport {
|
|
|
675
837
|
return;
|
|
676
838
|
}
|
|
677
839
|
this.sseKeepAliveInterval = setInterval(() => {
|
|
840
|
+
for (const response of this.modernStreams) {
|
|
841
|
+
if (!response.writableEnded)
|
|
842
|
+
this.writeToLiveStream(response, ": keepalive\n\n");
|
|
843
|
+
}
|
|
678
844
|
for (const streams of this.sseStreams.values()) {
|
|
679
845
|
for (const response of streams) {
|
|
680
846
|
if (!response.writableEnded) {
|
|
681
|
-
this.
|
|
847
|
+
this.writeToLiveStream(response, ": keepalive\n\n");
|
|
682
848
|
}
|
|
683
849
|
}
|
|
684
850
|
}
|
|
@@ -686,7 +852,8 @@ export class StreamableHttpTransport {
|
|
|
686
852
|
this.sseKeepAliveInterval.unref();
|
|
687
853
|
}
|
|
688
854
|
stopSseKeepAliveIfIdle() {
|
|
689
|
-
if (
|
|
855
|
+
if (this.modernStreams.size > 0 ||
|
|
856
|
+
[...this.sseStreams.values()].some((streams) => streams.size > 0)) {
|
|
690
857
|
return;
|
|
691
858
|
}
|
|
692
859
|
this.stopSseKeepAlive();
|
|
@@ -704,15 +871,19 @@ export class StreamableHttpTransport {
|
|
|
704
871
|
}
|
|
705
872
|
const id = this.nextNotificationEventId++;
|
|
706
873
|
const data = JSON.stringify(notification);
|
|
707
|
-
this.recordSseEvent(sessionId, id, data);
|
|
708
874
|
const streams = this.sseStreams.get(sessionId);
|
|
709
|
-
if (streams === undefined) {
|
|
710
|
-
return;
|
|
711
|
-
}
|
|
712
875
|
const event = formatSseEvent({
|
|
713
876
|
id: String(id),
|
|
714
877
|
data
|
|
715
878
|
});
|
|
879
|
+
if (Buffer.byteLength(event, "utf8") > this.maxResponseBytes) {
|
|
880
|
+
for (const response of streams ?? [])
|
|
881
|
+
response.destroy();
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
this.recordSseEvent(sessionId, id, data);
|
|
885
|
+
if (streams === undefined)
|
|
886
|
+
return;
|
|
716
887
|
let latestResponse;
|
|
717
888
|
for (const response of streams) {
|
|
718
889
|
if (!response.writableEnded) {
|
|
@@ -720,10 +891,16 @@ export class StreamableHttpTransport {
|
|
|
720
891
|
}
|
|
721
892
|
}
|
|
722
893
|
if (latestResponse !== undefined) {
|
|
723
|
-
this.
|
|
894
|
+
this.writeToLiveStream(latestResponse, event);
|
|
724
895
|
}
|
|
725
896
|
}
|
|
726
|
-
|
|
897
|
+
writeToLiveStream(response, data) {
|
|
898
|
+
if (response.destroyed || response.writableEnded)
|
|
899
|
+
return;
|
|
900
|
+
if (Buffer.byteLength(data, "utf8") > this.maxResponseBytes) {
|
|
901
|
+
response.destroy();
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
727
904
|
if ((response.writableLength ?? 0) > this.maxStreamBufferBytes) {
|
|
728
905
|
response.end();
|
|
729
906
|
return;
|
|
@@ -754,7 +931,7 @@ export class StreamableHttpTransport {
|
|
|
754
931
|
const history = this.sseEventHistory.get(sessionId) ?? [];
|
|
755
932
|
for (const event of history) {
|
|
756
933
|
if (event.id > lastEventId && !res.writableEnded) {
|
|
757
|
-
this.
|
|
934
|
+
this.writeToLiveStream(res, formatSseEvent({ id: String(event.id), data: event.data }));
|
|
758
935
|
}
|
|
759
936
|
}
|
|
760
937
|
}
|
|
@@ -877,6 +1054,10 @@ export class StreamableHttpTransport {
|
|
|
877
1054
|
this.respondWithStatus(res, statusCode, undefined, { "Content-Type": "application/json" }, JSON.stringify({ error: reason, message }));
|
|
878
1055
|
}
|
|
879
1056
|
respondWithStatus(res, statusCode, sessionId, headers, body) {
|
|
1057
|
+
if (body !== undefined && Buffer.byteLength(body, "utf8") > this.maxResponseBytes) {
|
|
1058
|
+
res.destroy();
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
880
1061
|
res.writeHead(statusCode, this.withSessionHeader(headers, sessionId, res));
|
|
881
1062
|
res.end(body);
|
|
882
1063
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export { createServer, defineSchema, Image, Audio, File, toContentBlocks, fileTypeFromBuffer, JSON_RPC_ERROR_CODES, ToolError } from "tiny-stdio-mcp-server";
|
|
2
|
-
export type { Server, TypedSchema, ImageContent, AudioContent, EmbeddedResource, TextResourceContents, BlobResourceContents, ContentBlock, TextContent, FileTypeResult, ToolReturn, ServerOptions, ToolHandler, ToolDefinition, Tool, ToolAnnotations, ToolExecution, Icon, ContentAnnotations, ResourceLink, CallToolResult, PromptContentItem, PromptArgument, Prompt, PromptMessage, GetPromptResult, PromptHandler, PromptDefinition, Resource, ResourceTemplate, ResourceContents, ReadResourceResult, ResourceHandler, ResourceDefinition, ResourceTemplateDefinition, HandleResult, ContentItem, JSONSchema, JSONSchemaProperty, Transport, SDKTransport, JSONRPCRequest, JSONRPCResponse, JSONRPCError, JSONRPCMessage, JSONRPCNotification, InitializeResult } from "tiny-stdio-mcp-server";
|
|
2
|
+
export type { Server, MessageRequestContext, MessageSession, MessageSessionContext, TypedSchema, TypedOutputSchema, ImageContent, AudioContent, EmbeddedResource, TextResourceContents, BlobResourceContents, ContentBlock, TextContent, FileTypeResult, ToolReturn, ServerOptions, ToolHandler, HandlerRequestContext, InputRequiredResult, ToolDefinition, Tool, ToolAnnotations, ToolExecution, Icon, ContentAnnotations, ResourceLink, CallToolResult, PromptContentItem, PromptArgument, Prompt, PromptMessage, GetPromptResult, PromptHandler, PromptDefinition, Resource, ResourceTemplate, ResourceContents, ReadResourceResult, ResourceHandler, ResourceDefinition, ResourceTemplateDefinition, HandleResult, ContentItem, JSONSchema, OutputSchema, JSONSchemaProperty, Transport, SDKTransport, JSONRPCRequest, JSONRPCResponse, JSONRPCError, JSONRPCMessage, JSONRPCNotification, InitializeResult, DiscoverResult } from "tiny-stdio-mcp-server";
|
|
3
3
|
export { createExpressMiddleware, createExpressOAuthHandlers, createProtectedResourceMetadataRouter } from "./express-middleware.js";
|
|
4
4
|
export type { CreateExpressOAuthHandlersOptions } from "./express-middleware.js";
|
|
5
5
|
export { createHttpServer, createProtectedResourceMetadataDocument } from "./http-server.js";
|
|
6
|
-
export type { HttpToolContext, HttpToolHandler, HttpAdditionalRequestHandler, HttpListenOptions, HttpServer, HttpServerHandle, HttpTransportOptions, ProtectedResourceMetadataOptions, TinyHttpMcpServerOAuthOptions } from "./http-server.js";
|
|
6
|
+
export type { HttpToolContext, HttpRequestContext, HttpToolHandler, HttpAdditionalRequestHandler, HttpListenOptions, HttpServer, HttpServerHandle, HttpTransportOptions, ProtectedResourceMetadataOptions, TinyHttpMcpServerOAuthOptions } from "./http-server.js";
|
|
7
7
|
export { TokenVerificationError } from "./auth.js";
|
|
8
8
|
export type { RequestAuthInfo, TokenVerifier, VerifiedAccessToken } from "./auth.js";
|
|
9
9
|
export { StreamableHttpTransport } from "./http-transport.js";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { IncomingHttpHeaders } from "node:http";
|
|
2
|
+
import { type JSONRPCError, type JSONRPCNotification, type JSONRPCRequest } from "tiny-stdio-mcp-server";
|
|
3
|
+
export declare function validateModernHeaders(headers: IncomingHttpHeaders, request: JSONRPCRequest | JSONRPCNotification): JSONRPCError | undefined;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { JSON_RPC_ERROR_CODES } from "tiny-stdio-mcp-server";
|
|
2
|
+
import { decodeHeaderValue } from "tiny-stdio-mcp-server/headers";
|
|
3
|
+
export function validateModernHeaders(headers, request) {
|
|
4
|
+
const metadata = request.params?._meta;
|
|
5
|
+
const version = typeof metadata === "object" && metadata !== null
|
|
6
|
+
? metadata["io.modelcontextprotocol/protocolVersion"]
|
|
7
|
+
: undefined;
|
|
8
|
+
if (typeof version !== "string")
|
|
9
|
+
return {
|
|
10
|
+
code: JSON_RPC_ERROR_CODES.INVALID_PARAMS,
|
|
11
|
+
message: "Missing protocol version request metadata"
|
|
12
|
+
};
|
|
13
|
+
const expected = { "mcp-protocol-version": version };
|
|
14
|
+
// Notifications require the protocol version header but no method/name mirrors.
|
|
15
|
+
if ("id" in request) {
|
|
16
|
+
expected["mcp-method"] = request.method;
|
|
17
|
+
if (request.method === "tools/call" || request.method === "prompts/get")
|
|
18
|
+
expected["mcp-name"] = request.params?.name;
|
|
19
|
+
else if (request.method === "resources/read")
|
|
20
|
+
expected["mcp-name"] = request.params?.uri;
|
|
21
|
+
}
|
|
22
|
+
for (const [field, value] of Object.entries(expected)) {
|
|
23
|
+
const header = headers[field];
|
|
24
|
+
const actual = field === "mcp-name"
|
|
25
|
+
? decodeHeaderValue(header)
|
|
26
|
+
: typeof header === "string"
|
|
27
|
+
? header
|
|
28
|
+
: undefined;
|
|
29
|
+
if (typeof value !== "string" || actual === undefined || actual !== value)
|
|
30
|
+
return {
|
|
31
|
+
code: -32020,
|
|
32
|
+
message: `Header mismatch: ${field} must match the request body`
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
package/dist/parse-body.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { IncomingMessage } from "node:http";
|
|
2
2
|
import type { JSONRPCMessage, JSONRPCNotification, JSONRPCRequest, JSONRPCResponse } from "tiny-stdio-mcp-server";
|
|
3
3
|
export interface ClassifiedBody {
|
|
4
|
+
isBatch: boolean;
|
|
4
5
|
entries: Array<JSONRPCMessage | null>;
|
|
5
6
|
messages: JSONRPCMessage[];
|
|
6
7
|
hasRequests: boolean;
|
package/dist/parse-body.js
CHANGED
|
@@ -21,9 +21,7 @@ function isValidResponse(value) {
|
|
|
21
21
|
if (!hasOwn(value, "id")) {
|
|
22
22
|
return false;
|
|
23
23
|
}
|
|
24
|
-
if (value.id !== null &&
|
|
25
|
-
typeof value.id !== "string" &&
|
|
26
|
-
typeof value.id !== "number") {
|
|
24
|
+
if (value.id !== null && typeof value.id !== "string" && typeof value.id !== "number") {
|
|
27
25
|
return false;
|
|
28
26
|
}
|
|
29
27
|
const hasResult = hasOwn(value, "result");
|
|
@@ -61,7 +59,12 @@ async function readStreamBody(req, maxBytes) {
|
|
|
61
59
|
}
|
|
62
60
|
chunks.push(bytes);
|
|
63
61
|
}
|
|
64
|
-
|
|
62
|
+
try {
|
|
63
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks));
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
throw new Error("Parse error");
|
|
67
|
+
}
|
|
65
68
|
}
|
|
66
69
|
async function readRawBody(req, preParsed, options) {
|
|
67
70
|
const reqWithBody = req;
|
|
@@ -145,6 +148,7 @@ export async function readAndClassifyBody(req, preParsed, options = {}) {
|
|
|
145
148
|
entries.push(request);
|
|
146
149
|
}
|
|
147
150
|
return {
|
|
151
|
+
isBatch,
|
|
148
152
|
entries,
|
|
149
153
|
messages,
|
|
150
154
|
hasRequests: requests.length > 0,
|
|
@@ -152,6 +156,6 @@ export async function readAndClassifyBody(req, preParsed, options = {}) {
|
|
|
152
156
|
hasResponses: responses.length > 0,
|
|
153
157
|
requests,
|
|
154
158
|
notifications,
|
|
155
|
-
responses
|
|
159
|
+
responses
|
|
156
160
|
};
|
|
157
161
|
}
|