@j0hanz/superfetch 1.2.5 → 2.0.1
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 +131 -156
- package/dist/config/auth-config.d.ts +16 -0
- package/dist/config/auth-config.js +53 -0
- package/dist/config/constants.d.ts +11 -13
- package/dist/config/constants.js +1 -3
- package/dist/config/env-parsers.d.ts +7 -0
- package/dist/config/env-parsers.js +84 -0
- package/dist/config/formatting.d.ts +2 -2
- package/dist/config/index.d.ts +47 -53
- package/dist/config/index.js +35 -64
- package/dist/config/types/content.d.ts +1 -49
- package/dist/config/types/runtime.d.ts +8 -16
- package/dist/config/types/tools.d.ts +2 -28
- package/dist/http/accept-policy.d.ts +3 -0
- package/dist/http/accept-policy.js +45 -0
- package/dist/http/async-handler.d.ts +2 -0
- package/dist/http/async-handler.js +5 -0
- package/dist/http/auth-introspection.d.ts +2 -0
- package/dist/http/auth-introspection.js +141 -0
- package/dist/http/auth-static.d.ts +2 -0
- package/dist/http/auth-static.js +23 -0
- package/dist/http/auth.d.ts +3 -2
- package/dist/http/auth.js +254 -23
- package/dist/http/cors.d.ts +6 -6
- package/dist/http/cors.js +7 -42
- package/dist/http/download-routes.d.ts +0 -12
- package/dist/http/download-routes.js +21 -58
- package/dist/http/host-allowlist.d.ts +3 -0
- package/dist/http/host-allowlist.js +117 -0
- package/dist/http/jsonrpc-http.d.ts +2 -0
- package/dist/http/jsonrpc-http.js +10 -0
- package/dist/http/mcp-routes.d.ts +8 -3
- package/dist/http/mcp-routes.js +137 -31
- package/dist/http/mcp-session-eviction.d.ts +3 -0
- package/dist/http/mcp-session-eviction.js +24 -0
- package/dist/http/mcp-session-helpers.d.ts +0 -1
- package/dist/http/mcp-session-helpers.js +1 -1
- package/dist/http/mcp-session-init.d.ts +7 -0
- package/dist/http/mcp-session-init.js +94 -0
- package/dist/http/mcp-session-slots.d.ts +17 -0
- package/dist/http/mcp-session-slots.js +55 -0
- package/dist/http/mcp-session-transport-init.d.ts +7 -0
- package/dist/http/mcp-session-transport-init.js +41 -0
- package/dist/http/mcp-session-transport.d.ts +7 -0
- package/dist/http/mcp-session-transport.js +57 -0
- package/dist/http/mcp-session-types.d.ts +5 -0
- package/dist/http/mcp-session-types.js +1 -0
- package/dist/http/mcp-session.d.ts +9 -9
- package/dist/http/mcp-session.js +15 -137
- package/dist/http/mcp-sessions.d.ts +43 -0
- package/dist/http/mcp-sessions.js +392 -0
- package/dist/http/mcp-validation.d.ts +1 -0
- package/dist/http/mcp-validation.js +11 -10
- package/dist/http/protocol-policy.d.ts +2 -0
- package/dist/http/protocol-policy.js +31 -0
- package/dist/http/rate-limit.js +7 -4
- package/dist/http/server-config.d.ts +1 -0
- package/dist/http/server-config.js +40 -0
- package/dist/http/server-middleware.d.ts +7 -9
- package/dist/http/server-middleware.js +9 -70
- package/dist/http/server-shutdown.d.ts +4 -0
- package/dist/http/server-shutdown.js +43 -0
- package/dist/http/server.d.ts +10 -0
- package/dist/http/server.js +546 -61
- package/dist/http/session-cleanup.js +8 -5
- package/dist/middleware/error-handler.d.ts +1 -1
- package/dist/middleware/error-handler.js +32 -33
- package/dist/resources/cached-content-params.d.ts +5 -0
- package/dist/resources/cached-content-params.js +36 -0
- package/dist/resources/cached-content.js +67 -125
- package/dist/resources/index.js +0 -82
- package/dist/server.js +50 -29
- package/dist/services/cache-events.d.ts +8 -0
- package/dist/services/cache-events.js +19 -0
- package/dist/services/cache-keys.d.ts +7 -0
- package/dist/services/cache-keys.js +57 -0
- package/dist/services/cache.d.ts +4 -9
- package/dist/services/cache.js +77 -139
- package/dist/services/context.d.ts +0 -1
- package/dist/services/context.js +0 -7
- package/dist/services/extractor.js +55 -116
- package/dist/services/fetcher/agents.d.ts +2 -2
- package/dist/services/fetcher/agents.js +35 -96
- package/dist/services/fetcher/dns-selection.d.ts +2 -0
- package/dist/services/fetcher/dns-selection.js +72 -0
- package/dist/services/fetcher/interceptors.d.ts +0 -22
- package/dist/services/fetcher/interceptors.js +18 -32
- package/dist/services/fetcher/redirects.js +16 -7
- package/dist/services/fetcher/response.js +79 -34
- package/dist/services/fetcher.d.ts +22 -3
- package/dist/services/fetcher.js +544 -44
- package/dist/services/fifo-queue.d.ts +8 -0
- package/dist/services/fifo-queue.js +25 -0
- package/dist/services/logger.js +2 -2
- package/dist/services/metadata-collector.d.ts +1 -9
- package/dist/services/metadata-collector.js +71 -2
- package/dist/services/transform-worker-pool.d.ts +4 -14
- package/dist/services/transform-worker-pool.js +177 -129
- package/dist/services/transform-worker-types.d.ts +32 -0
- package/dist/services/transform-worker-types.js +14 -0
- package/dist/tools/handlers/fetch-markdown.tool.d.ts +3 -4
- package/dist/tools/handlers/fetch-markdown.tool.js +20 -72
- package/dist/tools/handlers/fetch-single.shared.d.ts +11 -22
- package/dist/tools/handlers/fetch-single.shared.js +175 -89
- package/dist/tools/handlers/fetch-url.tool.d.ts +7 -1
- package/dist/tools/handlers/fetch-url.tool.js +84 -119
- package/dist/tools/index.js +21 -40
- package/dist/tools/schemas.d.ts +1 -51
- package/dist/tools/schemas.js +1 -107
- package/dist/tools/utils/cached-markdown.d.ts +5 -0
- package/dist/tools/utils/cached-markdown.js +46 -0
- package/dist/tools/utils/content-shaping.d.ts +4 -0
- package/dist/tools/utils/content-shaping.js +67 -0
- package/dist/tools/utils/content-transform.d.ts +5 -17
- package/dist/tools/utils/content-transform.js +134 -114
- package/dist/tools/utils/fetch-pipeline.d.ts +0 -8
- package/dist/tools/utils/fetch-pipeline.js +57 -63
- package/dist/tools/utils/frontmatter.d.ts +3 -0
- package/dist/tools/utils/frontmatter.js +73 -0
- package/dist/tools/utils/inline-content.d.ts +1 -2
- package/dist/tools/utils/inline-content.js +4 -7
- package/dist/tools/utils/markdown-heuristics.d.ts +1 -0
- package/dist/tools/utils/markdown-heuristics.js +19 -0
- package/dist/tools/utils/markdown-signals.d.ts +1 -0
- package/dist/tools/utils/markdown-signals.js +19 -0
- package/dist/tools/utils/raw-markdown-frontmatter.d.ts +3 -0
- package/dist/tools/utils/raw-markdown-frontmatter.js +73 -0
- package/dist/tools/utils/raw-markdown.d.ts +6 -0
- package/dist/tools/utils/raw-markdown.js +135 -0
- package/dist/transformers/markdown/fenced-code-rule.d.ts +2 -0
- package/dist/transformers/markdown/fenced-code-rule.js +38 -0
- package/dist/transformers/markdown/frontmatter.d.ts +2 -0
- package/dist/transformers/markdown/frontmatter.js +45 -0
- package/dist/transformers/markdown/noise-rule.d.ts +2 -0
- package/dist/transformers/markdown/noise-rule.js +80 -0
- package/dist/transformers/markdown/turndown-instance.d.ts +2 -0
- package/dist/transformers/markdown/turndown-instance.js +19 -0
- package/dist/transformers/markdown.d.ts +2 -0
- package/dist/transformers/markdown.js +185 -0
- package/dist/transformers/markdown.transformer.js +5 -117
- package/dist/utils/cached-payload.d.ts +7 -0
- package/dist/utils/cached-payload.js +36 -0
- package/dist/utils/code-language-bash.d.ts +1 -0
- package/dist/utils/code-language-bash.js +48 -0
- package/dist/utils/code-language-core.d.ts +2 -0
- package/dist/utils/code-language-core.js +13 -0
- package/dist/utils/code-language-detectors.d.ts +5 -0
- package/dist/utils/code-language-detectors.js +142 -0
- package/dist/utils/code-language-helpers.d.ts +5 -0
- package/dist/utils/code-language-helpers.js +62 -0
- package/dist/utils/code-language-parsing.d.ts +5 -0
- package/dist/utils/code-language-parsing.js +62 -0
- package/dist/utils/code-language.d.ts +9 -0
- package/dist/utils/code-language.js +250 -46
- package/dist/utils/error-details.d.ts +3 -0
- package/dist/utils/error-details.js +12 -0
- package/dist/utils/error-utils.js +1 -1
- package/dist/utils/filename-generator.js +34 -12
- package/dist/utils/guards.d.ts +1 -0
- package/dist/utils/guards.js +3 -0
- package/dist/utils/header-normalizer.d.ts +0 -3
- package/dist/utils/header-normalizer.js +3 -3
- package/dist/utils/ip-address.d.ts +4 -0
- package/dist/utils/ip-address.js +6 -0
- package/dist/utils/tool-error-handler.d.ts +2 -2
- package/dist/utils/tool-error-handler.js +14 -46
- package/dist/utils/url-transformer.d.ts +7 -0
- package/dist/utils/url-transformer.js +147 -0
- package/dist/utils/url-validator.d.ts +1 -2
- package/dist/utils/url-validator.js +53 -114
- package/dist/workers/content-transform.worker.d.ts +1 -0
- package/dist/workers/content-transform.worker.js +40 -0
- package/package.json +17 -18
package/dist/http/mcp-session.js
CHANGED
|
@@ -1,145 +1,23 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
3
1
|
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
tracker.releaseSlot();
|
|
18
|
-
void transport.close().catch((error) => {
|
|
19
|
-
logWarn('Failed to close stalled session', {
|
|
20
|
-
error: getErrorMessage(error),
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
logWarn('Session initialization timed out', { timeoutMs });
|
|
24
|
-
}, timeoutMs);
|
|
25
|
-
timeout.unref();
|
|
26
|
-
return timeout;
|
|
27
|
-
}
|
|
28
|
-
function handleSessionInitialized(id, transport, options, tracker, clearInitTimeout) {
|
|
29
|
-
clearInitTimeout();
|
|
30
|
-
tracker.markInitialized();
|
|
31
|
-
tracker.releaseSlot();
|
|
32
|
-
const now = Date.now();
|
|
33
|
-
options.sessionStore.set(id, {
|
|
34
|
-
transport,
|
|
35
|
-
createdAt: now,
|
|
36
|
-
lastSeen: now,
|
|
37
|
-
});
|
|
38
|
-
logInfo('Session initialized', { sessionId: id });
|
|
39
|
-
}
|
|
40
|
-
function handleSessionClosed(id, options) {
|
|
41
|
-
options.sessionStore.remove(id);
|
|
42
|
-
logInfo('Session closed', { sessionId: id });
|
|
43
|
-
}
|
|
44
|
-
function handleTransportClose(transport, options, tracker, clearInitTimeout) {
|
|
45
|
-
clearInitTimeout();
|
|
46
|
-
if (!tracker.isInitialized()) {
|
|
47
|
-
tracker.releaseSlot();
|
|
48
|
-
}
|
|
49
|
-
if (transport.sessionId) {
|
|
50
|
-
options.sessionStore.remove(transport.sessionId);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
function createTransportForNewSession(options) {
|
|
54
|
-
const tracker = createSlotTracker();
|
|
55
|
-
let initTimeout = null;
|
|
56
|
-
const clearInitTimeout = () => {
|
|
57
|
-
if (!initTimeout)
|
|
58
|
-
return;
|
|
59
|
-
clearTimeout(initTimeout);
|
|
60
|
-
initTimeout = null;
|
|
61
|
-
};
|
|
62
|
-
const transport = new StreamableHTTPServerTransport({
|
|
63
|
-
sessionIdGenerator: () => randomUUID(),
|
|
64
|
-
onsessioninitialized: (id) => {
|
|
65
|
-
handleSessionInitialized(id, transport, options, tracker, clearInitTimeout);
|
|
66
|
-
},
|
|
67
|
-
onsessionclosed: (id) => {
|
|
68
|
-
handleSessionClosed(id, options);
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
transport.onclose = () => {
|
|
72
|
-
handleTransportClose(transport, options, tracker, clearInitTimeout);
|
|
73
|
-
};
|
|
74
|
-
initTimeout = startSessionInitTimeout(transport, tracker, clearInitTimeout, config.server.sessionInitTimeoutMs);
|
|
75
|
-
return { transport, releaseSlot: tracker.releaseSlot, clearInitTimeout };
|
|
76
|
-
}
|
|
77
|
-
function findExistingTransport(sessionId, options) {
|
|
78
|
-
if (!sessionId) {
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
const existingSession = options.sessionStore.get(sessionId);
|
|
82
|
-
if (!existingSession) {
|
|
2
|
+
import { sendJsonRpcError } from './jsonrpc-http.js';
|
|
3
|
+
import { evictExpiredSessions } from './mcp-session-eviction.js';
|
|
4
|
+
import { createAndConnectTransport } from './mcp-session-init.js';
|
|
5
|
+
import { respondBadRequest } from './mcp-session-slots.js';
|
|
6
|
+
export async function resolveTransportForPost({ res, body, sessionId, options, }) {
|
|
7
|
+
if (sessionId) {
|
|
8
|
+
const existingSession = options.sessionStore.get(sessionId);
|
|
9
|
+
if (existingSession) {
|
|
10
|
+
options.sessionStore.touch(sessionId);
|
|
11
|
+
return existingSession.transport;
|
|
12
|
+
}
|
|
13
|
+
// Client supplied a session id but it doesn't exist; Streamable HTTP: invalid session IDs => 404.
|
|
14
|
+
sendJsonRpcError(res, -32600, 'Session not found', 404);
|
|
83
15
|
return null;
|
|
84
16
|
}
|
|
85
|
-
|
|
86
|
-
return existingSession.transport;
|
|
87
|
-
}
|
|
88
|
-
function shouldInitializeSession(sessionId, body) {
|
|
89
|
-
return !sessionId && isInitializeRequest(body);
|
|
90
|
-
}
|
|
91
|
-
async function createAndConnectTransport(options, res) {
|
|
92
|
-
if (!ensureSessionCapacity(options.sessionStore, options.maxSessions, res, evictOldestSession)) {
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
if (!reserveSessionSlot(options.sessionStore, options.maxSessions)) {
|
|
96
|
-
respondServerBusy(res);
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
const { transport, releaseSlot, clearInitTimeout } = createTransportForNewSession(options);
|
|
100
|
-
const mcpServer = createMcpServer();
|
|
101
|
-
try {
|
|
102
|
-
await mcpServer.connect(transport);
|
|
103
|
-
}
|
|
104
|
-
catch (error) {
|
|
105
|
-
clearInitTimeout();
|
|
106
|
-
releaseSlot();
|
|
107
|
-
logError('Failed to initialize MCP session', error instanceof Error ? error : undefined);
|
|
108
|
-
throw error;
|
|
109
|
-
}
|
|
110
|
-
return transport;
|
|
111
|
-
}
|
|
112
|
-
export async function resolveTransportForPost(_req, res, body, sessionId, options) {
|
|
113
|
-
const existingTransport = findExistingTransport(sessionId, options);
|
|
114
|
-
if (existingTransport) {
|
|
115
|
-
return existingTransport;
|
|
116
|
-
}
|
|
117
|
-
if (!shouldInitializeSession(sessionId, body)) {
|
|
17
|
+
if (!isInitializeRequest(body)) {
|
|
118
18
|
respondBadRequest(res);
|
|
119
19
|
return null;
|
|
120
20
|
}
|
|
121
21
|
evictExpiredSessions(options.sessionStore);
|
|
122
|
-
return createAndConnectTransport(options, res);
|
|
123
|
-
}
|
|
124
|
-
export function evictExpiredSessions(store) {
|
|
125
|
-
const evicted = store.evictExpired();
|
|
126
|
-
for (const session of evicted) {
|
|
127
|
-
void session.transport.close().catch((error) => {
|
|
128
|
-
logWarn('Failed to close expired session', {
|
|
129
|
-
error: getErrorMessage(error),
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
return evicted.length;
|
|
134
|
-
}
|
|
135
|
-
function evictOldestSession(store) {
|
|
136
|
-
const session = store.evictOldest();
|
|
137
|
-
if (!session)
|
|
138
|
-
return false;
|
|
139
|
-
void session.transport.close().catch((error) => {
|
|
140
|
-
logWarn('Failed to close evicted session', {
|
|
141
|
-
error: getErrorMessage(error),
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
return true;
|
|
22
|
+
return createAndConnectTransport({ options, res });
|
|
145
23
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Request, Response } from 'express';
|
|
2
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
3
|
+
import type { SessionEntry } from '../config/types/runtime.js';
|
|
4
|
+
export interface SessionStore {
|
|
5
|
+
get: (sessionId: string) => SessionEntry | undefined;
|
|
6
|
+
touch: (sessionId: string) => void;
|
|
7
|
+
set: (sessionId: string, entry: SessionEntry) => void;
|
|
8
|
+
remove: (sessionId: string) => SessionEntry | undefined;
|
|
9
|
+
size: () => number;
|
|
10
|
+
clear: () => SessionEntry[];
|
|
11
|
+
evictExpired: () => SessionEntry[];
|
|
12
|
+
evictOldest: () => SessionEntry | undefined;
|
|
13
|
+
}
|
|
14
|
+
export interface McpSessionOptions {
|
|
15
|
+
readonly sessionStore: SessionStore;
|
|
16
|
+
readonly maxSessions: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function sendJsonRpcError(res: Response, code: number, message: string, status?: number): void;
|
|
19
|
+
export declare function getSessionId(req: Request): string | undefined;
|
|
20
|
+
export declare function createSessionStore(sessionTtlMs: number): SessionStore;
|
|
21
|
+
export declare function reserveSessionSlot(store: SessionStore, maxSessions: number): boolean;
|
|
22
|
+
interface SlotTracker {
|
|
23
|
+
readonly releaseSlot: () => void;
|
|
24
|
+
readonly markInitialized: () => void;
|
|
25
|
+
readonly isInitialized: () => boolean;
|
|
26
|
+
}
|
|
27
|
+
export declare function createSlotTracker(): SlotTracker;
|
|
28
|
+
export declare function ensureSessionCapacity({ store, maxSessions, res, evictOldest, }: {
|
|
29
|
+
store: SessionStore;
|
|
30
|
+
maxSessions: number;
|
|
31
|
+
res: Response;
|
|
32
|
+
evictOldest: (store: SessionStore) => boolean;
|
|
33
|
+
}): boolean;
|
|
34
|
+
export declare function resolveTransportForPost({ res, body, sessionId, options, }: {
|
|
35
|
+
res: Response;
|
|
36
|
+
body: {
|
|
37
|
+
method: string;
|
|
38
|
+
};
|
|
39
|
+
sessionId: string | undefined;
|
|
40
|
+
options: McpSessionOptions;
|
|
41
|
+
}): Promise<StreamableHTTPServerTransport | null>;
|
|
42
|
+
export declare function startSessionCleanupLoop(store: SessionStore, sessionTtlMs: number): AbortController;
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { setInterval as setIntervalPromise } from 'node:timers/promises';
|
|
3
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
4
|
+
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { config } from '../config/index.js';
|
|
6
|
+
import { logError, logInfo, logWarn } from '../services/logger.js';
|
|
7
|
+
import { getErrorMessage } from '../utils/error-details.js';
|
|
8
|
+
import { createMcpServer } from '../server.js';
|
|
9
|
+
export function sendJsonRpcError(res, code, message, status = 400) {
|
|
10
|
+
res.status(status).json({
|
|
11
|
+
jsonrpc: '2.0',
|
|
12
|
+
error: {
|
|
13
|
+
code,
|
|
14
|
+
message,
|
|
15
|
+
},
|
|
16
|
+
id: null,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export function getSessionId(req) {
|
|
20
|
+
const header = req.headers['mcp-session-id'];
|
|
21
|
+
return Array.isArray(header) ? header[0] : header;
|
|
22
|
+
}
|
|
23
|
+
export function createSessionStore(sessionTtlMs) {
|
|
24
|
+
const sessions = new Map();
|
|
25
|
+
return {
|
|
26
|
+
get: (sessionId) => sessions.get(sessionId),
|
|
27
|
+
touch: (sessionId) => {
|
|
28
|
+
touchSession(sessions, sessionId);
|
|
29
|
+
},
|
|
30
|
+
set: (sessionId, entry) => {
|
|
31
|
+
sessions.set(sessionId, entry);
|
|
32
|
+
},
|
|
33
|
+
remove: (sessionId) => removeSession(sessions, sessionId),
|
|
34
|
+
size: () => sessions.size,
|
|
35
|
+
clear: () => clearSessions(sessions),
|
|
36
|
+
evictExpired: () => evictExpiredSessions(sessions, sessionTtlMs),
|
|
37
|
+
evictOldest: () => evictOldestSession(sessions),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function touchSession(sessions, sessionId) {
|
|
41
|
+
const session = sessions.get(sessionId);
|
|
42
|
+
if (session) {
|
|
43
|
+
session.lastSeen = Date.now();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function removeSession(sessions, sessionId) {
|
|
47
|
+
const session = sessions.get(sessionId);
|
|
48
|
+
sessions.delete(sessionId);
|
|
49
|
+
return session;
|
|
50
|
+
}
|
|
51
|
+
function clearSessions(sessions) {
|
|
52
|
+
const entries = Array.from(sessions.values());
|
|
53
|
+
sessions.clear();
|
|
54
|
+
return entries;
|
|
55
|
+
}
|
|
56
|
+
function evictExpiredSessions(sessions, sessionTtlMs) {
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
const evicted = [];
|
|
59
|
+
for (const [id, session] of sessions.entries()) {
|
|
60
|
+
if (now - session.lastSeen > sessionTtlMs) {
|
|
61
|
+
sessions.delete(id);
|
|
62
|
+
evicted.push(session);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return evicted;
|
|
66
|
+
}
|
|
67
|
+
function evictOldestSession(sessions) {
|
|
68
|
+
let oldestId;
|
|
69
|
+
let oldestSeen = Number.POSITIVE_INFINITY;
|
|
70
|
+
for (const [id, session] of sessions.entries()) {
|
|
71
|
+
if (session.lastSeen < oldestSeen) {
|
|
72
|
+
oldestSeen = session.lastSeen;
|
|
73
|
+
oldestId = id;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (!oldestId)
|
|
77
|
+
return undefined;
|
|
78
|
+
const session = sessions.get(oldestId);
|
|
79
|
+
sessions.delete(oldestId);
|
|
80
|
+
return session;
|
|
81
|
+
}
|
|
82
|
+
let inFlightSessions = 0;
|
|
83
|
+
export function reserveSessionSlot(store, maxSessions) {
|
|
84
|
+
if (store.size() + inFlightSessions >= maxSessions) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
inFlightSessions += 1;
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
function releaseSessionSlot() {
|
|
91
|
+
if (inFlightSessions > 0) {
|
|
92
|
+
inFlightSessions -= 1;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export function createSlotTracker() {
|
|
96
|
+
let slotReleased = false;
|
|
97
|
+
let initialized = false;
|
|
98
|
+
return {
|
|
99
|
+
releaseSlot: () => {
|
|
100
|
+
if (slotReleased)
|
|
101
|
+
return;
|
|
102
|
+
slotReleased = true;
|
|
103
|
+
releaseSessionSlot();
|
|
104
|
+
},
|
|
105
|
+
markInitialized: () => {
|
|
106
|
+
initialized = true;
|
|
107
|
+
},
|
|
108
|
+
isInitialized: () => initialized,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function isServerAtCapacity(store, maxSessions) {
|
|
112
|
+
return store.size() + inFlightSessions >= maxSessions;
|
|
113
|
+
}
|
|
114
|
+
function tryEvictSlot(store, maxSessions, evictOldest) {
|
|
115
|
+
const currentSize = store.size();
|
|
116
|
+
const canFreeSlot = currentSize >= maxSessions &&
|
|
117
|
+
currentSize - 1 + inFlightSessions < maxSessions;
|
|
118
|
+
return canFreeSlot && evictOldest(store);
|
|
119
|
+
}
|
|
120
|
+
export function ensureSessionCapacity({ store, maxSessions, res, evictOldest, }) {
|
|
121
|
+
if (!isServerAtCapacity(store, maxSessions)) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
if (tryEvictSlot(store, maxSessions, evictOldest)) {
|
|
125
|
+
return !isServerAtCapacity(store, maxSessions);
|
|
126
|
+
}
|
|
127
|
+
respondServerBusy(res);
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
function respondServerBusy(res) {
|
|
131
|
+
sendJsonRpcError(res, -32000, 'Server busy: maximum sessions reached', 503);
|
|
132
|
+
}
|
|
133
|
+
function respondBadRequest(res) {
|
|
134
|
+
sendJsonRpcError(res, -32000, 'Bad Request: Missing session ID or not an initialize request', 400);
|
|
135
|
+
}
|
|
136
|
+
function createTimeoutController() {
|
|
137
|
+
let initTimeout = null;
|
|
138
|
+
return {
|
|
139
|
+
clear: () => {
|
|
140
|
+
if (!initTimeout)
|
|
141
|
+
return;
|
|
142
|
+
clearTimeout(initTimeout);
|
|
143
|
+
initTimeout = null;
|
|
144
|
+
},
|
|
145
|
+
set: (timeout) => {
|
|
146
|
+
initTimeout = timeout;
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function createTransportAdapter(transport) {
|
|
151
|
+
const adapter = buildTransportAdapter(transport);
|
|
152
|
+
attachTransportAccessors(adapter, transport);
|
|
153
|
+
return adapter;
|
|
154
|
+
}
|
|
155
|
+
function buildTransportAdapter(transport) {
|
|
156
|
+
return {
|
|
157
|
+
start: () => transport.start(),
|
|
158
|
+
send: (message, options) => transport.send(message, options),
|
|
159
|
+
close: () => transport.close(),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function createAccessorDescriptor(getter, setter) {
|
|
163
|
+
return {
|
|
164
|
+
get: getter,
|
|
165
|
+
...(setter ? { set: setter } : {}),
|
|
166
|
+
enumerable: true,
|
|
167
|
+
configurable: true,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function createOnCloseDescriptor(transport) {
|
|
171
|
+
return createAccessorDescriptor(() => transport.onclose, (handler) => {
|
|
172
|
+
transport.onclose = handler;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function createOnErrorDescriptor(transport) {
|
|
176
|
+
return createAccessorDescriptor(() => transport.onerror, (handler) => {
|
|
177
|
+
transport.onerror = handler;
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
function createOnMessageDescriptor(transport) {
|
|
181
|
+
return createAccessorDescriptor(() => transport.onmessage, (handler) => {
|
|
182
|
+
transport.onmessage = handler;
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
function attachTransportAccessors(adapter, transport) {
|
|
186
|
+
Object.defineProperties(adapter, {
|
|
187
|
+
onclose: createOnCloseDescriptor(transport),
|
|
188
|
+
onerror: createOnErrorDescriptor(transport),
|
|
189
|
+
onmessage: createOnMessageDescriptor(transport),
|
|
190
|
+
sessionId: createAccessorDescriptor(() => transport.sessionId),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
function startSessionInitTimeout({ transport, tracker, clearInitTimeout, timeoutMs, }) {
|
|
194
|
+
if (timeoutMs <= 0)
|
|
195
|
+
return null;
|
|
196
|
+
const timeout = setTimeout(() => {
|
|
197
|
+
clearInitTimeout();
|
|
198
|
+
if (tracker.isInitialized())
|
|
199
|
+
return;
|
|
200
|
+
tracker.releaseSlot();
|
|
201
|
+
void transport.close().catch((error) => {
|
|
202
|
+
logWarn('Failed to close stalled session', {
|
|
203
|
+
error: getErrorMessage(error),
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
logWarn('Session initialization timed out', { timeoutMs });
|
|
207
|
+
}, timeoutMs);
|
|
208
|
+
timeout.unref();
|
|
209
|
+
return timeout;
|
|
210
|
+
}
|
|
211
|
+
function createSessionTransport({ tracker, timeoutController, }) {
|
|
212
|
+
const transport = new StreamableHTTPServerTransport({
|
|
213
|
+
sessionIdGenerator: () => randomUUID(),
|
|
214
|
+
});
|
|
215
|
+
transport.onclose = () => {
|
|
216
|
+
timeoutController.clear();
|
|
217
|
+
if (!tracker.isInitialized()) {
|
|
218
|
+
tracker.releaseSlot();
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
timeoutController.set(startSessionInitTimeout({
|
|
222
|
+
transport,
|
|
223
|
+
tracker,
|
|
224
|
+
clearInitTimeout: timeoutController.clear,
|
|
225
|
+
timeoutMs: config.server.sessionInitTimeoutMs,
|
|
226
|
+
}));
|
|
227
|
+
return transport;
|
|
228
|
+
}
|
|
229
|
+
async function connectTransportOrThrow({ transport, clearInitTimeout, releaseSlot, }) {
|
|
230
|
+
const mcpServer = createMcpServer();
|
|
231
|
+
const transportAdapter = createTransportAdapter(transport);
|
|
232
|
+
try {
|
|
233
|
+
await mcpServer.connect(transportAdapter);
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
clearInitTimeout();
|
|
237
|
+
releaseSlot();
|
|
238
|
+
void transport.close().catch((closeError) => {
|
|
239
|
+
logWarn('Failed to close transport after connect error', {
|
|
240
|
+
error: getErrorMessage(closeError),
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
logError('Failed to initialize MCP session', error instanceof Error ? error : undefined);
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function evictExpiredSessionsWithClose(store) {
|
|
248
|
+
const evicted = store.evictExpired();
|
|
249
|
+
for (const session of evicted) {
|
|
250
|
+
void session.transport.close().catch((error) => {
|
|
251
|
+
logWarn('Failed to close expired session', {
|
|
252
|
+
error: getErrorMessage(error),
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return evicted.length;
|
|
257
|
+
}
|
|
258
|
+
function evictOldestSessionWithClose(store) {
|
|
259
|
+
const session = store.evictOldest();
|
|
260
|
+
if (!session)
|
|
261
|
+
return false;
|
|
262
|
+
void session.transport.close().catch((error) => {
|
|
263
|
+
logWarn('Failed to close evicted session', {
|
|
264
|
+
error: getErrorMessage(error),
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
function reserveSessionIfPossible({ options, res, }) {
|
|
270
|
+
if (!ensureSessionCapacity({
|
|
271
|
+
store: options.sessionStore,
|
|
272
|
+
maxSessions: options.maxSessions,
|
|
273
|
+
res,
|
|
274
|
+
evictOldest: evictOldestSessionWithClose,
|
|
275
|
+
})) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
if (!reserveSessionSlot(options.sessionStore, options.maxSessions)) {
|
|
279
|
+
respondServerBusy(res);
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
function resolveSessionId({ transport, res, tracker, clearInitTimeout, }) {
|
|
285
|
+
const { sessionId } = transport;
|
|
286
|
+
if (typeof sessionId !== 'string') {
|
|
287
|
+
clearInitTimeout();
|
|
288
|
+
tracker.releaseSlot();
|
|
289
|
+
respondBadRequest(res);
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
return sessionId;
|
|
293
|
+
}
|
|
294
|
+
function finalizeSession({ store, transport, sessionId, tracker, clearInitTimeout, }) {
|
|
295
|
+
clearInitTimeout();
|
|
296
|
+
tracker.markInitialized();
|
|
297
|
+
tracker.releaseSlot();
|
|
298
|
+
const now = Date.now();
|
|
299
|
+
store.set(sessionId, {
|
|
300
|
+
transport,
|
|
301
|
+
createdAt: now,
|
|
302
|
+
lastSeen: now,
|
|
303
|
+
});
|
|
304
|
+
transport.onclose = () => {
|
|
305
|
+
store.remove(sessionId);
|
|
306
|
+
logInfo('Session closed');
|
|
307
|
+
};
|
|
308
|
+
logInfo('Session initialized');
|
|
309
|
+
}
|
|
310
|
+
async function createAndConnectTransport({ options, res, }) {
|
|
311
|
+
if (!reserveSessionIfPossible({ options, res }))
|
|
312
|
+
return null;
|
|
313
|
+
const tracker = createSlotTracker();
|
|
314
|
+
const timeoutController = createTimeoutController();
|
|
315
|
+
const transport = createSessionTransport({ tracker, timeoutController });
|
|
316
|
+
await connectTransportOrThrow({
|
|
317
|
+
transport,
|
|
318
|
+
clearInitTimeout: timeoutController.clear,
|
|
319
|
+
releaseSlot: tracker.releaseSlot,
|
|
320
|
+
});
|
|
321
|
+
const sessionId = resolveSessionId({
|
|
322
|
+
transport,
|
|
323
|
+
res,
|
|
324
|
+
tracker,
|
|
325
|
+
clearInitTimeout: timeoutController.clear,
|
|
326
|
+
});
|
|
327
|
+
if (!sessionId)
|
|
328
|
+
return null;
|
|
329
|
+
finalizeSession({
|
|
330
|
+
store: options.sessionStore,
|
|
331
|
+
transport,
|
|
332
|
+
sessionId,
|
|
333
|
+
tracker,
|
|
334
|
+
clearInitTimeout: timeoutController.clear,
|
|
335
|
+
});
|
|
336
|
+
return transport;
|
|
337
|
+
}
|
|
338
|
+
export async function resolveTransportForPost({ res, body, sessionId, options, }) {
|
|
339
|
+
if (sessionId) {
|
|
340
|
+
const existingSession = options.sessionStore.get(sessionId);
|
|
341
|
+
if (existingSession) {
|
|
342
|
+
options.sessionStore.touch(sessionId);
|
|
343
|
+
return existingSession.transport;
|
|
344
|
+
}
|
|
345
|
+
// Client supplied a session id but it doesn't exist; Streamable HTTP: invalid session IDs => 404.
|
|
346
|
+
sendJsonRpcError(res, -32600, 'Session not found', 404);
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
if (!isInitializeRequest(body)) {
|
|
350
|
+
respondBadRequest(res);
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
evictExpiredSessionsWithClose(options.sessionStore);
|
|
354
|
+
return createAndConnectTransport({ options, res });
|
|
355
|
+
}
|
|
356
|
+
export function startSessionCleanupLoop(store, sessionTtlMs) {
|
|
357
|
+
const controller = new AbortController();
|
|
358
|
+
void runSessionCleanupLoop(store, sessionTtlMs, controller.signal).catch(handleSessionCleanupError);
|
|
359
|
+
return controller;
|
|
360
|
+
}
|
|
361
|
+
async function runSessionCleanupLoop(store, sessionTtlMs, signal) {
|
|
362
|
+
const intervalMs = getCleanupIntervalMs(sessionTtlMs);
|
|
363
|
+
for await (const getNow of setIntervalPromise(intervalMs, Date.now, {
|
|
364
|
+
signal,
|
|
365
|
+
ref: false,
|
|
366
|
+
})) {
|
|
367
|
+
handleSessionEvictions(store, getNow());
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function getCleanupIntervalMs(sessionTtlMs) {
|
|
371
|
+
return Math.min(Math.max(Math.floor(sessionTtlMs / 2), 10000), 60000);
|
|
372
|
+
}
|
|
373
|
+
function isAbortError(error) {
|
|
374
|
+
return error instanceof Error && error.name === 'AbortError';
|
|
375
|
+
}
|
|
376
|
+
function handleSessionEvictions(store, now) {
|
|
377
|
+
const evicted = evictExpiredSessionsWithClose(store);
|
|
378
|
+
if (evicted > 0) {
|
|
379
|
+
logInfo('Expired sessions evicted', {
|
|
380
|
+
evicted,
|
|
381
|
+
timestamp: new Date(now).toISOString(),
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function handleSessionCleanupError(error) {
|
|
386
|
+
if (isAbortError(error)) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
logWarn('Session cleanup loop failed', {
|
|
390
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
391
|
+
});
|
|
392
|
+
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const paramsSchema = z.looseObject({});
|
|
3
|
+
const mcpRequestSchema = z.looseObject({
|
|
4
|
+
jsonrpc: z.literal('2.0'),
|
|
5
|
+
method: z.string().min(1),
|
|
6
|
+
id: z.union([z.string(), z.number()]).optional(),
|
|
7
|
+
params: paramsSchema.optional(),
|
|
8
|
+
});
|
|
9
|
+
export function isJsonRpcBatchRequest(body) {
|
|
10
|
+
return Array.isArray(body);
|
|
3
11
|
}
|
|
4
12
|
export function isMcpRequestBody(body) {
|
|
5
|
-
|
|
6
|
-
return false;
|
|
7
|
-
const { method, id, jsonrpc, params } = body;
|
|
8
|
-
const methodValid = method === undefined || typeof method === 'string';
|
|
9
|
-
const idValid = id === undefined || typeof id === 'string' || typeof id === 'number';
|
|
10
|
-
const jsonrpcValid = jsonrpc === undefined || jsonrpc === '2.0';
|
|
11
|
-
const paramsValid = params === undefined || typeof params === 'object';
|
|
12
|
-
return methodValid && idValid && jsonrpcValid && paramsValid;
|
|
13
|
+
return mcpRequestSchema.safeParse(body).success;
|
|
13
14
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { sendJsonRpcError } from './jsonrpc-http.js';
|
|
2
|
+
const MCP_PROTOCOL_VERSION_HEADER = 'mcp-protocol-version';
|
|
3
|
+
const MCP_PROTOCOL_VERSIONS = {
|
|
4
|
+
defaultVersion: '2025-03-26',
|
|
5
|
+
supported: new Set(['2025-03-26', '2025-11-25']),
|
|
6
|
+
};
|
|
7
|
+
function getHeaderValue(req, headerNameLower) {
|
|
8
|
+
const value = req.headers[headerNameLower];
|
|
9
|
+
if (typeof value === 'string')
|
|
10
|
+
return value;
|
|
11
|
+
if (Array.isArray(value))
|
|
12
|
+
return value[0] ?? null;
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
function setHeaderValue(req, headerNameLower, value) {
|
|
16
|
+
// Express exposes req.headers as a plain object, but the type is readonly-ish.
|
|
17
|
+
req.headers[headerNameLower] = value;
|
|
18
|
+
}
|
|
19
|
+
export function ensureMcpProtocolVersionHeader(req, res) {
|
|
20
|
+
const raw = getHeaderValue(req, MCP_PROTOCOL_VERSION_HEADER);
|
|
21
|
+
const version = raw?.trim();
|
|
22
|
+
if (!version) {
|
|
23
|
+
setHeaderValue(req, MCP_PROTOCOL_VERSION_HEADER, MCP_PROTOCOL_VERSIONS.defaultVersion);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
if (!MCP_PROTOCOL_VERSIONS.supported.has(version)) {
|
|
27
|
+
sendJsonRpcError(res, -32600, `Unsupported MCP-Protocol-Version: ${version}`, 400);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
}
|
package/dist/http/rate-limit.js
CHANGED
|
@@ -13,7 +13,11 @@ export function createRateLimitMiddleware(options) {
|
|
|
13
13
|
const stop = () => {
|
|
14
14
|
cleanupController.abort();
|
|
15
15
|
};
|
|
16
|
-
const middleware = (
|
|
16
|
+
const middleware = createRateLimitHandler(store, options);
|
|
17
|
+
return { middleware, stop, store };
|
|
18
|
+
}
|
|
19
|
+
function createRateLimitHandler(store, options) {
|
|
20
|
+
return (req, res, next) => {
|
|
17
21
|
if (shouldSkipRateLimit(req, options)) {
|
|
18
22
|
next();
|
|
19
23
|
return;
|
|
@@ -30,11 +34,10 @@ export function createRateLimitMiddleware(options) {
|
|
|
30
34
|
}
|
|
31
35
|
next();
|
|
32
36
|
};
|
|
33
|
-
return { middleware, stop, store };
|
|
34
37
|
}
|
|
35
38
|
async function startCleanupLoop(store, options, signal) {
|
|
36
|
-
for await (const
|
|
37
|
-
evictStaleEntries(store, options,
|
|
39
|
+
for await (const getNow of setIntervalPromise(options.cleanupIntervalMs, Date.now, { signal, ref: false })) {
|
|
40
|
+
evictStaleEntries(store, options, getNow());
|
|
38
41
|
}
|
|
39
42
|
}
|
|
40
43
|
function evictStaleEntries(store, options, now) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function assertHttpConfiguration(): void;
|