tiny-http-mcp-server 0.1.10 → 0.1.12
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/README.md +4 -4
- package/dist/cli.js +8 -2
- package/dist/composition.json +1 -1
- package/dist/http-transport.d.ts +4 -0
- package/dist/http-transport.js +34 -15
- package/node_modules/tiny-mcp-client/dist/index.d.ts +1 -0
- package/node_modules/tiny-mcp-client/dist/index.js +51 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -430,8 +430,8 @@ Returned `HttpServer` instances support:
|
|
|
430
430
|
| `allowedOrigins` | `readonly string[]` | `[]` | Allowed CORS `Origin` values. Empty means no cross-origin browser clients are allowed. |
|
|
431
431
|
| `maxRequestBytes` | `number` | unlimited | Maximum JSON request body size. |
|
|
432
432
|
| `maxBatchSize` | `number` | unlimited | Maximum JSON-RPC batch member count. |
|
|
433
|
-
| `maxSessions` | `number` |
|
|
434
|
-
| `sessionTtlMs` | `number` |
|
|
433
|
+
| `maxSessions` | `number` | `128` | Maximum active sessions. |
|
|
434
|
+
| `sessionTtlMs` | `number` | `900000` | Expire idle sessions after this duration. |
|
|
435
435
|
| `maxStreamsPerSession` | `number` | `1` | Maximum concurrent GET SSE streams per session. |
|
|
436
436
|
| `maxStreamBufferBytes` | `number` | `1048576` | End a GET SSE stream before a live write when its buffered bytes exceed this limit. |
|
|
437
437
|
| `maxSseEventHistory` | `number` | `100` | Number of server-sent events retained for `Last-Event-ID` replay. |
|
|
@@ -535,8 +535,8 @@ Returned by `listenHttp()`:
|
|
|
535
535
|
| `allowedOrigins` | `readonly string[]` | `[]` | Allowed CORS origins. |
|
|
536
536
|
| `maxRequestBytes` | `number` | unlimited | Maximum JSON request body size. |
|
|
537
537
|
| `maxBatchSize` | `number` | unlimited | Maximum JSON-RPC batch member count. |
|
|
538
|
-
| `maxSessions` | `number` |
|
|
539
|
-
| `sessionTtlMs` | `number` |
|
|
538
|
+
| `maxSessions` | `number` | `128` | Maximum active sessions. |
|
|
539
|
+
| `sessionTtlMs` | `number` | `900000` | Idle session expiration window. |
|
|
540
540
|
| `maxStreamsPerSession` | `number` | `1` | Maximum concurrent GET SSE streams per session. |
|
|
541
541
|
| `maxStreamBufferBytes` | `number` | `1048576` | End a GET SSE stream before a live write when its buffered bytes exceed this limit. |
|
|
542
542
|
| `maxSseEventHistory` | `number` | `100` | Number of SSE events retained for replay. |
|
package/dist/cli.js
CHANGED
|
@@ -31,8 +31,10 @@ const HELP_TEXT = [
|
|
|
31
31
|
" Maximum JSON request body size",
|
|
32
32
|
" --max-batch-size <count>",
|
|
33
33
|
" Maximum JSON-RPC batch member count",
|
|
34
|
-
" --max-sessions <count> Maximum active sessions",
|
|
35
|
-
" --
|
|
34
|
+
" --max-sessions <count> Maximum active sessions (default: 128)",
|
|
35
|
+
" --max-sessions-per-subject <count>",
|
|
36
|
+
" Maximum sessions per authenticated subject (default: 16)",
|
|
37
|
+
" --session-ttl-ms <ms> Expire idle sessions (default: 900000)",
|
|
36
38
|
" --max-streams-per-session <count>",
|
|
37
39
|
" Maximum concurrent GET SSE streams per session",
|
|
38
40
|
" --max-stream-buffer-bytes <bytes>",
|
|
@@ -192,6 +194,7 @@ function parseCliOptions(args) {
|
|
|
192
194
|
"max-request-bytes": { type: "string" },
|
|
193
195
|
"max-batch-size": { type: "string" },
|
|
194
196
|
"max-sessions": { type: "string" },
|
|
197
|
+
"max-sessions-per-subject": { type: "string" },
|
|
195
198
|
"session-ttl-ms": { type: "string" },
|
|
196
199
|
"max-streams-per-session": { type: "string" },
|
|
197
200
|
"max-stream-buffer-bytes": { type: "string" },
|
|
@@ -217,6 +220,7 @@ function parseCliOptions(args) {
|
|
|
217
220
|
const maxRequestBytes = parseOptionalInteger(values["max-request-bytes"], "--max-request-bytes", 1);
|
|
218
221
|
const maxBatchSize = parseOptionalInteger(values["max-batch-size"], "--max-batch-size", 1);
|
|
219
222
|
const maxSessions = parseOptionalInteger(values["max-sessions"], "--max-sessions", 1);
|
|
223
|
+
const maxSessionsPerSubject = parseOptionalInteger(values["max-sessions-per-subject"], "--max-sessions-per-subject", 1);
|
|
220
224
|
const sessionTtlMs = parseOptionalInteger(values["session-ttl-ms"], "--session-ttl-ms", 1);
|
|
221
225
|
const maxStreamsPerSession = parseOptionalInteger(values["max-streams-per-session"], "--max-streams-per-session", 1);
|
|
222
226
|
const maxStreamBufferBytes = parseOptionalInteger(values["max-stream-buffer-bytes"], "--max-stream-buffer-bytes", 0);
|
|
@@ -246,6 +250,7 @@ function parseCliOptions(args) {
|
|
|
246
250
|
...(maxRequestBytes === undefined ? {} : { maxRequestBytes }),
|
|
247
251
|
...(maxBatchSize === undefined ? {} : { maxBatchSize }),
|
|
248
252
|
...(maxSessions === undefined ? {} : { maxSessions }),
|
|
253
|
+
...(maxSessionsPerSubject === undefined ? {} : { maxSessionsPerSubject }),
|
|
249
254
|
...(sessionTtlMs === undefined ? {} : { sessionTtlMs }),
|
|
250
255
|
...(maxStreamsPerSession === undefined ? {} : { maxStreamsPerSession }),
|
|
251
256
|
...(maxStreamBufferBytes === undefined ? {} : { maxStreamBufferBytes }),
|
|
@@ -392,6 +397,7 @@ export async function runCli(args = process.argv.slice(2), dependencies = {}) {
|
|
|
392
397
|
: { maxRequestBytes: options.maxRequestBytes }),
|
|
393
398
|
...(options.maxBatchSize === undefined ? {} : { maxBatchSize: options.maxBatchSize }),
|
|
394
399
|
...(options.maxSessions === undefined ? {} : { maxSessions: options.maxSessions }),
|
|
400
|
+
...(options.maxSessionsPerSubject === undefined ? {} : { maxSessionsPerSubject: options.maxSessionsPerSubject }),
|
|
395
401
|
...(options.sessionTtlMs === undefined ? {} : { sessionTtlMs: options.sessionTtlMs }),
|
|
396
402
|
...(options.maxStreamsPerSession === undefined
|
|
397
403
|
? {}
|
package/dist/composition.json
CHANGED
package/dist/http-transport.d.ts
CHANGED
|
@@ -66,6 +66,8 @@ export interface StreamableHttpTransportOptions {
|
|
|
66
66
|
maxRequestBytes?: number;
|
|
67
67
|
maxBatchSize?: number;
|
|
68
68
|
maxSessions?: number;
|
|
69
|
+
/** Maximum sessions per authenticated subject or client ID; defaults to 16. */
|
|
70
|
+
maxSessionsPerSubject?: number;
|
|
69
71
|
sessionTtlMs?: number;
|
|
70
72
|
maxStreamsPerSession?: number;
|
|
71
73
|
maxStreamBufferBytes?: number;
|
|
@@ -88,6 +90,7 @@ export declare class StreamableHttpTransport {
|
|
|
88
90
|
private readonly maxRequestBytes;
|
|
89
91
|
private readonly maxBatchSize;
|
|
90
92
|
private readonly maxSessions;
|
|
93
|
+
private readonly maxSessionsPerSubject;
|
|
91
94
|
private readonly sessionTtlMs;
|
|
92
95
|
private readonly maxStreamsPerSession;
|
|
93
96
|
private readonly maxStreamBufferBytes;
|
|
@@ -128,6 +131,7 @@ export declare class StreamableHttpTransport {
|
|
|
128
131
|
private touchSession;
|
|
129
132
|
private isExpired;
|
|
130
133
|
private purgeExpiredSessions;
|
|
134
|
+
private sessions;
|
|
131
135
|
private sessionCount;
|
|
132
136
|
private deleteSession;
|
|
133
137
|
private createLocalMessageSession;
|
package/dist/http-transport.js
CHANGED
|
@@ -27,6 +27,7 @@ export class StreamableHttpTransport {
|
|
|
27
27
|
maxRequestBytes;
|
|
28
28
|
maxBatchSize;
|
|
29
29
|
maxSessions;
|
|
30
|
+
maxSessionsPerSubject;
|
|
30
31
|
sessionTtlMs;
|
|
31
32
|
maxStreamsPerSession;
|
|
32
33
|
maxStreamBufferBytes;
|
|
@@ -61,8 +62,10 @@ export class StreamableHttpTransport {
|
|
|
61
62
|
this.allowedHosts = new Set((options.allowedHosts ?? LOCAL_HOSTS).map((host) => this.normalizeHost(host)));
|
|
62
63
|
this.maxRequestBytes = validateOptionalIntegerOption("maxRequestBytes", options.maxRequestBytes, 1);
|
|
63
64
|
this.maxBatchSize = validateOptionalIntegerOption("maxBatchSize", options.maxBatchSize, 1);
|
|
64
|
-
this.maxSessions = validateOptionalIntegerOption("maxSessions", options.maxSessions, 1);
|
|
65
|
-
this.
|
|
65
|
+
this.maxSessions = validateOptionalIntegerOption("maxSessions", options.maxSessions, 1) ?? 128;
|
|
66
|
+
this.maxSessionsPerSubject =
|
|
67
|
+
validateOptionalIntegerOption("maxSessionsPerSubject", options.maxSessionsPerSubject, 1) ?? 16;
|
|
68
|
+
this.sessionTtlMs = validateOptionalIntegerOption("sessionTtlMs", options.sessionTtlMs, 1) ?? 15 * 60_000;
|
|
66
69
|
this.maxStreamsPerSession =
|
|
67
70
|
validateOptionalIntegerOption("maxStreamsPerSession", options.maxStreamsPerSession, 1) ?? 1;
|
|
68
71
|
this.maxStreamBufferBytes =
|
|
@@ -77,7 +80,7 @@ export class StreamableHttpTransport {
|
|
|
77
80
|
this.requestIdGenerator = options.requestIdGenerator ?? (() => `req-${this.nextRequestId++}`);
|
|
78
81
|
this.observability = options.observability ?? {};
|
|
79
82
|
this.trustedProxy = options.trustedProxy ?? false;
|
|
80
|
-
if (this.
|
|
83
|
+
if (this.sessionIdGenerator !== undefined) {
|
|
81
84
|
this.sessionExpiryInterval = setInterval(() => {
|
|
82
85
|
try {
|
|
83
86
|
this.purgeExpiredSessions();
|
|
@@ -228,10 +231,16 @@ export class StreamableHttpTransport {
|
|
|
228
231
|
this.respondWithRejection(res, 400, "session_id_required", "Mcp-Session-Id is required; initialize a session first.");
|
|
229
232
|
return;
|
|
230
233
|
}
|
|
231
|
-
|
|
234
|
+
this.purgeExpiredSessions();
|
|
235
|
+
if (this.sessionCount() >= this.maxSessions) {
|
|
232
236
|
this.respondWithRejection(res, 503, "session_limit_reached", "The server has reached its session limit; close a session or retry later.");
|
|
233
237
|
return;
|
|
234
238
|
}
|
|
239
|
+
const authSubject = this.readAuthSubject(req);
|
|
240
|
+
if (authSubject !== undefined && this.sessionCount(authSubject) >= this.maxSessionsPerSubject) {
|
|
241
|
+
this.respondWithRejection(res, 429, "subject_session_limit_reached", "The authenticated subject has reached its session limit; close a session or retry later.");
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
235
244
|
const newSessionId = this.sessionIdGenerator();
|
|
236
245
|
if (!this.isValidNewSessionId(newSessionId)) {
|
|
237
246
|
this.respondWithStatus(res, 500);
|
|
@@ -239,7 +248,6 @@ export class StreamableHttpTransport {
|
|
|
239
248
|
}
|
|
240
249
|
sessionId = newSessionId;
|
|
241
250
|
activeSession = this.sessionStore.create(newSessionId);
|
|
242
|
-
const authSubject = this.readAuthSubject(req);
|
|
243
251
|
if (authSubject !== undefined) {
|
|
244
252
|
activeSession.authSubject = authSubject;
|
|
245
253
|
}
|
|
@@ -562,26 +570,37 @@ export class StreamableHttpTransport {
|
|
|
562
570
|
session.lastSeenAt = new Date();
|
|
563
571
|
}
|
|
564
572
|
isExpired(session) {
|
|
565
|
-
if (this.sessionTtlMs === undefined) {
|
|
566
|
-
return false;
|
|
567
|
-
}
|
|
568
573
|
return Date.now() - session.lastSeenAt.getTime() > this.sessionTtlMs;
|
|
569
574
|
}
|
|
570
575
|
purgeExpiredSessions() {
|
|
571
|
-
|
|
572
|
-
return;
|
|
573
|
-
}
|
|
574
|
-
for (const session of [...this.sessionStore.entries()]) {
|
|
576
|
+
for (const session of [...this.sessions()]) {
|
|
575
577
|
if (this.isExpired(session)) {
|
|
576
578
|
this.deleteSession(session.id, "expired");
|
|
577
579
|
}
|
|
578
580
|
}
|
|
579
581
|
}
|
|
580
|
-
|
|
582
|
+
*sessions() {
|
|
581
583
|
if (this.sessionStore.entries !== undefined) {
|
|
582
|
-
|
|
584
|
+
yield* this.sessionStore.entries();
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
for (const id of this.sessionMessages.keys()) {
|
|
588
|
+
const session = this.sessionStore.get(id);
|
|
589
|
+
if (session !== undefined)
|
|
590
|
+
yield session;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
sessionCount(authSubject) {
|
|
595
|
+
if (authSubject === undefined && this.sessionStore.entries === undefined) {
|
|
596
|
+
return this.sessionMessages.size;
|
|
597
|
+
}
|
|
598
|
+
let count = 0;
|
|
599
|
+
for (const session of this.sessions()) {
|
|
600
|
+
if (authSubject === undefined || session.authSubject === authSubject)
|
|
601
|
+
count++;
|
|
583
602
|
}
|
|
584
|
-
return
|
|
603
|
+
return count;
|
|
585
604
|
}
|
|
586
605
|
deleteSession(sessionId, reason) {
|
|
587
606
|
const deleted = this.sessionStore.delete(sessionId);
|
|
@@ -567,6 +567,7 @@ declare class HttpTransport implements McpTransport {
|
|
|
567
567
|
private cancelOpenSseReaders;
|
|
568
568
|
private fetchWithAbort;
|
|
569
569
|
private consumeWrittenLines;
|
|
570
|
+
private sendPost;
|
|
570
571
|
private createPostHeaders;
|
|
571
572
|
private createGetHeaders;
|
|
572
573
|
private createDeleteHeaders;
|
|
@@ -2919,6 +2919,9 @@ var HttpTransport = class {
|
|
|
2919
2919
|
this.openSseReaders.clear();
|
|
2920
2920
|
}
|
|
2921
2921
|
async fetchWithAbort(input, init) {
|
|
2922
|
+
if (this.disposed) {
|
|
2923
|
+
throw new Error("HTTP transport disposed");
|
|
2924
|
+
}
|
|
2922
2925
|
const abortController = new AbortController();
|
|
2923
2926
|
this.inFlightFetchAbortControllers.add(abortController);
|
|
2924
2927
|
try {
|
|
@@ -2935,25 +2938,44 @@ var HttpTransport = class {
|
|
|
2935
2938
|
if (this.disposed || line.length === 0) {
|
|
2936
2939
|
continue;
|
|
2937
2940
|
}
|
|
2938
|
-
const
|
|
2939
|
-
const
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
this.dispose(new Error("HTTP transport session expired (404 response)"));
|
|
2947
|
-
return;
|
|
2941
|
+
const parsed = parseJsonRpcMessage(line);
|
|
2942
|
+
const post = this.sendPost(line);
|
|
2943
|
+
if (parsed.type === "request" && parsed.message.method === "initialize" || parsed.type === "notification" && parsed.message.method === "notifications/initialized") {
|
|
2944
|
+
await post;
|
|
2945
|
+
} else {
|
|
2946
|
+
void post.catch((error) => {
|
|
2947
|
+
this.dispose(error instanceof Error ? error : new Error(String(error)));
|
|
2948
|
+
});
|
|
2948
2949
|
}
|
|
2949
|
-
await this.throwForPostHttpError(response);
|
|
2950
|
-
this.captureSessionId(response);
|
|
2951
|
-
this.maybeOpenGetSseStream();
|
|
2952
|
-
void this.forwardResponseMessages(response).catch((error) => {
|
|
2953
|
-
this.dispose(error instanceof Error ? error : new Error(String(error)));
|
|
2954
|
-
});
|
|
2955
2950
|
}
|
|
2956
2951
|
}
|
|
2952
|
+
async sendPost(line) {
|
|
2953
|
+
const hasSessionId = this.sessionId !== void 0;
|
|
2954
|
+
const response = await this.fetchWithOAuthRetry({
|
|
2955
|
+
method: "POST",
|
|
2956
|
+
createHeaders: () => this.createPostHeaders(),
|
|
2957
|
+
body: line
|
|
2958
|
+
});
|
|
2959
|
+
if (this.disposed) {
|
|
2960
|
+
await response.body?.cancel();
|
|
2961
|
+
return;
|
|
2962
|
+
}
|
|
2963
|
+
if (hasSessionId && response.status === 404) {
|
|
2964
|
+
this.sessionId = void 0;
|
|
2965
|
+
this.dispose(new Error("HTTP transport session expired (404 response)"));
|
|
2966
|
+
return;
|
|
2967
|
+
}
|
|
2968
|
+
await this.throwForPostHttpError(response);
|
|
2969
|
+
if (this.disposed) {
|
|
2970
|
+
await response.body?.cancel();
|
|
2971
|
+
return;
|
|
2972
|
+
}
|
|
2973
|
+
this.captureSessionId(response);
|
|
2974
|
+
this.maybeOpenGetSseStream();
|
|
2975
|
+
void this.forwardResponseMessages(response).catch((error) => {
|
|
2976
|
+
this.dispose(error instanceof Error ? error : new Error(String(error)));
|
|
2977
|
+
});
|
|
2978
|
+
}
|
|
2957
2979
|
async createPostHeaders() {
|
|
2958
2980
|
const headers = new Headers(this.headers);
|
|
2959
2981
|
headers.set("Accept", "application/json, text/event-stream");
|
|
@@ -3750,6 +3772,19 @@ function isJsonRpcErrorObject(value) {
|
|
|
3750
3772
|
}
|
|
3751
3773
|
return value.data === void 0 || hasOwn(value, "data");
|
|
3752
3774
|
}
|
|
3775
|
+
function parseJsonRpcMessage(line) {
|
|
3776
|
+
let parsed;
|
|
3777
|
+
try {
|
|
3778
|
+
parsed = JSON.parse(line);
|
|
3779
|
+
} catch {
|
|
3780
|
+
return {
|
|
3781
|
+
type: "invalid",
|
|
3782
|
+
id: null,
|
|
3783
|
+
error: parseError()
|
|
3784
|
+
};
|
|
3785
|
+
}
|
|
3786
|
+
return parseJsonRpcPayload(parsed);
|
|
3787
|
+
}
|
|
3753
3788
|
function parseJsonRpcPayload(parsed) {
|
|
3754
3789
|
if (!isObjectRecord5(parsed)) {
|
|
3755
3790
|
return {
|