@nitrostack/core 1.0.16 → 1.1.0-beta.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/dist/core/app-decorator.d.ts +5 -0
- package/dist/core/app-decorator.d.ts.map +1 -1
- package/dist/core/app-decorator.js +2 -1
- package/dist/core/app-decorator.js.map +1 -1
- package/dist/core/builders.d.ts.map +1 -1
- package/dist/core/builders.js +2 -0
- package/dist/core/builders.js.map +1 -1
- package/dist/core/decorators.d.ts +11 -0
- package/dist/core/decorators.d.ts.map +1 -1
- package/dist/core/decorators.js.map +1 -1
- package/dist/core/health/health-checks.resource.d.ts +4 -3
- package/dist/core/health/health-checks.resource.d.ts.map +1 -1
- package/dist/core/health/health-checks.resource.js +35 -8
- package/dist/core/health/health-checks.resource.js.map +1 -1
- package/dist/core/health/health.interface.d.ts +40 -0
- package/dist/core/health/health.interface.d.ts.map +1 -0
- package/dist/core/health/health.interface.js +2 -0
- package/dist/core/health/health.interface.js.map +1 -0
- package/dist/core/index.d.ts +11 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +9 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/interceptors/data-spillover.interceptor.d.ts +30 -0
- package/dist/core/interceptors/data-spillover.interceptor.d.ts.map +1 -0
- package/dist/core/interceptors/data-spillover.interceptor.js +174 -0
- package/dist/core/interceptors/data-spillover.interceptor.js.map +1 -0
- package/dist/core/interceptors/interceptor.decorator.d.ts +17 -8
- package/dist/core/interceptors/interceptor.decorator.d.ts.map +1 -1
- package/dist/core/interceptors/interceptor.decorator.js +36 -11
- package/dist/core/interceptors/interceptor.decorator.js.map +1 -1
- package/dist/core/interceptors/interceptor.interface.d.ts +4 -0
- package/dist/core/interceptors/interceptor.interface.d.ts.map +1 -1
- package/dist/core/interceptors/spillover/fs-spillover.store.d.ts +54 -0
- package/dist/core/interceptors/spillover/fs-spillover.store.d.ts.map +1 -0
- package/dist/core/interceptors/spillover/fs-spillover.store.js +302 -0
- package/dist/core/interceptors/spillover/fs-spillover.store.js.map +1 -0
- package/dist/core/interceptors/spillover/index.d.ts +6 -0
- package/dist/core/interceptors/spillover/index.d.ts.map +1 -0
- package/dist/core/interceptors/spillover/index.js +6 -0
- package/dist/core/interceptors/spillover/index.js.map +1 -0
- package/dist/core/interceptors/spillover/memory-spillover.store.d.ts +25 -0
- package/dist/core/interceptors/spillover/memory-spillover.store.d.ts.map +1 -0
- package/dist/core/interceptors/spillover/memory-spillover.store.js +115 -0
- package/dist/core/interceptors/spillover/memory-spillover.store.js.map +1 -0
- package/dist/core/interceptors/spillover/preview-generator.d.ts +7 -0
- package/dist/core/interceptors/spillover/preview-generator.d.ts.map +1 -0
- package/dist/core/interceptors/spillover/preview-generator.js +79 -0
- package/dist/core/interceptors/spillover/preview-generator.js.map +1 -0
- package/dist/core/interceptors/spillover/spillover-store.interface.d.ts +20 -0
- package/dist/core/interceptors/spillover/spillover-store.interface.d.ts.map +1 -0
- package/dist/core/interceptors/spillover/spillover-store.interface.js +2 -0
- package/dist/core/interceptors/spillover/spillover-store.interface.js.map +1 -0
- package/dist/core/interceptors/spillover/types.d.ts +71 -0
- package/dist/core/interceptors/spillover/types.d.ts.map +1 -0
- package/dist/core/interceptors/spillover/types.js +12 -0
- package/dist/core/interceptors/spillover/types.js.map +1 -0
- package/dist/core/protocol/adapter.d.ts +7 -1
- package/dist/core/protocol/adapter.d.ts.map +1 -1
- package/dist/core/protocol/modern-v2.adapter.d.ts +95 -2
- package/dist/core/protocol/modern-v2.adapter.d.ts.map +1 -1
- package/dist/core/protocol/modern-v2.adapter.js +688 -157
- package/dist/core/protocol/modern-v2.adapter.js.map +1 -1
- package/dist/core/server.d.ts +104 -4
- package/dist/core/server.d.ts.map +1 -1
- package/dist/core/server.js +450 -44
- package/dist/core/server.js.map +1 -1
- package/dist/core/task.d.ts.map +1 -1
- package/dist/core/task.js +8 -7
- package/dist/core/task.js.map +1 -1
- package/dist/core/tool.d.ts +12 -3
- package/dist/core/tool.d.ts.map +1 -1
- package/dist/core/tool.js +21 -3
- package/dist/core/tool.js.map +1 -1
- package/dist/core/transforms/catalog.transform.d.ts +40 -0
- package/dist/core/transforms/catalog.transform.d.ts.map +1 -0
- package/dist/core/transforms/catalog.transform.js +52 -0
- package/dist/core/transforms/catalog.transform.js.map +1 -0
- package/dist/core/transforms/code-mode/code-mode.transform.d.ts +75 -0
- package/dist/core/transforms/code-mode/code-mode.transform.d.ts.map +1 -0
- package/dist/core/transforms/code-mode/code-mode.transform.js +226 -0
- package/dist/core/transforms/code-mode/code-mode.transform.js.map +1 -0
- package/dist/core/transforms/code-mode/destructive-guard.d.ts +7 -0
- package/dist/core/transforms/code-mode/destructive-guard.d.ts.map +1 -0
- package/dist/core/transforms/code-mode/destructive-guard.js +18 -0
- package/dist/core/transforms/code-mode/destructive-guard.js.map +1 -0
- package/dist/core/transforms/code-mode/guest-evaluator.d.ts +17 -0
- package/dist/core/transforms/code-mode/guest-evaluator.d.ts.map +1 -0
- package/dist/core/transforms/code-mode/guest-evaluator.js +277 -0
- package/dist/core/transforms/code-mode/guest-evaluator.js.map +1 -0
- package/dist/core/transforms/code-mode/index.d.ts +11 -0
- package/dist/core/transforms/code-mode/index.d.ts.map +1 -0
- package/dist/core/transforms/code-mode/index.js +11 -0
- package/dist/core/transforms/code-mode/index.js.map +1 -0
- package/dist/core/transforms/code-mode/ipc-messages.d.ts +31 -0
- package/dist/core/transforms/code-mode/ipc-messages.d.ts.map +1 -0
- package/dist/core/transforms/code-mode/ipc-messages.js +2 -0
- package/dist/core/transforms/code-mode/ipc-messages.js.map +1 -0
- package/dist/core/transforms/code-mode/legible-error.d.ts +8 -0
- package/dist/core/transforms/code-mode/legible-error.d.ts.map +1 -0
- package/dist/core/transforms/code-mode/legible-error.js +17 -0
- package/dist/core/transforms/code-mode/legible-error.js.map +1 -0
- package/dist/core/transforms/code-mode/quickjs-wasm.provider.d.ts +25 -0
- package/dist/core/transforms/code-mode/quickjs-wasm.provider.d.ts.map +1 -0
- package/dist/core/transforms/code-mode/quickjs-wasm.provider.js +31 -0
- package/dist/core/transforms/code-mode/quickjs-wasm.provider.js.map +1 -0
- package/dist/core/transforms/code-mode/sandbox.interface.d.ts +10 -0
- package/dist/core/transforms/code-mode/sandbox.interface.d.ts.map +1 -0
- package/dist/core/transforms/code-mode/sandbox.interface.js +2 -0
- package/dist/core/transforms/code-mode/sandbox.interface.js.map +1 -0
- package/dist/core/transforms/code-mode/sandbox.worker.d.ts +2 -0
- package/dist/core/transforms/code-mode/sandbox.worker.d.ts.map +1 -0
- package/dist/core/transforms/code-mode/sandbox.worker.js +69 -0
- package/dist/core/transforms/code-mode/sandbox.worker.js.map +1 -0
- package/dist/core/transforms/code-mode/synthetic-tools.d.ts +23 -0
- package/dist/core/transforms/code-mode/synthetic-tools.d.ts.map +1 -0
- package/dist/core/transforms/code-mode/synthetic-tools.js +112 -0
- package/dist/core/transforms/code-mode/synthetic-tools.js.map +1 -0
- package/dist/core/transforms/code-mode/types.d.ts +32 -0
- package/dist/core/transforms/code-mode/types.d.ts.map +1 -0
- package/dist/core/transforms/code-mode/types.js +2 -0
- package/dist/core/transforms/code-mode/types.js.map +1 -0
- package/dist/core/transforms/code-mode/worker-pool.d.ts +71 -0
- package/dist/core/transforms/code-mode/worker-pool.d.ts.map +1 -0
- package/dist/core/transforms/code-mode/worker-pool.js +435 -0
- package/dist/core/transforms/code-mode/worker-pool.js.map +1 -0
- package/dist/core/transforms/index.d.ts +6 -0
- package/dist/core/transforms/index.d.ts.map +1 -0
- package/dist/core/transforms/index.js +6 -0
- package/dist/core/transforms/index.js.map +1 -0
- package/dist/core/transforms/search/base-search.transform.d.ts +59 -0
- package/dist/core/transforms/search/base-search.transform.d.ts.map +1 -0
- package/dist/core/transforms/search/base-search.transform.js +166 -0
- package/dist/core/transforms/search/base-search.transform.js.map +1 -0
- package/dist/core/transforms/search/bm25-search.transform.d.ts +27 -0
- package/dist/core/transforms/search/bm25-search.transform.d.ts.map +1 -0
- package/dist/core/transforms/search/bm25-search.transform.js +37 -0
- package/dist/core/transforms/search/bm25-search.transform.js.map +1 -0
- package/dist/core/transforms/search/bm25.engine.d.ts +46 -0
- package/dist/core/transforms/search/bm25.engine.d.ts.map +1 -0
- package/dist/core/transforms/search/bm25.engine.js +160 -0
- package/dist/core/transforms/search/bm25.engine.js.map +1 -0
- package/dist/core/transforms/search/index.d.ts +9 -0
- package/dist/core/transforms/search/index.d.ts.map +1 -0
- package/dist/core/transforms/search/index.js +9 -0
- package/dist/core/transforms/search/index.js.map +1 -0
- package/dist/core/transforms/search/regex-match.worker.d.ts +2 -0
- package/dist/core/transforms/search/regex-match.worker.d.ts.map +1 -0
- package/dist/core/transforms/search/regex-match.worker.js +19 -0
- package/dist/core/transforms/search/regex-match.worker.js.map +1 -0
- package/dist/core/transforms/search/regex-search.transform.d.ts +41 -0
- package/dist/core/transforms/search/regex-search.transform.d.ts.map +1 -0
- package/dist/core/transforms/search/regex-search.transform.js +237 -0
- package/dist/core/transforms/search/regex-search.transform.js.map +1 -0
- package/dist/core/transforms/search/synthetic-tools.d.ts +11 -0
- package/dist/core/transforms/search/synthetic-tools.d.ts.map +1 -0
- package/dist/core/transforms/search/synthetic-tools.js +76 -0
- package/dist/core/transforms/search/synthetic-tools.js.map +1 -0
- package/dist/core/transforms/search/tokenizer.d.ts +11 -0
- package/dist/core/transforms/search/tokenizer.d.ts.map +1 -0
- package/dist/core/transforms/search/tokenizer.js +38 -0
- package/dist/core/transforms/search/tokenizer.js.map +1 -0
- package/dist/core/transforms/search/tool-serializer.d.ts +6 -0
- package/dist/core/transforms/search/tool-serializer.d.ts.map +1 -0
- package/dist/core/transforms/search/tool-serializer.js +41 -0
- package/dist/core/transforms/search/tool-serializer.js.map +1 -0
- package/dist/core/transforms/search/types.d.ts +36 -0
- package/dist/core/transforms/search/types.d.ts.map +1 -0
- package/dist/core/transforms/search/types.js +2 -0
- package/dist/core/transforms/search/types.js.map +1 -0
- package/dist/core/transforms/tool-cache-key.d.ts +15 -0
- package/dist/core/transforms/tool-cache-key.d.ts.map +1 -0
- package/dist/core/transforms/tool-cache-key.js +61 -0
- package/dist/core/transforms/tool-cache-key.js.map +1 -0
- package/dist/core/transforms/transform.interface.d.ts +34 -0
- package/dist/core/transforms/transform.interface.d.ts.map +1 -0
- package/dist/core/transforms/transform.interface.js +2 -0
- package/dist/core/transforms/transform.interface.js.map +1 -0
- package/dist/core/transforms/validate-tool-arguments.d.ts +10 -0
- package/dist/core/transforms/validate-tool-arguments.d.ts.map +1 -0
- package/dist/core/transforms/validate-tool-arguments.js +62 -0
- package/dist/core/transforms/validate-tool-arguments.js.map +1 -0
- package/dist/core/transforms/visibility/index.d.ts +3 -0
- package/dist/core/transforms/visibility/index.d.ts.map +1 -0
- package/dist/core/transforms/visibility/index.js +3 -0
- package/dist/core/transforms/visibility/index.js.map +1 -0
- package/dist/core/transforms/visibility/session-store.d.ts +132 -0
- package/dist/core/transforms/visibility/session-store.d.ts.map +1 -0
- package/dist/core/transforms/visibility/session-store.js +330 -0
- package/dist/core/transforms/visibility/session-store.js.map +1 -0
- package/dist/core/transforms/visibility/visibility.transform.d.ts +42 -0
- package/dist/core/transforms/visibility/visibility.transform.d.ts.map +1 -0
- package/dist/core/transforms/visibility/visibility.transform.js +109 -0
- package/dist/core/transforms/visibility/visibility.transform.js.map +1 -0
- package/dist/core/types.d.ts +60 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +3 -0
- package/dist/testing/index.js.map +1 -1
- package/package.json +9 -6
- package/dist/core/widget-bundle-paths.d.ts +0 -5
- package/dist/core/widget-bundle-paths.d.ts.map +0 -1
- package/dist/core/widget-bundle-paths.js +0 -25
- package/dist/core/widget-bundle-paths.js.map +0 -1
- package/dist/core/widget-html-mime.d.ts +0 -7
- package/dist/core/widget-html-mime.d.ts.map +0 -1
- package/dist/core/widget-html-mime.js +0 -7
- package/dist/core/widget-html-mime.js.map +0 -1
- package/dist/core/widget-mime.d.ts +0 -29
- package/dist/core/widget-mime.d.ts.map +0 -1
- package/dist/core/widget-mime.js +0 -41
- package/dist/core/widget-mime.js.map +0 -1
|
@@ -30,6 +30,12 @@ import { mapToJsonRpcError } from './features/errors.js';
|
|
|
30
30
|
import { isInputRequired } from './features/mrtr.js';
|
|
31
31
|
import { isMcpAppMode, isOpenAiMode } from '../app-mode.js';
|
|
32
32
|
import { TaskContext, TaskAugmentationRequiredError } from '../task.js';
|
|
33
|
+
function sessionRequiredError() {
|
|
34
|
+
const error = new Error('Session required');
|
|
35
|
+
error.name = 'SessionRequiredError';
|
|
36
|
+
error.code = -32600;
|
|
37
|
+
return error;
|
|
38
|
+
}
|
|
33
39
|
/** Meta-key constants (SEP-2575 / SEP-414) used to read the request envelope. */
|
|
34
40
|
const META = {
|
|
35
41
|
PROTOCOL_VERSION: 'io.modelcontextprotocol/protocolVersion',
|
|
@@ -42,8 +48,23 @@ export class ModernProtocolAdapter {
|
|
|
42
48
|
era = 'modern';
|
|
43
49
|
handler;
|
|
44
50
|
stdioHandle;
|
|
51
|
+
/** Process-local session for the single stdio peer. Not taken from a client header. */
|
|
52
|
+
stdioSessionId;
|
|
53
|
+
/** Tool registrations on the pinned stdio server, kept to resync after visibility changes. */
|
|
54
|
+
stdioCatalog;
|
|
55
|
+
stdioResync = Promise.resolve();
|
|
45
56
|
serverSdkPromise;
|
|
46
57
|
taskManager;
|
|
58
|
+
/**
|
|
59
|
+
* HTTP session ids minted by this process. Client-supplied ids are not members.
|
|
60
|
+
* The first verified subject to use an id owns it.
|
|
61
|
+
*/
|
|
62
|
+
issuedSessions = new Map();
|
|
63
|
+
/** Method the HTTP gate already classified for this request. The SDK consumes the body before the factory runs. */
|
|
64
|
+
gatedMethods = new WeakMap();
|
|
65
|
+
static ISSUED_SESSION_TTL_MS = 30 * 60 * 1000;
|
|
66
|
+
static MAX_ISSUED_SESSIONS = 1000;
|
|
67
|
+
lastSessionCapWarning = 0;
|
|
47
68
|
constructor(registry, options = { legacyMode: 'reject' }) {
|
|
48
69
|
this.registry = registry;
|
|
49
70
|
this.options = options;
|
|
@@ -58,91 +79,338 @@ export class ModernProtocolAdapter {
|
|
|
58
79
|
// ==========================================================================
|
|
59
80
|
// Server construction (called per request by the SDK factory)
|
|
60
81
|
// ==========================================================================
|
|
61
|
-
async buildServer() {
|
|
82
|
+
async buildServer(factoryCtx, source = 'http') {
|
|
62
83
|
const sdk = await this.loadServerSdk();
|
|
63
84
|
const config = this.registry.config;
|
|
64
85
|
const extensions = this.advertisedExtensions();
|
|
65
86
|
const serverOptions = {
|
|
66
87
|
cacheHints: this.buildServerCacheHints(),
|
|
67
88
|
};
|
|
89
|
+
// The SDK installs tools/list only after a tool is registered or when the
|
|
90
|
+
// tools capability is set. A session that has hidden every tool must still
|
|
91
|
+
// answer tools/list with an empty catalog.
|
|
92
|
+
const capabilities = {};
|
|
68
93
|
if (Object.keys(extensions).length > 0) {
|
|
69
|
-
|
|
70
|
-
|
|
94
|
+
capabilities.extensions = extensions;
|
|
95
|
+
}
|
|
96
|
+
if (this.registry.getTools().size > 0) {
|
|
97
|
+
capabilities.tools = {};
|
|
98
|
+
}
|
|
99
|
+
if (Object.keys(capabilities).length > 0) {
|
|
100
|
+
serverOptions.capabilities = capabilities;
|
|
71
101
|
}
|
|
72
102
|
const server = new sdk.McpServer({ name: config.name, version: config.version }, serverOptions);
|
|
73
|
-
|
|
74
|
-
|
|
103
|
+
// The SDK calls this factory once per HTTP request, with the request on
|
|
104
|
+
// `requestInfo`. Catalog shaping (session visibility) and spillover reads
|
|
105
|
+
// both need that session; a context-free list is the stateless catalog.
|
|
106
|
+
// `source` selects the stdio peer id. Hardcoding `http` made stdio list
|
|
107
|
+
// with no session and threw Session required when visibility was on.
|
|
108
|
+
const requestContext = await this.contextFromFactory(factoryCtx, source);
|
|
109
|
+
const handles = await this.registerTools(server, sdk, requestContext);
|
|
110
|
+
if (source === 'stdio') {
|
|
111
|
+
this.stdioCatalog = { server, sdk, handles };
|
|
112
|
+
}
|
|
113
|
+
await this.registerResources(server, sdk, requestContext);
|
|
75
114
|
await this.registerPrompts(server, sdk);
|
|
76
115
|
return server;
|
|
77
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Session and auth from the per-request factory context.
|
|
119
|
+
* `createMcpHandler` supplies `requestInfo` (the HTTP request) and optional `authInfo`.
|
|
120
|
+
*/
|
|
121
|
+
async contextFromFactory(factoryCtx, source = 'http') {
|
|
122
|
+
const request = factoryCtx?.requestInfo;
|
|
123
|
+
const headerSession = request?.headers?.get?.('mcp-session-id') ||
|
|
124
|
+
request?.headers?.get?.('Mcp-Session-Id') ||
|
|
125
|
+
undefined;
|
|
126
|
+
const headerMethod = request?.headers?.get?.('mcp-method') || request?.headers?.get?.('Mcp-Method') || undefined;
|
|
127
|
+
// The legacy fallback builds the server from a cloned request, so the
|
|
128
|
+
// gate's WeakMap entry (on the original) is missing. The clone's body is
|
|
129
|
+
// still readable here; the transport reads it afterwards.
|
|
130
|
+
let method = (request ? this.gatedMethods.get(request) : undefined) ?? headerMethod;
|
|
131
|
+
if (!method && source === 'http' && request?.clone) {
|
|
132
|
+
try {
|
|
133
|
+
const body = (await request.clone().json());
|
|
134
|
+
method = typeof body?.method === 'string' ? body.method : undefined;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
method = undefined;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// initialize, ping, and server/discover run before a session exists.
|
|
141
|
+
// A client-supplied id on those methods is not an isolation key.
|
|
142
|
+
// Stdio ignores a caller-supplied Mcp-Session-Id. That header is the HTTP
|
|
143
|
+
// isolation key, and accepting it here would let the local peer read it.
|
|
144
|
+
const sessionId = source === 'stdio'
|
|
145
|
+
? this.stdioSessionId
|
|
146
|
+
: this.isSessionFreeMethod(method)
|
|
147
|
+
? undefined
|
|
148
|
+
: this.httpSessionOrThrow(headerSession);
|
|
149
|
+
if (source !== 'stdio') {
|
|
150
|
+
this.bindIssuedSubject(sessionId, factoryCtx?.authInfo);
|
|
151
|
+
}
|
|
152
|
+
if (!factoryCtx && !sessionId)
|
|
153
|
+
return undefined;
|
|
154
|
+
return this.executionContextFromRequest({
|
|
155
|
+
sessionId,
|
|
156
|
+
authInfo: factoryCtx?.authInfo,
|
|
157
|
+
protocolVersion: MODERN_PROTOCOL_VERSION,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
visibilityRequiresSession() {
|
|
161
|
+
return this.registry.hasSessionVisibility();
|
|
162
|
+
}
|
|
163
|
+
/** Methods that proceed without a server-issued session. */
|
|
164
|
+
isSessionFreeMethod(method) {
|
|
165
|
+
return method === 'initialize' || method === 'ping' || method === 'server/discover';
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Remember a session id this process minted.
|
|
169
|
+
* Tests use this to present the same id a successful `initialize` would return.
|
|
170
|
+
* An id the client invented is not accepted by {@link httpSessionOrThrow}.
|
|
171
|
+
*/
|
|
172
|
+
issueSession(id = crypto.randomUUID()) {
|
|
173
|
+
this.sweepIssuedSessions();
|
|
174
|
+
if (!this.issuedSessions.has(id) && this.issuedSessions.size >= ModernProtocolAdapter.MAX_ISSUED_SESSIONS) {
|
|
175
|
+
// Refusing here would let anonymous `initialize` calls lock out every new client.
|
|
176
|
+
this.evictIssuedSession();
|
|
177
|
+
}
|
|
178
|
+
const now = Date.now();
|
|
179
|
+
const existing = this.issuedSessions.get(id);
|
|
180
|
+
this.issuedSessions.set(id, {
|
|
181
|
+
createdAt: existing?.createdAt ?? now,
|
|
182
|
+
lastActive: now,
|
|
183
|
+
});
|
|
184
|
+
return id;
|
|
185
|
+
}
|
|
186
|
+
/** True when `id` is unexpired and was minted here. Refreshes its idle timer. */
|
|
187
|
+
touchIssuedSession(id) {
|
|
188
|
+
const entry = this.issuedSessions.get(id);
|
|
189
|
+
if (!entry)
|
|
190
|
+
return false;
|
|
191
|
+
if (Date.now() - entry.lastActive > ModernProtocolAdapter.ISSUED_SESSION_TTL_MS) {
|
|
192
|
+
this.issuedSessions.delete(id);
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
entry.lastActive = Date.now();
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
/** Drops the least recently active unclaimed session, or the least recently active one if all are claimed. */
|
|
199
|
+
evictIssuedSession() {
|
|
200
|
+
let oldestUnclaimed;
|
|
201
|
+
let oldest;
|
|
202
|
+
for (const [id, entry] of this.issuedSessions) {
|
|
203
|
+
if (!oldest || entry.lastActive < oldest[1])
|
|
204
|
+
oldest = [id, entry.lastActive];
|
|
205
|
+
if (!entry.subject && (!oldestUnclaimed || entry.lastActive < oldestUnclaimed[1])) {
|
|
206
|
+
oldestUnclaimed = [id, entry.lastActive];
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const victim = (oldestUnclaimed ?? oldest)?.[0];
|
|
210
|
+
if (victim === undefined)
|
|
211
|
+
return;
|
|
212
|
+
this.issuedSessions.delete(victim);
|
|
213
|
+
const now = Date.now();
|
|
214
|
+
if (now - this.lastSessionCapWarning > 60_000) {
|
|
215
|
+
this.lastSessionCapWarning = now;
|
|
216
|
+
this.registry.logger.warn('Issued session cap reached; evicting idle sessions', {
|
|
217
|
+
cap: ModernProtocolAdapter.MAX_ISSUED_SESSIONS,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
sweepIssuedSessions() {
|
|
222
|
+
const now = Date.now();
|
|
223
|
+
for (const [id, entry] of this.issuedSessions) {
|
|
224
|
+
if (now - entry.lastActive > ModernProtocolAdapter.ISSUED_SESSION_TTL_MS) {
|
|
225
|
+
this.issuedSessions.delete(id);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* When visibility is installed, an HTTP session id must be one `issueSession` recorded.
|
|
231
|
+
* A missing or unknown id throws. Stdio does not call this.
|
|
232
|
+
*/
|
|
233
|
+
httpSessionOrThrow(sessionId) {
|
|
234
|
+
if (!this.visibilityRequiresSession())
|
|
235
|
+
return sessionId;
|
|
236
|
+
if (!sessionId || !this.touchIssuedSession(sessionId)) {
|
|
237
|
+
throw sessionRequiredError();
|
|
238
|
+
}
|
|
239
|
+
return sessionId;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* The first verified subject to present a minted id owns it.
|
|
243
|
+
* A later call with a different subject, or with no subject, is rejected.
|
|
244
|
+
* An id that has only been used anonymously stays anonymous until a subject claims it.
|
|
245
|
+
*/
|
|
246
|
+
bindIssuedSubject(sessionId, authInfo) {
|
|
247
|
+
if (!this.visibilityRequiresSession() || !sessionId)
|
|
248
|
+
return;
|
|
249
|
+
const entry = this.issuedSessions.get(sessionId);
|
|
250
|
+
if (!entry)
|
|
251
|
+
return;
|
|
252
|
+
const subject = this.verifiedSubject(authInfo);
|
|
253
|
+
if (entry.subject && entry.subject !== subject) {
|
|
254
|
+
throw sessionRequiredError();
|
|
255
|
+
}
|
|
256
|
+
if (!entry.subject && subject) {
|
|
257
|
+
entry.subject = subject;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
verifiedSubject(authInfo) {
|
|
261
|
+
if (!authInfo)
|
|
262
|
+
return undefined;
|
|
263
|
+
const subject = this.mapAuthInfo(authInfo)?.subject;
|
|
264
|
+
return typeof subject === 'string' && subject.length > 0 ? subject : undefined;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* One context for tools/list, tools/call, and resources/read.
|
|
268
|
+
* `authInfo` is the verified SDK principal. Metadata may still carry a raw
|
|
269
|
+
* bearer token; `createExecutionContext` does not use that token as the
|
|
270
|
+
* isolation subject.
|
|
271
|
+
*/
|
|
272
|
+
executionContextFromRequest(input) {
|
|
273
|
+
return this.registry.createExecutionContext({
|
|
274
|
+
toolName: input.toolName,
|
|
275
|
+
metadata: input.metadata,
|
|
276
|
+
extra: {
|
|
277
|
+
sessionId: input.sessionId,
|
|
278
|
+
protocolVersion: input.protocolVersion ?? MODERN_PROTOCOL_VERSION,
|
|
279
|
+
clientInfo: input.clientInfo,
|
|
280
|
+
clientCapabilities: input.clientCapabilities,
|
|
281
|
+
requestState: input.requestState,
|
|
282
|
+
inputResponses: input.inputResponses,
|
|
283
|
+
trace: input.trace,
|
|
284
|
+
auth: input.authInfo ? this.mapAuthInfo(input.authInfo) : undefined,
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
}
|
|
78
288
|
/** Server-level per-operation cache hints for list/discover results. */
|
|
79
289
|
buildServerCacheHints() {
|
|
80
290
|
// Conservative shared default for list surfaces; per-resource hints still win.
|
|
81
291
|
return undefined;
|
|
82
292
|
}
|
|
83
|
-
async registerTools(server, sdk) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (componentMeta['openai/widgetCSP'] !== undefined) {
|
|
112
|
-
meta['openai/widgetCSP'] = componentMeta['openai/widgetCSP'];
|
|
113
|
-
}
|
|
114
|
-
if (componentMeta['openai/widgetDescription'] !== undefined) {
|
|
115
|
-
meta['openai/widgetDescription'] = componentMeta['openai/widgetDescription'];
|
|
116
|
-
}
|
|
117
|
-
if (componentMeta['openai/widgetPrefersBorder'] !== undefined) {
|
|
118
|
-
meta['openai/widgetPrefersBorder'] = componentMeta['openai/widgetPrefersBorder'];
|
|
119
|
-
}
|
|
120
|
-
if (componentMeta['openai/widgetDomain'] !== undefined) {
|
|
121
|
-
meta['openai/widgetDomain'] = componentMeta['openai/widgetDomain'];
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
else if (tool.widget?.route || tool.outputTemplate) {
|
|
126
|
-
const route = tool.widget?.route || tool.outputTemplate;
|
|
127
|
-
const normalized = route?.startsWith('/') ? route : `/${route}`;
|
|
128
|
-
const resourceUri = `/widgets${normalized}`;
|
|
129
|
-
meta['ui/template'] = resourceUri;
|
|
130
|
-
meta['openai/outputTemplate'] = resourceUri;
|
|
131
|
-
meta['ui'] = { resourceUri };
|
|
132
|
-
}
|
|
133
|
-
if (tool.examples) {
|
|
134
|
-
meta['tool/examples'] = tool.examples;
|
|
293
|
+
async registerTools(server, sdk, requestContext) {
|
|
294
|
+
const tools = await this.registry.getTransformedTools(requestContext);
|
|
295
|
+
const handles = new Map();
|
|
296
|
+
for (const tool of tools.values()) {
|
|
297
|
+
handles.set(tool.name, await this.registerOneTool(server, sdk, tool));
|
|
298
|
+
}
|
|
299
|
+
return handles;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* The stdio SDK pins one server per connection, so its catalog is built once.
|
|
303
|
+
* Re-run the pipeline for the peer session and add or remove registrations to
|
|
304
|
+
* match. Each change makes the SDK send `notifications/tools/list_changed`.
|
|
305
|
+
*/
|
|
306
|
+
resyncStdioTools() {
|
|
307
|
+
const catalog = this.stdioCatalog;
|
|
308
|
+
if (!catalog)
|
|
309
|
+
return;
|
|
310
|
+
this.stdioResync = this.stdioResync
|
|
311
|
+
.then(async () => {
|
|
312
|
+
if (this.stdioCatalog !== catalog)
|
|
313
|
+
return;
|
|
314
|
+
const context = this.executionContextFromRequest({ sessionId: this.stdioSessionId });
|
|
315
|
+
const next = await this.registry.getTransformedTools(context);
|
|
316
|
+
for (const [name, handle] of [...catalog.handles]) {
|
|
317
|
+
if (next.has(name))
|
|
318
|
+
continue;
|
|
319
|
+
handle.remove();
|
|
320
|
+
catalog.handles.delete(name);
|
|
135
321
|
}
|
|
136
|
-
|
|
137
|
-
|
|
322
|
+
for (const tool of next.values()) {
|
|
323
|
+
if (catalog.handles.has(tool.name))
|
|
324
|
+
continue;
|
|
325
|
+
catalog.handles.set(tool.name, await this.registerOneTool(catalog.server, catalog.sdk, tool));
|
|
138
326
|
}
|
|
139
|
-
|
|
140
|
-
|
|
327
|
+
})
|
|
328
|
+
.catch((error) => {
|
|
329
|
+
this.registry.logger.warn('Failed to refresh the stdio tool catalog', {
|
|
330
|
+
error: error instanceof Error ? error.message : String(error),
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
async registerOneTool(server, sdk, tool) {
|
|
335
|
+
const inputSchema = await this.toModernSchema(tool.inputSchema, 'input', sdk);
|
|
336
|
+
const outputSchema = tool.outputSchema
|
|
337
|
+
? await this.toModernSchema(tool.outputSchema, 'output', sdk)
|
|
338
|
+
: undefined;
|
|
339
|
+
const config = {
|
|
340
|
+
description: tool.description,
|
|
341
|
+
inputSchema,
|
|
342
|
+
};
|
|
343
|
+
if (tool.title)
|
|
344
|
+
config.title = tool.title;
|
|
345
|
+
if (outputSchema)
|
|
346
|
+
config.outputSchema = outputSchema;
|
|
347
|
+
if (tool.annotations)
|
|
348
|
+
config.annotations = tool.annotations;
|
|
349
|
+
const meta = {};
|
|
350
|
+
const cacheHint = resolveToolCacheHint(tool);
|
|
351
|
+
if (cacheHint)
|
|
352
|
+
meta['io.modelcontextprotocol/cacheHint'] = cacheHint;
|
|
353
|
+
if (tool.hasComponent && tool.hasComponent()) {
|
|
354
|
+
const component = tool.getComponent();
|
|
355
|
+
const resourceUri = component.getResourceUri();
|
|
356
|
+
const componentMeta = component.getResourceMetadata();
|
|
357
|
+
meta['ui/template'] = resourceUri;
|
|
358
|
+
meta['openai/outputTemplate'] = resourceUri;
|
|
359
|
+
meta['ui'] = { resourceUri };
|
|
360
|
+
if (componentMeta) {
|
|
361
|
+
if (componentMeta['openai/widgetCSP'] !== undefined) {
|
|
362
|
+
meta['openai/widgetCSP'] = componentMeta['openai/widgetCSP'];
|
|
363
|
+
}
|
|
364
|
+
if (componentMeta['openai/widgetDescription'] !== undefined) {
|
|
365
|
+
meta['openai/widgetDescription'] = componentMeta['openai/widgetDescription'];
|
|
366
|
+
}
|
|
367
|
+
if (componentMeta['openai/widgetPrefersBorder'] !== undefined) {
|
|
368
|
+
meta['openai/widgetPrefersBorder'] = componentMeta['openai/widgetPrefersBorder'];
|
|
369
|
+
}
|
|
370
|
+
if (componentMeta['openai/widgetDomain'] !== undefined) {
|
|
371
|
+
meta['openai/widgetDomain'] = componentMeta['openai/widgetDomain'];
|
|
372
|
+
}
|
|
141
373
|
}
|
|
142
|
-
server.registerTool(tool.name, config, async (args, ctx) => this.runTool(tool, args, ctx, sdk));
|
|
143
374
|
}
|
|
375
|
+
else if (tool.widget?.route || tool.outputTemplate) {
|
|
376
|
+
const route = tool.widget?.route || tool.outputTemplate;
|
|
377
|
+
const normalized = route?.startsWith('/') ? route : `/${route}`;
|
|
378
|
+
const resourceUri = `/widgets${normalized}`;
|
|
379
|
+
meta['ui/template'] = resourceUri;
|
|
380
|
+
meta['openai/outputTemplate'] = resourceUri;
|
|
381
|
+
meta['ui'] = { resourceUri };
|
|
382
|
+
}
|
|
383
|
+
if (tool.examples) {
|
|
384
|
+
meta['tool/examples'] = tool.examples;
|
|
385
|
+
}
|
|
386
|
+
if (tool.isInitial) {
|
|
387
|
+
meta['tool/initial'] = true;
|
|
388
|
+
}
|
|
389
|
+
if (Object.keys(meta).length > 0) {
|
|
390
|
+
config._meta = meta;
|
|
391
|
+
}
|
|
392
|
+
return server.registerTool(tool.name, config, async (args, ctx) => {
|
|
393
|
+
// Build the context first so resolution is session-aware: authorization
|
|
394
|
+
// transforms (session visibility) need the sessionId to decide.
|
|
395
|
+
const context = this.buildContext(ctx, { toolName: tool.name });
|
|
396
|
+
const resolved = await this.registry.resolveTool(tool.name, context);
|
|
397
|
+
if (!resolved) {
|
|
398
|
+
const Missing = sdk.MethodNotFoundError;
|
|
399
|
+
if (Missing) {
|
|
400
|
+
throw new Missing(`Tool '${tool.name}' not found`);
|
|
401
|
+
}
|
|
402
|
+
const missing = new Error(`Tool '${tool.name}' not found`);
|
|
403
|
+
missing.code = -32601;
|
|
404
|
+
throw missing;
|
|
405
|
+
}
|
|
406
|
+
const result = await this.runTool(resolved, args, ctx, sdk, context);
|
|
407
|
+
// A visibility change made by this call must be listed before its result arrives.
|
|
408
|
+
if (this.stdioCatalog?.server === server)
|
|
409
|
+
await this.stdioResync;
|
|
410
|
+
return result;
|
|
411
|
+
});
|
|
144
412
|
}
|
|
145
|
-
async registerResources(server, sdk) {
|
|
413
|
+
async registerResources(server, sdk, requestContext) {
|
|
146
414
|
// Static resources and template resources both flow through registerResource.
|
|
147
415
|
const templateResources = this.registry.getTemplateResources();
|
|
148
416
|
for (const resource of this.registry.getResources().values()) {
|
|
@@ -159,7 +427,7 @@ export class ModernProtocolAdapter {
|
|
|
159
427
|
config.title = resource.title;
|
|
160
428
|
if (cacheHint)
|
|
161
429
|
config.cacheHint = cacheHint;
|
|
162
|
-
server.registerResource(resource.name, resource.uri, config, async (uri) => this.readResource(String(uri?.href ?? uri), resource, sdk));
|
|
430
|
+
server.registerResource(resource.name, resource.uri, config, async (uri) => this.readResource(String(uri?.href ?? uri), resource, sdk, requestContext));
|
|
163
431
|
}
|
|
164
432
|
// Template resources keyed by uri template.
|
|
165
433
|
for (const [uriTemplate, resource] of templateResources.entries()) {
|
|
@@ -177,7 +445,7 @@ export class ModernProtocolAdapter {
|
|
|
177
445
|
const template = ResourceTemplateCtor
|
|
178
446
|
? new ResourceTemplateCtor(uriTemplate, { list: undefined })
|
|
179
447
|
: uriTemplate;
|
|
180
|
-
server.registerResource(resource.name, template, config, async (uri) => this.readResource(String(uri?.href ?? uri), resource, sdk));
|
|
448
|
+
server.registerResource(resource.name, template, config, async (uri) => this.readResource(String(uri?.href ?? uri), resource, sdk, requestContext));
|
|
181
449
|
}
|
|
182
450
|
catch (err) {
|
|
183
451
|
this.registry.logger.warn('Failed to register modern resource template', {
|
|
@@ -194,10 +462,12 @@ export class ModernProtocolAdapter {
|
|
|
194
462
|
if ((rawResources.size > 0 || templateResources.size > 0) && server.server && typeof server.server.setRequestHandler === 'function') {
|
|
195
463
|
server.server.setRequestHandler('resources/read', async (request, ctx) => {
|
|
196
464
|
const reqUri = String(request?.params?.uri ?? '');
|
|
465
|
+
const fromCall = ctx ? this.buildContext(ctx, {}) : undefined;
|
|
466
|
+
const readContext = fromCall?.sessionId ? fromCall : requestContext;
|
|
197
467
|
// 1. Check exact match in registered resources (including path-based URIs like /widgets/...)
|
|
198
468
|
const matchingResource = rawResources.get(reqUri);
|
|
199
469
|
if (matchingResource) {
|
|
200
|
-
const resResult = await this.readResource(reqUri, matchingResource, sdk);
|
|
470
|
+
const resResult = await this.readResource(reqUri, matchingResource, sdk, readContext);
|
|
201
471
|
const cacheHint = resolveResourceCacheHint(matchingResource);
|
|
202
472
|
if (cacheHint) {
|
|
203
473
|
return { ...resResult, cacheHint };
|
|
@@ -211,10 +481,9 @@ export class ModernProtocolAdapter {
|
|
|
211
481
|
}
|
|
212
482
|
catch {
|
|
213
483
|
// If not parseable as standard URL, check if any resource matches
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
484
|
+
const exact = rawResources.get(reqUri);
|
|
485
|
+
if (exact) {
|
|
486
|
+
return this.readResource(reqUri, exact, sdk, readContext);
|
|
218
487
|
}
|
|
219
488
|
}
|
|
220
489
|
if (parsedUrl) {
|
|
@@ -224,7 +493,7 @@ export class ModernProtocolAdapter {
|
|
|
224
493
|
if (typeof registered.readCallback === 'function') {
|
|
225
494
|
return registered.readCallback(parsedUrl, ctx);
|
|
226
495
|
}
|
|
227
|
-
return this.readResource(reqUri, registered, sdk);
|
|
496
|
+
return this.readResource(reqUri, registered, sdk, readContext);
|
|
228
497
|
}
|
|
229
498
|
}
|
|
230
499
|
// 3. Check template resources
|
|
@@ -296,8 +565,8 @@ export class ModernProtocolAdapter {
|
|
|
296
565
|
// ==========================================================================
|
|
297
566
|
// Handlers
|
|
298
567
|
// ==========================================================================
|
|
299
|
-
async runTool(tool, args, ctx, sdk) {
|
|
300
|
-
const context = this.buildContext(ctx, { toolName: tool.name });
|
|
568
|
+
async runTool(tool, args, ctx, sdk, prebuiltContext) {
|
|
569
|
+
const context = prebuiltContext ?? this.buildContext(ctx, { toolName: tool.name });
|
|
301
570
|
const isTaskAugmented = ctx?.task !== undefined || ctx?.mcpReq?.params?.task !== undefined;
|
|
302
571
|
// Enforce tool-level task support negotiation
|
|
303
572
|
if (tool.taskSupport === 'required' && !isTaskAugmented) {
|
|
@@ -401,10 +670,14 @@ export class ModernProtocolAdapter {
|
|
|
401
670
|
err.data = mapped.data;
|
|
402
671
|
return err;
|
|
403
672
|
}
|
|
404
|
-
async readResource(uri, resource, sdk) {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
673
|
+
async readResource(uri, resource, sdk, requestContext) {
|
|
674
|
+
// requestContext already passed through createExecutionContext once.
|
|
675
|
+
// Feeding its sessionId back in as extra.sessionId runs sessionIsolationKey
|
|
676
|
+
// a second time and the spillover read no longer matches the write.
|
|
677
|
+
const context = requestContext ??
|
|
678
|
+
this.registry.createExecutionContext({
|
|
679
|
+
extra: { protocolVersion: MODERN_PROTOCOL_VERSION },
|
|
680
|
+
});
|
|
408
681
|
let content;
|
|
409
682
|
try {
|
|
410
683
|
content = await resource.fetch(context, uri);
|
|
@@ -455,11 +728,9 @@ export class ModernProtocolAdapter {
|
|
|
455
728
|
}
|
|
456
729
|
const trace = extractTraceContext({ ...meta, ...envelope });
|
|
457
730
|
const inputResponses = mcpReq.inputResponses;
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
ctx?.authInfo ??
|
|
462
|
-
ctx?.auth;
|
|
731
|
+
// Envelope and `_meta` are the client request. A subject there must not
|
|
732
|
+
// become the isolation principal. Only auth the host or SDK attached.
|
|
733
|
+
const authInfo = this.trustedAuthInfo(ctx);
|
|
463
734
|
const rawHeaders = {};
|
|
464
735
|
const reqHeaders = ctx?.http?.req?.headers ||
|
|
465
736
|
ctx?.http?.headers ||
|
|
@@ -520,18 +791,33 @@ export class ModernProtocolAdapter {
|
|
|
520
791
|
metadata._oauth = rawToken;
|
|
521
792
|
metadata.jwtToken = rawToken;
|
|
522
793
|
}
|
|
523
|
-
|
|
794
|
+
// Header only. requestState is client-echoed MRTR state and must not select a session.
|
|
795
|
+
let sessionId = rawHeaders['mcp-session-id'] ||
|
|
796
|
+
rawHeaders['Mcp-Session-Id'] ||
|
|
797
|
+
ctx?.request?.headers?.get?.('mcp-session-id') ||
|
|
798
|
+
ctx?.req?.headers?.get?.('mcp-session-id') ||
|
|
799
|
+
undefined;
|
|
800
|
+
const hasHttpRequest = Boolean(reqHeaders || ctx?.http || ctx?.request || ctx?.req);
|
|
801
|
+
const usingStdioPeer = !hasHttpRequest && Boolean(this.stdioSessionId);
|
|
802
|
+
if (usingStdioPeer) {
|
|
803
|
+
// The stdio peer has one minted id. A header here is not a second session.
|
|
804
|
+
sessionId = this.stdioSessionId;
|
|
805
|
+
}
|
|
806
|
+
else {
|
|
807
|
+
sessionId = this.httpSessionOrThrow(sessionId);
|
|
808
|
+
this.bindIssuedSubject(sessionId, authInfo);
|
|
809
|
+
}
|
|
810
|
+
return this.executionContextFromRequest({
|
|
524
811
|
toolName: opts.toolName,
|
|
525
812
|
metadata,
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
},
|
|
813
|
+
sessionId,
|
|
814
|
+
authInfo,
|
|
815
|
+
protocolVersion,
|
|
816
|
+
clientInfo,
|
|
817
|
+
clientCapabilities,
|
|
818
|
+
requestState,
|
|
819
|
+
inputResponses,
|
|
820
|
+
trace,
|
|
535
821
|
});
|
|
536
822
|
}
|
|
537
823
|
mapAuthInfo(authInfo) {
|
|
@@ -581,7 +867,7 @@ export class ModernProtocolAdapter {
|
|
|
581
867
|
async getHttpHandler() {
|
|
582
868
|
if (!this.handler) {
|
|
583
869
|
const sdk = await this.loadServerSdk();
|
|
584
|
-
const rawHandler = sdk.createMcpHandler(() => this.buildServer(), {
|
|
870
|
+
const rawHandler = sdk.createMcpHandler((ctx) => this.buildServer(ctx, 'http'), {
|
|
585
871
|
legacy: this.options.legacyMode,
|
|
586
872
|
onerror: (error) => {
|
|
587
873
|
this.registry.logger.error('Modern MCP handler error', { error: error.message });
|
|
@@ -591,6 +877,11 @@ export class ModernProtocolAdapter {
|
|
|
591
877
|
this.handler = {
|
|
592
878
|
...rawHandler,
|
|
593
879
|
fetch: async (request, requestOptions) => {
|
|
880
|
+
if (this.visibilityRequiresSession()) {
|
|
881
|
+
const blocked = await this.visibilityHttpGate(request);
|
|
882
|
+
if (blocked)
|
|
883
|
+
return blocked;
|
|
884
|
+
}
|
|
594
885
|
if (request.headers.get('mcp-method') === 'ping') {
|
|
595
886
|
try {
|
|
596
887
|
const clone = request.clone();
|
|
@@ -624,23 +915,209 @@ export class ModernProtocolAdapter {
|
|
|
624
915
|
// Not JSON or cannot intercept; let rawFetch handle
|
|
625
916
|
}
|
|
626
917
|
}
|
|
627
|
-
|
|
918
|
+
const response = await rawFetch(request, requestOptions);
|
|
919
|
+
if (this.visibilityRequiresSession()) {
|
|
920
|
+
return this.attachIssuedSession(request, response);
|
|
921
|
+
}
|
|
922
|
+
return response;
|
|
628
923
|
},
|
|
629
924
|
};
|
|
630
925
|
}
|
|
631
926
|
return this.handler;
|
|
632
927
|
}
|
|
633
|
-
|
|
928
|
+
sessionRequiredResponse(id) {
|
|
929
|
+
return new Response(JSON.stringify({
|
|
930
|
+
jsonrpc: '2.0',
|
|
931
|
+
id,
|
|
932
|
+
error: { code: -32600, message: 'Session required' },
|
|
933
|
+
}), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Session gate for the modern HTTP fetch path.
|
|
937
|
+
* `initialize`, `ping`, and `server/discover` proceed without a session.
|
|
938
|
+
* `tools/list`, `tools/call`, and `resources/read` need an id this process minted.
|
|
939
|
+
* A POST whose method cannot be read fails closed.
|
|
940
|
+
*/
|
|
941
|
+
async visibilityHttpGate(request) {
|
|
942
|
+
const headerMethod = request.headers.get('mcp-method') || request.headers.get('Mcp-Method');
|
|
943
|
+
let bodyMethod;
|
|
944
|
+
let bodyId = null;
|
|
945
|
+
let bodyReadable = true;
|
|
946
|
+
if (request.method === 'POST') {
|
|
947
|
+
try {
|
|
948
|
+
const body = (await request.clone().json());
|
|
949
|
+
bodyMethod = typeof body?.method === 'string' ? body.method : undefined;
|
|
950
|
+
bodyId = body?.id ?? null;
|
|
951
|
+
}
|
|
952
|
+
catch {
|
|
953
|
+
bodyReadable = false;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
if (request.method === 'POST' && !bodyReadable && !headerMethod) {
|
|
957
|
+
return this.sessionRequiredResponse(null);
|
|
958
|
+
}
|
|
959
|
+
// The body is the method that will run. A session-free header must not
|
|
960
|
+
// hide tasks/get or tools/call.
|
|
961
|
+
if (headerMethod && bodyMethod && headerMethod !== bodyMethod) {
|
|
962
|
+
return this.sessionRequiredResponse(bodyId);
|
|
963
|
+
}
|
|
964
|
+
const method = bodyMethod ?? headerMethod;
|
|
965
|
+
this.gatedMethods.set(request, typeof method === 'string' ? method : undefined);
|
|
966
|
+
if (this.isSessionFreeMethod(method)) {
|
|
967
|
+
return null;
|
|
968
|
+
}
|
|
969
|
+
const session = request.headers.get('mcp-session-id') || request.headers.get('Mcp-Session-Id') || undefined;
|
|
970
|
+
const needsSession = method === 'tools/list' ||
|
|
971
|
+
method === 'tools/call' ||
|
|
972
|
+
method === 'resources/read' ||
|
|
973
|
+
method === 'tasks/get' ||
|
|
974
|
+
method === 'tasks/cancel' ||
|
|
975
|
+
method === 'tasks/update' ||
|
|
976
|
+
(!method && request.method === 'POST');
|
|
977
|
+
if ((needsSession && !session) || (session && !this.touchIssuedSession(session))) {
|
|
978
|
+
return this.sessionRequiredResponse(bodyId);
|
|
979
|
+
}
|
|
980
|
+
return null;
|
|
981
|
+
}
|
|
982
|
+
/** Stamp a freshly minted session id onto a successful initialize response. */
|
|
983
|
+
async attachIssuedSession(request, response) {
|
|
984
|
+
const headerMethod = request.headers.get('mcp-method') || request.headers.get('Mcp-Method');
|
|
985
|
+
const method = this.gatedMethods.get(request) ?? headerMethod;
|
|
986
|
+
if (method !== 'initialize' || !response.ok)
|
|
987
|
+
return response;
|
|
988
|
+
const contentType = response.headers.get('content-type');
|
|
989
|
+
const text = await this.readResponseText(response, contentType);
|
|
990
|
+
const parsed = this.extractRpcMessage(text, contentType);
|
|
991
|
+
const headers = new Headers(response.headers);
|
|
992
|
+
if (parsed && parsed.error == null && parsed.result) {
|
|
993
|
+
headers.set('mcp-session-id', this.issueSession());
|
|
994
|
+
}
|
|
995
|
+
return new Response(text, { status: response.status, statusText: response.statusText, headers });
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Read a finite JSON body, or the first SSE frame.
|
|
999
|
+
* A legacy initialize answer is a stream that stays open after the handshake.
|
|
1000
|
+
*/
|
|
1001
|
+
async readResponseText(response, contentType) {
|
|
1002
|
+
if (!contentType?.includes('text/event-stream') || !response.body) {
|
|
1003
|
+
return response.text();
|
|
1004
|
+
}
|
|
1005
|
+
const reader = response.body.getReader();
|
|
1006
|
+
const decoder = new TextDecoder();
|
|
1007
|
+
let buffer = '';
|
|
1008
|
+
const deadline = Date.now() + 5000;
|
|
1009
|
+
try {
|
|
1010
|
+
while (Date.now() < deadline) {
|
|
1011
|
+
const { value, done } = await reader.read();
|
|
1012
|
+
if (value)
|
|
1013
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1014
|
+
if (this.extractRpcMessage(buffer, contentType))
|
|
1015
|
+
break;
|
|
1016
|
+
if (done)
|
|
1017
|
+
break;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
finally {
|
|
1021
|
+
await reader.cancel().catch(() => undefined);
|
|
1022
|
+
}
|
|
1023
|
+
return buffer;
|
|
1024
|
+
}
|
|
1025
|
+
/** First JSON-RPC message from a JSON body or an SSE `data:` frame. */
|
|
1026
|
+
extractRpcMessage(text, contentType) {
|
|
1027
|
+
if (contentType?.includes('text/event-stream')) {
|
|
1028
|
+
for (const line of text.split('\n')) {
|
|
1029
|
+
const trimmed = line.trim();
|
|
1030
|
+
if (!trimmed.startsWith('data:'))
|
|
1031
|
+
continue;
|
|
1032
|
+
try {
|
|
1033
|
+
const parsed = JSON.parse(trimmed.slice(5).trim());
|
|
1034
|
+
if (parsed && (parsed.result !== undefined || parsed.error !== undefined))
|
|
1035
|
+
return parsed;
|
|
1036
|
+
}
|
|
1037
|
+
catch {
|
|
1038
|
+
/* keep scanning */
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
return undefined;
|
|
1042
|
+
}
|
|
1043
|
+
try {
|
|
1044
|
+
return JSON.parse(text);
|
|
1045
|
+
}
|
|
1046
|
+
catch {
|
|
1047
|
+
return undefined;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
/** Raw mcp-session-id from a Web Request or an Express request. Not an isolation key. */
|
|
1051
|
+
requestSessionId(req) {
|
|
634
1052
|
const reqAny = req;
|
|
635
|
-
const
|
|
1053
|
+
const headers = reqAny?.headers;
|
|
1054
|
+
const fromObject = headers?.['mcp-session-id'] || headers?.['Mcp-Session-Id'];
|
|
1055
|
+
const fromGetter = typeof headers?.get === 'function'
|
|
1056
|
+
? headers.get('mcp-session-id') || headers.get('Mcp-Session-Id')
|
|
1057
|
+
: undefined;
|
|
1058
|
+
const fromExpress = typeof reqAny?.get === 'function'
|
|
1059
|
+
? reqAny.get('mcp-session-id') || reqAny.get('Mcp-Session-Id')
|
|
1060
|
+
: undefined;
|
|
1061
|
+
const value = fromObject || fromGetter || fromExpress;
|
|
1062
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Principal attached by the host or the SDK.
|
|
1066
|
+
* `auth` on the request body and `_meta` are not verified and are ignored.
|
|
1067
|
+
*/
|
|
1068
|
+
trustedAuthInfo(source) {
|
|
1069
|
+
if (!source || typeof source !== 'object')
|
|
1070
|
+
return undefined;
|
|
1071
|
+
const mcpReq = (source.mcpReq ?? {});
|
|
1072
|
+
const candidates = [source.http?.authInfo, mcpReq.http?.authInfo, source.authInfo];
|
|
1073
|
+
for (const candidate of candidates) {
|
|
1074
|
+
if (candidate && typeof candidate === 'object')
|
|
1075
|
+
return candidate;
|
|
1076
|
+
}
|
|
1077
|
+
return undefined;
|
|
1078
|
+
}
|
|
1079
|
+
/** SDK authInfo when the host attached it. A bearer header is not verified here. */
|
|
1080
|
+
requestAuthInfo(req) {
|
|
1081
|
+
return this.trustedAuthInfo(req);
|
|
1082
|
+
}
|
|
1083
|
+
extractAccessContext(req, _parsedBody) {
|
|
1084
|
+
const reqAny = req;
|
|
1085
|
+
const auth = (reqAny?.authInfo || reqAny?.auth || reqAny?.user);
|
|
636
1086
|
const userId = auth?.sub || auth?.userId || auth?.id;
|
|
637
1087
|
const tenantId = auth?.tenantId || auth?.orgId;
|
|
638
|
-
const sessionId =
|
|
1088
|
+
const sessionId = this.requestSessionId(req);
|
|
639
1089
|
if (!userId && !tenantId && !sessionId) {
|
|
640
1090
|
return undefined;
|
|
641
1091
|
}
|
|
642
1092
|
return { userId, tenantId, sessionId };
|
|
643
1093
|
}
|
|
1094
|
+
/**
|
|
1095
|
+
* tasks/get, tasks/cancel, and tasks/update carry tool results. When visibility
|
|
1096
|
+
* is on they need the same minted session as tools/call. params.sessionId is
|
|
1097
|
+
* not a credential.
|
|
1098
|
+
*/
|
|
1099
|
+
taskAccessForRequest(req, id, accessContext) {
|
|
1100
|
+
if (!this.visibilityRequiresSession()) {
|
|
1101
|
+
return { access: accessContext };
|
|
1102
|
+
}
|
|
1103
|
+
try {
|
|
1104
|
+
const issued = this.httpSessionOrThrow(this.requestSessionId(req));
|
|
1105
|
+
this.bindIssuedSubject(issued, this.requestAuthInfo(req));
|
|
1106
|
+
return { access: { ...(accessContext ?? {}), sessionId: issued } };
|
|
1107
|
+
}
|
|
1108
|
+
catch (err) {
|
|
1109
|
+
return {
|
|
1110
|
+
error: {
|
|
1111
|
+
jsonrpc: '2.0',
|
|
1112
|
+
id: id ?? null,
|
|
1113
|
+
error: {
|
|
1114
|
+
code: -32600,
|
|
1115
|
+
message: err instanceof Error ? err.message : 'Session required',
|
|
1116
|
+
},
|
|
1117
|
+
},
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
644
1121
|
async handleTaskPreDispatch(body, req) {
|
|
645
1122
|
if (!this.taskManager || !body || typeof body !== 'object')
|
|
646
1123
|
return null;
|
|
@@ -652,8 +1129,11 @@ export class ModernProtocolAdapter {
|
|
|
652
1129
|
if (!taskId) {
|
|
653
1130
|
return { jsonrpc: '2.0', id: id ?? null, error: { code: -32602, message: 'Invalid params: taskId is required' } };
|
|
654
1131
|
}
|
|
1132
|
+
const gated = this.taskAccessForRequest(req, id, accessContext);
|
|
1133
|
+
if (gated.error)
|
|
1134
|
+
return gated.error;
|
|
655
1135
|
try {
|
|
656
|
-
const entry = this.taskManager.getEntry(taskId,
|
|
1136
|
+
const entry = this.taskManager.getEntry(taskId, gated.access);
|
|
657
1137
|
const resultPayload = { ...entry.data };
|
|
658
1138
|
if (entry.data.status === 'completed' && entry.result !== undefined) {
|
|
659
1139
|
resultPayload.result = entry.result;
|
|
@@ -673,8 +1153,11 @@ export class ModernProtocolAdapter {
|
|
|
673
1153
|
if (!taskId) {
|
|
674
1154
|
return { jsonrpc: '2.0', id: id ?? null, error: { code: -32602, message: 'Invalid params: taskId is required' } };
|
|
675
1155
|
}
|
|
1156
|
+
const gated = this.taskAccessForRequest(req, id, accessContext);
|
|
1157
|
+
if (gated.error)
|
|
1158
|
+
return gated.error;
|
|
676
1159
|
try {
|
|
677
|
-
const taskData = this.taskManager.cancelTask(taskId,
|
|
1160
|
+
const taskData = this.taskManager.cancelTask(taskId, gated.access);
|
|
678
1161
|
return { jsonrpc: '2.0', id: id ?? null, result: taskData };
|
|
679
1162
|
}
|
|
680
1163
|
catch (err) {
|
|
@@ -687,8 +1170,11 @@ export class ModernProtocolAdapter {
|
|
|
687
1170
|
if (!taskId) {
|
|
688
1171
|
return { jsonrpc: '2.0', id: id ?? null, error: { code: -32602, message: 'Invalid params: taskId is required' } };
|
|
689
1172
|
}
|
|
1173
|
+
const gated = this.taskAccessForRequest(req, id, accessContext);
|
|
1174
|
+
if (gated.error)
|
|
1175
|
+
return gated.error;
|
|
690
1176
|
try {
|
|
691
|
-
const taskData = this.taskManager.updateStatus(taskId, params.status || 'working', params.statusMessage,
|
|
1177
|
+
const taskData = this.taskManager.updateStatus(taskId, params.status || 'working', params.statusMessage, gated.access);
|
|
692
1178
|
return { jsonrpc: '2.0', id: id ?? null, result: taskData };
|
|
693
1179
|
}
|
|
694
1180
|
catch (err) {
|
|
@@ -717,14 +1203,55 @@ export class ModernProtocolAdapter {
|
|
|
717
1203
|
},
|
|
718
1204
|
};
|
|
719
1205
|
}
|
|
720
|
-
// 6. tools/call with task augmentation OR mandatory task support check
|
|
721
|
-
|
|
1206
|
+
// 6. tools/call with task augmentation OR mandatory task support check.
|
|
1207
|
+
// Resolution goes through the transform chain so session visibility still applies.
|
|
1208
|
+
// The raw catalog lookup used to execute hidden and disabled tools.
|
|
1209
|
+
if (method === 'tools/call' && params && typeof params.name === 'string') {
|
|
722
1210
|
const toolName = params.name;
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
1211
|
+
let issuedSession;
|
|
1212
|
+
const taskAuth = this.requestAuthInfo(req);
|
|
1213
|
+
try {
|
|
1214
|
+
issuedSession = this.httpSessionOrThrow(this.requestSessionId(req));
|
|
1215
|
+
this.bindIssuedSubject(issuedSession, taskAuth);
|
|
1216
|
+
}
|
|
1217
|
+
catch (err) {
|
|
1218
|
+
return {
|
|
1219
|
+
jsonrpc: '2.0',
|
|
1220
|
+
id: id ?? null,
|
|
1221
|
+
error: {
|
|
1222
|
+
code: -32600,
|
|
1223
|
+
message: err instanceof Error ? err.message : 'Session required',
|
|
1224
|
+
},
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
const executionContext = this.executionContextFromRequest({
|
|
1228
|
+
toolName,
|
|
1229
|
+
sessionId: issuedSession,
|
|
1230
|
+
authInfo: taskAuth,
|
|
1231
|
+
});
|
|
1232
|
+
let tool;
|
|
1233
|
+
try {
|
|
1234
|
+
tool = await this.registry.resolveTool(toolName, executionContext);
|
|
1235
|
+
}
|
|
1236
|
+
catch (err) {
|
|
1237
|
+
const coded = err;
|
|
1238
|
+
return {
|
|
1239
|
+
jsonrpc: '2.0',
|
|
1240
|
+
id: id ?? null,
|
|
1241
|
+
error: {
|
|
1242
|
+
code: typeof coded.code === 'number' ? coded.code : -32603,
|
|
1243
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1244
|
+
},
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
if (!tool) {
|
|
1248
|
+
return {
|
|
1249
|
+
jsonrpc: '2.0',
|
|
1250
|
+
id: id ?? null,
|
|
1251
|
+
error: { code: -32601, message: `Tool '${toolName}' not found` },
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
726
1254
|
const isTaskAugmented = params.task !== undefined;
|
|
727
|
-
// Enforcement: if taskSupport === 'required' and not task-augmented
|
|
728
1255
|
if (!isTaskAugmented && tool.taskSupport === 'required') {
|
|
729
1256
|
return {
|
|
730
1257
|
jsonrpc: '2.0',
|
|
@@ -732,58 +1259,54 @@ export class ModernProtocolAdapter {
|
|
|
732
1259
|
error: { code: -32600, message: `Task augmentation required for tools/call requests on tool '${toolName}'` },
|
|
733
1260
|
};
|
|
734
1261
|
}
|
|
735
|
-
//
|
|
736
|
-
if (isTaskAugmented)
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
jsonrpc: '2.0',
|
|
740
|
-
id: id ?? null,
|
|
741
|
-
error: { code: -32601, message: `Tool '${toolName}' does not support task augmentation` },
|
|
742
|
-
};
|
|
743
|
-
}
|
|
744
|
-
const taskData = this.taskManager.createTask(params.task, toolName, accessContext);
|
|
745
|
-
const taskId = taskData.taskId;
|
|
746
|
-
const taskContext = new TaskContext(this.taskManager, taskId);
|
|
747
|
-
const executionContext = this.registry.createExecutionContext({
|
|
748
|
-
toolName,
|
|
749
|
-
extra: {
|
|
750
|
-
task: taskContext,
|
|
751
|
-
},
|
|
752
|
-
});
|
|
753
|
-
executionContext.task = taskContext;
|
|
754
|
-
const tm = this.taskManager;
|
|
755
|
-
// Run tool asynchronously in the background
|
|
756
|
-
Promise.resolve().then(async () => {
|
|
757
|
-
try {
|
|
758
|
-
const argsRecord = (params.arguments || {});
|
|
759
|
-
const { _meta: _, ...toolArgs } = argsRecord;
|
|
760
|
-
const toolResult = await tool.execute(toolArgs, executionContext);
|
|
761
|
-
if (tm.hasTask(taskId)) {
|
|
762
|
-
const current = tm.getTask(taskId);
|
|
763
|
-
if (current.status !== 'cancelled') {
|
|
764
|
-
tm.completeTask(taskId, toolResult, undefined, accessContext);
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
catch (err) {
|
|
769
|
-
if (tm.hasTask(taskId)) {
|
|
770
|
-
const current = tm.getTask(taskId);
|
|
771
|
-
if (current.status !== 'cancelled') {
|
|
772
|
-
tm.failTask(taskId, { code: err.code || -32603, message: err.message || String(err) }, undefined, accessContext);
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
});
|
|
777
|
-
// Return CreateTaskResult immediately
|
|
1262
|
+
// Synchronous tools/call stays on the registered handler, which resolves again.
|
|
1263
|
+
if (!isTaskAugmented)
|
|
1264
|
+
return null;
|
|
1265
|
+
if (tool.taskSupport === 'forbidden') {
|
|
778
1266
|
return {
|
|
779
1267
|
jsonrpc: '2.0',
|
|
780
1268
|
id: id ?? null,
|
|
781
|
-
|
|
782
|
-
task: taskData,
|
|
783
|
-
resultType: 'task',
|
|
784
|
-
},
|
|
1269
|
+
error: { code: -32601, message: `Tool '${toolName}' does not support task augmentation` },
|
|
785
1270
|
};
|
|
786
1271
|
}
|
|
1272
|
+
const taskData = this.taskManager.createTask(params.task, toolName, accessContext);
|
|
1273
|
+
const taskId = taskData.taskId;
|
|
1274
|
+
const taskContext = new TaskContext(this.taskManager, taskId);
|
|
1275
|
+
executionContext.task = taskContext;
|
|
1276
|
+
const tm = this.taskManager;
|
|
1277
|
+
Promise.resolve().then(async () => {
|
|
1278
|
+
try {
|
|
1279
|
+
const argsRecord = (params.arguments || {});
|
|
1280
|
+
const { _meta: _, ...toolArgs } = argsRecord;
|
|
1281
|
+
const toolResult = await tool.execute(toolArgs, executionContext);
|
|
1282
|
+
if (tm.hasTask(taskId)) {
|
|
1283
|
+
const current = tm.getTask(taskId);
|
|
1284
|
+
if (current.status !== 'cancelled') {
|
|
1285
|
+
tm.completeTask(taskId, toolResult, undefined, accessContext);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
catch (err) {
|
|
1290
|
+
if (tm.hasTask(taskId)) {
|
|
1291
|
+
const current = tm.getTask(taskId);
|
|
1292
|
+
if (current.status !== 'cancelled') {
|
|
1293
|
+
const coded = err;
|
|
1294
|
+
tm.failTask(taskId, {
|
|
1295
|
+
code: typeof coded.code === 'number' ? coded.code : -32603,
|
|
1296
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1297
|
+
}, undefined, accessContext);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
});
|
|
1302
|
+
return {
|
|
1303
|
+
jsonrpc: '2.0',
|
|
1304
|
+
id: id ?? null,
|
|
1305
|
+
result: {
|
|
1306
|
+
task: taskData,
|
|
1307
|
+
resultType: 'task',
|
|
1308
|
+
},
|
|
1309
|
+
};
|
|
787
1310
|
}
|
|
788
1311
|
return null;
|
|
789
1312
|
}
|
|
@@ -865,7 +1388,8 @@ export class ModernProtocolAdapter {
|
|
|
865
1388
|
}
|
|
866
1389
|
/**
|
|
867
1390
|
* SEP-2243/SEP-2575 CORS: expose and allow the new required request headers
|
|
868
|
-
* (`MCP-Protocol-Version`, `Mcp-Method`, `Mcp-Name`,
|
|
1391
|
+
* (`MCP-Protocol-Version`, `Mcp-Method`, `Mcp-Name`, `Mcp-Param-*`, and
|
|
1392
|
+
* `Mcp-Session-Id`). Session visibility and spillover both read that header.
|
|
869
1393
|
*/
|
|
870
1394
|
applyCorsHeaders(req, res) {
|
|
871
1395
|
const origin = req.headers.origin;
|
|
@@ -879,20 +1403,26 @@ export class ModernProtocolAdapter {
|
|
|
879
1403
|
'Mcp-Method',
|
|
880
1404
|
'Mcp-Name',
|
|
881
1405
|
'Mcp-Param-*',
|
|
1406
|
+
'Mcp-Session-Id',
|
|
882
1407
|
'Last-Event-ID',
|
|
883
1408
|
].join(', '));
|
|
884
1409
|
res.setHeader('Access-Control-Expose-Headers', ['MCP-Protocol-Version', 'Mcp-Method', 'Mcp-Name'].join(', '));
|
|
885
1410
|
}
|
|
886
1411
|
async serveStdio() {
|
|
1412
|
+
this.stdioSessionId ??= crypto.randomUUID();
|
|
887
1413
|
const stdio = (await import('@modelcontextprotocol/server/stdio'));
|
|
888
|
-
this.stdioHandle = stdio.serveStdio(() => this.buildServer());
|
|
1414
|
+
this.stdioHandle = stdio.serveStdio((ctx) => this.buildServer(ctx, 'stdio'));
|
|
889
1415
|
this.registry.logger.info(`Modern MCP (${MODERN_PROTOCOL_VERSION}) serving over stdio`);
|
|
890
1416
|
}
|
|
891
1417
|
// ==========================================================================
|
|
892
1418
|
// Notifications (subscriptions/listen bus)
|
|
893
1419
|
// ==========================================================================
|
|
894
|
-
notifyToolsListChanged() {
|
|
895
|
-
this.
|
|
1420
|
+
notifyToolsListChanged(sessionId) {
|
|
1421
|
+
if (!sessionId || sessionId === this.stdioSessionId) {
|
|
1422
|
+
this.resyncStdioTools();
|
|
1423
|
+
}
|
|
1424
|
+
this.handler?.notify?.toolsChanged?.(sessionId);
|
|
1425
|
+
this.handler?.bus?.emit?.('tools_changed', sessionId ? { sessionId } : {});
|
|
896
1426
|
}
|
|
897
1427
|
notifyResourcesListChanged() {
|
|
898
1428
|
this.handler?.notify?.resourcesChanged?.();
|
|
@@ -927,6 +1457,7 @@ export class ModernProtocolAdapter {
|
|
|
927
1457
|
}
|
|
928
1458
|
this.handler = undefined;
|
|
929
1459
|
this.stdioHandle = undefined;
|
|
1460
|
+
this.stdioCatalog = undefined;
|
|
930
1461
|
}
|
|
931
1462
|
/** The extensions map this adapter would advertise (for diagnostics/notes). */
|
|
932
1463
|
advertisedExtensions() {
|