@j0hanz/superfetch 2.0.0 → 2.1.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/README.md +139 -46
- package/dist/cache.d.ts +42 -0
- package/dist/cache.js +565 -0
- package/dist/config/env-parsers.d.ts +1 -0
- package/dist/config/env-parsers.js +12 -0
- package/dist/config/index.d.ts +7 -0
- package/dist/config/index.js +20 -8
- package/dist/config/types/content.d.ts +1 -0
- package/dist/config.d.ts +77 -0
- package/dist/config.js +261 -0
- package/dist/crypto.d.ts +2 -0
- package/dist/crypto.js +32 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.js +28 -0
- package/dist/fetch.d.ts +40 -0
- package/dist/fetch.js +910 -0
- package/dist/http/auth.js +161 -2
- package/dist/http/base-middleware.d.ts +7 -0
- package/dist/http/base-middleware.js +143 -0
- package/dist/http/cors.d.ts +0 -5
- package/dist/http/cors.js +0 -6
- package/dist/http/download-routes.js +6 -2
- package/dist/http/error-handler.d.ts +2 -0
- package/dist/http/error-handler.js +55 -0
- package/dist/http/host-allowlist.d.ts +3 -0
- package/dist/http/host-allowlist.js +117 -0
- package/dist/http/mcp-routes.d.ts +8 -2
- package/dist/http/mcp-routes.js +101 -8
- package/dist/http/mcp-session-eviction.d.ts +3 -0
- package/dist/http/mcp-session-eviction.js +24 -0
- 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-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 +5 -114
- package/dist/http/mcp-sessions.d.ts +41 -0
- package/dist/http/mcp-sessions.js +392 -0
- package/dist/http/rate-limit.js +2 -2
- package/dist/http/server-middleware.d.ts +6 -1
- package/dist/http/server-middleware.js +3 -117
- package/dist/http/server-shutdown.js +1 -1
- package/dist/http/server-tuning.d.ts +9 -0
- package/dist/http/server-tuning.js +45 -0
- package/dist/http/server.js +206 -9
- package/dist/http/session-cleanup.js +8 -5
- package/dist/http.d.ts +78 -0
- package/dist/http.js +1437 -0
- package/dist/index.js +3 -3
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.js +94 -0
- package/dist/middleware/error-handler.d.ts +1 -1
- package/dist/middleware/error-handler.js +31 -30
- package/dist/observability.d.ts +16 -0
- package/dist/observability.js +78 -0
- 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 +33 -33
- package/dist/server.js +21 -6
- package/dist/services/cache-events.d.ts +8 -0
- package/dist/services/cache-events.js +19 -0
- package/dist/services/cache.d.ts +5 -4
- package/dist/services/cache.js +49 -45
- package/dist/services/context.d.ts +2 -0
- package/dist/services/context.js +3 -0
- package/dist/services/extractor.d.ts +1 -0
- package/dist/services/extractor.js +77 -40
- package/dist/services/fetcher/agents.js +1 -1
- package/dist/services/fetcher/dns-selection.js +1 -1
- package/dist/services/fetcher/interceptors.js +29 -60
- package/dist/services/fetcher/redirects.js +12 -4
- package/dist/services/fetcher/response.js +18 -8
- package/dist/services/fetcher.d.ts +23 -0
- package/dist/services/fetcher.js +553 -13
- package/dist/services/logger.js +4 -1
- package/dist/services/telemetry.d.ts +19 -0
- package/dist/services/telemetry.js +43 -0
- package/dist/services/transform-worker-pool.d.ts +10 -3
- package/dist/services/transform-worker-pool.js +213 -184
- package/dist/tools/handlers/fetch-single.shared.d.ts +11 -3
- package/dist/tools/handlers/fetch-single.shared.js +131 -2
- package/dist/tools/handlers/fetch-url.tool.d.ts +6 -0
- package/dist/tools/handlers/fetch-url.tool.js +56 -12
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +13 -1
- package/dist/tools/schemas.d.ts +2 -0
- package/dist/tools/schemas.js +8 -0
- package/dist/tools/utils/content-shaping.js +19 -4
- package/dist/tools/utils/content-transform-core.d.ts +5 -0
- package/dist/tools/utils/content-transform-core.js +180 -0
- package/dist/tools/utils/content-transform-workers.d.ts +1 -0
- package/dist/tools/utils/content-transform-workers.js +1 -0
- package/dist/tools/utils/content-transform.d.ts +2 -1
- package/dist/tools/utils/content-transform.js +37 -136
- package/dist/tools/utils/fetch-pipeline.js +47 -56
- package/dist/tools/utils/frontmatter.d.ts +3 -0
- package/dist/tools/utils/frontmatter.js +73 -0
- 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 +149 -0
- package/dist/tools.d.ts +104 -0
- package/dist/tools.js +421 -0
- package/dist/transform.d.ts +69 -0
- package/dist/transform.js +1509 -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 +5 -0
- package/dist/transformers/markdown.js +314 -0
- package/dist/transformers/markdown.transformer.js +2 -189
- package/dist/utils/cancellation.d.ts +1 -0
- package/dist/utils/cancellation.js +18 -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.js +250 -46
- package/dist/utils/error-details.d.ts +3 -0
- package/dist/utils/error-details.js +12 -0
- package/dist/utils/filename-generator.js +14 -3
- package/dist/utils/host-normalizer.d.ts +1 -0
- package/dist/utils/host-normalizer.js +37 -0
- package/dist/utils/ip-address.d.ts +4 -0
- package/dist/utils/ip-address.js +6 -0
- package/dist/utils/tool-error-handler.js +12 -17
- package/dist/utils/url-redactor.d.ts +1 -0
- package/dist/utils/url-redactor.js +13 -0
- package/dist/utils/url-validator.js +35 -20
- package/dist/workers/transform-worker.js +82 -38
- package/package.json +13 -10
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { logWarn } from '../services/logger.js';
|
|
2
|
+
import { getErrorMessage } from '../utils/error-details.js';
|
|
3
|
+
export function evictExpiredSessions(store) {
|
|
4
|
+
const evicted = store.evictExpired();
|
|
5
|
+
for (const session of evicted) {
|
|
6
|
+
void session.transport.close().catch((error) => {
|
|
7
|
+
logWarn('Failed to close expired session', {
|
|
8
|
+
error: getErrorMessage(error),
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
return evicted.length;
|
|
13
|
+
}
|
|
14
|
+
export function evictOldestSession(store) {
|
|
15
|
+
const session = store.evictOldest();
|
|
16
|
+
if (!session)
|
|
17
|
+
return false;
|
|
18
|
+
void session.transport.close().catch((error) => {
|
|
19
|
+
logWarn('Failed to close evicted session', {
|
|
20
|
+
error: getErrorMessage(error),
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Response } from 'express';
|
|
2
|
+
import type { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
3
|
+
import type { McpSessionOptions } from './mcp-session-types.js';
|
|
4
|
+
export declare function createAndConnectTransport({ options, res, }: {
|
|
5
|
+
options: McpSessionOptions;
|
|
6
|
+
res: Response;
|
|
7
|
+
}): Promise<StreamableHTTPServerTransport | null>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { logError, logInfo, logWarn } from '../services/logger.js';
|
|
2
|
+
import { getErrorMessage } from '../utils/error-details.js';
|
|
3
|
+
import { createMcpServer } from '../server.js';
|
|
4
|
+
import { evictOldestSession } from './mcp-session-eviction.js';
|
|
5
|
+
import { createSlotTracker, ensureSessionCapacity, reserveSessionSlot, respondBadRequest, respondServerBusy, } from './mcp-session-slots.js';
|
|
6
|
+
import { createSessionTransport } from './mcp-session-transport-init.js';
|
|
7
|
+
import { createTimeoutController, createTransportAdapter, } from './mcp-session-transport.js';
|
|
8
|
+
async function connectTransportOrThrow({ transport, clearInitTimeout, releaseSlot, }) {
|
|
9
|
+
const mcpServer = createMcpServer();
|
|
10
|
+
const transportAdapter = createTransportAdapter(transport);
|
|
11
|
+
try {
|
|
12
|
+
await mcpServer.connect(transportAdapter);
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
clearInitTimeout();
|
|
16
|
+
releaseSlot();
|
|
17
|
+
void transport.close().catch((closeError) => {
|
|
18
|
+
logWarn('Failed to close transport after connect error', {
|
|
19
|
+
error: getErrorMessage(closeError),
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
logError('Failed to initialize MCP session', error instanceof Error ? error : undefined);
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function createAndConnectTransport({ options, res, }) {
|
|
27
|
+
if (!reserveSessionIfPossible({ options, res }))
|
|
28
|
+
return null;
|
|
29
|
+
const tracker = createSlotTracker();
|
|
30
|
+
const timeoutController = createTimeoutController();
|
|
31
|
+
const transport = createSessionTransport({ tracker, timeoutController });
|
|
32
|
+
await connectTransportOrThrow({
|
|
33
|
+
transport,
|
|
34
|
+
clearInitTimeout: timeoutController.clear,
|
|
35
|
+
releaseSlot: tracker.releaseSlot,
|
|
36
|
+
});
|
|
37
|
+
const sessionId = resolveSessionId({
|
|
38
|
+
transport,
|
|
39
|
+
res,
|
|
40
|
+
tracker,
|
|
41
|
+
clearInitTimeout: timeoutController.clear,
|
|
42
|
+
});
|
|
43
|
+
if (!sessionId)
|
|
44
|
+
return null;
|
|
45
|
+
finalizeSession({
|
|
46
|
+
store: options.sessionStore,
|
|
47
|
+
transport,
|
|
48
|
+
sessionId,
|
|
49
|
+
tracker,
|
|
50
|
+
clearInitTimeout: timeoutController.clear,
|
|
51
|
+
});
|
|
52
|
+
return transport;
|
|
53
|
+
}
|
|
54
|
+
function reserveSessionIfPossible({ options, res, }) {
|
|
55
|
+
if (!ensureSessionCapacity({
|
|
56
|
+
store: options.sessionStore,
|
|
57
|
+
maxSessions: options.maxSessions,
|
|
58
|
+
res,
|
|
59
|
+
evictOldest: evictOldestSession,
|
|
60
|
+
})) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
if (!reserveSessionSlot(options.sessionStore, options.maxSessions)) {
|
|
64
|
+
respondServerBusy(res);
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
function resolveSessionId({ transport, res, tracker, clearInitTimeout, }) {
|
|
70
|
+
const { sessionId } = transport;
|
|
71
|
+
if (typeof sessionId !== 'string') {
|
|
72
|
+
clearInitTimeout();
|
|
73
|
+
tracker.releaseSlot();
|
|
74
|
+
respondBadRequest(res);
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return sessionId;
|
|
78
|
+
}
|
|
79
|
+
function finalizeSession({ store, transport, sessionId, tracker, clearInitTimeout, }) {
|
|
80
|
+
clearInitTimeout();
|
|
81
|
+
tracker.markInitialized();
|
|
82
|
+
tracker.releaseSlot();
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
store.set(sessionId, {
|
|
85
|
+
transport,
|
|
86
|
+
createdAt: now,
|
|
87
|
+
lastSeen: now,
|
|
88
|
+
});
|
|
89
|
+
transport.onclose = () => {
|
|
90
|
+
store.remove(sessionId);
|
|
91
|
+
logInfo('Session closed');
|
|
92
|
+
};
|
|
93
|
+
logInfo('Session initialized');
|
|
94
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Response } from 'express';
|
|
2
|
+
import type { SessionStore } from './sessions.js';
|
|
3
|
+
export interface SlotTracker {
|
|
4
|
+
readonly releaseSlot: () => void;
|
|
5
|
+
readonly markInitialized: () => void;
|
|
6
|
+
readonly isInitialized: () => boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function reserveSessionSlot(store: SessionStore, maxSessions: number): boolean;
|
|
9
|
+
export declare function createSlotTracker(): SlotTracker;
|
|
10
|
+
export declare function ensureSessionCapacity({ store, maxSessions, res, evictOldest, }: {
|
|
11
|
+
store: SessionStore;
|
|
12
|
+
maxSessions: number;
|
|
13
|
+
res: Response;
|
|
14
|
+
evictOldest: (store: SessionStore) => boolean;
|
|
15
|
+
}): boolean;
|
|
16
|
+
export declare function respondServerBusy(res: Response): void;
|
|
17
|
+
export declare function respondBadRequest(res: Response): void;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { sendJsonRpcError } from './jsonrpc-http.js';
|
|
2
|
+
let inFlightSessions = 0;
|
|
3
|
+
export function reserveSessionSlot(store, maxSessions) {
|
|
4
|
+
if (store.size() + inFlightSessions >= maxSessions) {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
inFlightSessions += 1;
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
function releaseSessionSlot() {
|
|
11
|
+
if (inFlightSessions > 0) {
|
|
12
|
+
inFlightSessions -= 1;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function createSlotTracker() {
|
|
16
|
+
let slotReleased = false;
|
|
17
|
+
let initialized = false;
|
|
18
|
+
return {
|
|
19
|
+
releaseSlot: () => {
|
|
20
|
+
if (slotReleased)
|
|
21
|
+
return;
|
|
22
|
+
slotReleased = true;
|
|
23
|
+
releaseSessionSlot();
|
|
24
|
+
},
|
|
25
|
+
markInitialized: () => {
|
|
26
|
+
initialized = true;
|
|
27
|
+
},
|
|
28
|
+
isInitialized: () => initialized,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function ensureSessionCapacity({ store, maxSessions, res, evictOldest, }) {
|
|
32
|
+
if (!isServerAtCapacity(store, maxSessions)) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
if (tryEvictSlot(store, maxSessions, evictOldest)) {
|
|
36
|
+
return !isServerAtCapacity(store, maxSessions);
|
|
37
|
+
}
|
|
38
|
+
respondServerBusy(res);
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
function isServerAtCapacity(store, maxSessions) {
|
|
42
|
+
return store.size() + inFlightSessions >= maxSessions;
|
|
43
|
+
}
|
|
44
|
+
function tryEvictSlot(store, maxSessions, evictOldest) {
|
|
45
|
+
const currentSize = store.size();
|
|
46
|
+
const canFreeSlot = currentSize >= maxSessions &&
|
|
47
|
+
currentSize - 1 + inFlightSessions < maxSessions;
|
|
48
|
+
return canFreeSlot && evictOldest(store);
|
|
49
|
+
}
|
|
50
|
+
export function respondServerBusy(res) {
|
|
51
|
+
sendJsonRpcError(res, -32000, 'Server busy: maximum sessions reached', 503);
|
|
52
|
+
}
|
|
53
|
+
export function respondBadRequest(res) {
|
|
54
|
+
sendJsonRpcError(res, -32000, 'Bad Request: Missing session ID or not an initialize request', 400);
|
|
55
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
2
|
+
import type { SlotTracker } from './mcp-session-slots.js';
|
|
3
|
+
import type { createTimeoutController } from './mcp-session-transport.js';
|
|
4
|
+
export declare function createSessionTransport({ tracker, timeoutController, }: {
|
|
5
|
+
tracker: SlotTracker;
|
|
6
|
+
timeoutController: ReturnType<typeof createTimeoutController>;
|
|
7
|
+
}): StreamableHTTPServerTransport;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
3
|
+
import { config } from '../config/index.js';
|
|
4
|
+
import { logWarn } from '../services/logger.js';
|
|
5
|
+
import { getErrorMessage } from '../utils/error-details.js';
|
|
6
|
+
function startSessionInitTimeout({ transport, tracker, clearInitTimeout, timeoutMs, }) {
|
|
7
|
+
if (timeoutMs <= 0)
|
|
8
|
+
return null;
|
|
9
|
+
const timeout = setTimeout(() => {
|
|
10
|
+
clearInitTimeout();
|
|
11
|
+
if (tracker.isInitialized())
|
|
12
|
+
return;
|
|
13
|
+
tracker.releaseSlot();
|
|
14
|
+
void transport.close().catch((error) => {
|
|
15
|
+
logWarn('Failed to close stalled session', {
|
|
16
|
+
error: getErrorMessage(error),
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
logWarn('Session initialization timed out', { timeoutMs });
|
|
20
|
+
}, timeoutMs);
|
|
21
|
+
timeout.unref();
|
|
22
|
+
return timeout;
|
|
23
|
+
}
|
|
24
|
+
export function createSessionTransport({ tracker, timeoutController, }) {
|
|
25
|
+
const transport = new StreamableHTTPServerTransport({
|
|
26
|
+
sessionIdGenerator: () => randomUUID(),
|
|
27
|
+
});
|
|
28
|
+
transport.onclose = () => {
|
|
29
|
+
timeoutController.clear();
|
|
30
|
+
if (!tracker.isInitialized()) {
|
|
31
|
+
tracker.releaseSlot();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
timeoutController.set(startSessionInitTimeout({
|
|
35
|
+
transport,
|
|
36
|
+
tracker,
|
|
37
|
+
clearInitTimeout: timeoutController.clear,
|
|
38
|
+
timeoutMs: config.server.sessionInitTimeoutMs,
|
|
39
|
+
}));
|
|
40
|
+
return transport;
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
1
|
+
import type { Response } from 'express';
|
|
2
|
+
import type { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
3
3
|
import type { McpRequestBody } from '../config/types/runtime.js';
|
|
4
|
-
import {
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
import type { McpSessionOptions } from './mcp-session-types.js';
|
|
5
|
+
export declare function resolveTransportForPost({ res, body, sessionId, options, }: {
|
|
6
|
+
res: Response;
|
|
7
|
+
body: McpRequestBody;
|
|
8
|
+
sessionId: string | undefined;
|
|
9
|
+
options: McpSessionOptions;
|
|
10
|
+
}): Promise<StreamableHTTPServerTransport | null>;
|
package/dist/http/mcp-session.js
CHANGED
|
@@ -1,96 +1,9 @@
|
|
|
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 { config } from '../config/index.js';
|
|
5
|
-
import { logError, logInfo, logWarn } from '../services/logger.js';
|
|
6
|
-
import { getErrorMessage } from '../utils/error-utils.js';
|
|
7
|
-
import { createMcpServer } from '../server.js';
|
|
8
2
|
import { sendJsonRpcError } from './jsonrpc-http.js';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {} from './
|
|
12
|
-
function
|
|
13
|
-
if (timeoutMs <= 0)
|
|
14
|
-
return null;
|
|
15
|
-
const timeout = setTimeout(() => {
|
|
16
|
-
clearInitTimeout();
|
|
17
|
-
if (tracker.isInitialized())
|
|
18
|
-
return;
|
|
19
|
-
tracker.releaseSlot();
|
|
20
|
-
void transport.close().catch((error) => {
|
|
21
|
-
logWarn('Failed to close stalled session', {
|
|
22
|
-
error: getErrorMessage(error),
|
|
23
|
-
});
|
|
24
|
-
});
|
|
25
|
-
logWarn('Session initialization timed out', { timeoutMs });
|
|
26
|
-
}, timeoutMs);
|
|
27
|
-
timeout.unref();
|
|
28
|
-
return timeout;
|
|
29
|
-
}
|
|
30
|
-
async function connectTransportOrThrow(transport, clearInitTimeout, releaseSlot) {
|
|
31
|
-
const mcpServer = createMcpServer();
|
|
32
|
-
const transportAdapter = createTransportAdapter(transport);
|
|
33
|
-
try {
|
|
34
|
-
await mcpServer.connect(transportAdapter);
|
|
35
|
-
}
|
|
36
|
-
catch (error) {
|
|
37
|
-
clearInitTimeout();
|
|
38
|
-
releaseSlot();
|
|
39
|
-
void transport.close().catch((closeError) => {
|
|
40
|
-
logWarn('Failed to close transport after connect error', {
|
|
41
|
-
error: getErrorMessage(closeError),
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
logError('Failed to initialize MCP session', error instanceof Error ? error : undefined);
|
|
45
|
-
throw error;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
async function createAndConnectTransport(options, res) {
|
|
49
|
-
if (!ensureSessionCapacity(options.sessionStore, options.maxSessions, res, evictOldestSession)) {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
if (!reserveSessionSlot(options.sessionStore, options.maxSessions)) {
|
|
53
|
-
respondServerBusy(res);
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
const tracker = createSlotTracker();
|
|
57
|
-
const timeoutController = createTimeoutController();
|
|
58
|
-
const { clear: clearInitTimeout } = timeoutController;
|
|
59
|
-
const transport = new StreamableHTTPServerTransport({
|
|
60
|
-
sessionIdGenerator: () => randomUUID(),
|
|
61
|
-
});
|
|
62
|
-
transport.onclose = () => {
|
|
63
|
-
clearInitTimeout();
|
|
64
|
-
if (!tracker.isInitialized()) {
|
|
65
|
-
tracker.releaseSlot();
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
timeoutController.set(startSessionInitTimeout(transport, tracker, clearInitTimeout, config.server.sessionInitTimeoutMs));
|
|
69
|
-
await connectTransportOrThrow(transport, clearInitTimeout, tracker.releaseSlot);
|
|
70
|
-
const { sessionId } = transport;
|
|
71
|
-
if (typeof sessionId !== 'string') {
|
|
72
|
-
clearInitTimeout();
|
|
73
|
-
tracker.releaseSlot();
|
|
74
|
-
respondBadRequest(res);
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
clearInitTimeout();
|
|
78
|
-
tracker.markInitialized();
|
|
79
|
-
tracker.releaseSlot();
|
|
80
|
-
const now = Date.now();
|
|
81
|
-
options.sessionStore.set(sessionId, {
|
|
82
|
-
transport,
|
|
83
|
-
createdAt: now,
|
|
84
|
-
lastSeen: now,
|
|
85
|
-
});
|
|
86
|
-
transport.onclose = () => {
|
|
87
|
-
options.sessionStore.remove(sessionId);
|
|
88
|
-
logInfo('Session closed');
|
|
89
|
-
};
|
|
90
|
-
logInfo('Session initialized');
|
|
91
|
-
return transport;
|
|
92
|
-
}
|
|
93
|
-
export async function resolveTransportForPost(_req, res, body, sessionId, options) {
|
|
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, }) {
|
|
94
7
|
if (sessionId) {
|
|
95
8
|
const existingSession = options.sessionStore.get(sessionId);
|
|
96
9
|
if (existingSession) {
|
|
@@ -106,27 +19,5 @@ export async function resolveTransportForPost(_req, res, body, sessionId, option
|
|
|
106
19
|
return null;
|
|
107
20
|
}
|
|
108
21
|
evictExpiredSessions(options.sessionStore);
|
|
109
|
-
return createAndConnectTransport(options, res);
|
|
110
|
-
}
|
|
111
|
-
export function evictExpiredSessions(store) {
|
|
112
|
-
const evicted = store.evictExpired();
|
|
113
|
-
for (const session of evicted) {
|
|
114
|
-
void session.transport.close().catch((error) => {
|
|
115
|
-
logWarn('Failed to close expired session', {
|
|
116
|
-
error: getErrorMessage(error),
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
return evicted.length;
|
|
121
|
-
}
|
|
122
|
-
function evictOldestSession(store) {
|
|
123
|
-
const session = store.evictOldest();
|
|
124
|
-
if (!session)
|
|
125
|
-
return false;
|
|
126
|
-
void session.transport.close().catch((error) => {
|
|
127
|
-
logWarn('Failed to close evicted session', {
|
|
128
|
-
error: getErrorMessage(error),
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
return true;
|
|
22
|
+
return createAndConnectTransport({ options, res });
|
|
132
23
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Request, Response } from 'express';
|
|
2
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
3
|
+
import type { McpRequestBody, 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, id?: string | number | null): 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: Pick<McpRequestBody, 'method' | 'id'>;
|
|
37
|
+
sessionId: string | undefined;
|
|
38
|
+
options: McpSessionOptions;
|
|
39
|
+
}): Promise<StreamableHTTPServerTransport | null>;
|
|
40
|
+
export declare function startSessionCleanupLoop(store: SessionStore, sessionTtlMs: number): AbortController;
|
|
41
|
+
export {};
|