@nitrostack/core 1.0.17 → 1.1.0-beta.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/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 +703 -165
- 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 +465 -52
- 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 +169 -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 +8 -2
|
@@ -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) {
|
|
@@ -335,14 +604,21 @@ export class ModernProtocolAdapter {
|
|
|
335
604
|
message: result.message,
|
|
336
605
|
});
|
|
337
606
|
}
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
607
|
+
const isCallToolResult = result !== null &&
|
|
608
|
+
typeof result === 'object' &&
|
|
609
|
+
Array.isArray(result.content) &&
|
|
610
|
+
result.content.length > 0 &&
|
|
611
|
+
typeof result.content[0]?.type === 'string';
|
|
612
|
+
const response = isCallToolResult
|
|
613
|
+
? { ...result }
|
|
614
|
+
: {
|
|
615
|
+
content: [
|
|
616
|
+
{
|
|
617
|
+
type: 'text',
|
|
618
|
+
text: typeof result === 'string' ? result : JSON.stringify(result, null, 2),
|
|
619
|
+
},
|
|
620
|
+
],
|
|
621
|
+
};
|
|
346
622
|
if (tool.hasComponent()) {
|
|
347
623
|
const component = tool.getComponent();
|
|
348
624
|
response.structuredContent = (await component.transformData(result, context));
|
|
@@ -401,10 +677,14 @@ export class ModernProtocolAdapter {
|
|
|
401
677
|
err.data = mapped.data;
|
|
402
678
|
return err;
|
|
403
679
|
}
|
|
404
|
-
async readResource(uri, resource, sdk) {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
680
|
+
async readResource(uri, resource, sdk, requestContext) {
|
|
681
|
+
// requestContext already passed through createExecutionContext once.
|
|
682
|
+
// Feeding its sessionId back in as extra.sessionId runs sessionIsolationKey
|
|
683
|
+
// a second time and the spillover read no longer matches the write.
|
|
684
|
+
const context = requestContext ??
|
|
685
|
+
this.registry.createExecutionContext({
|
|
686
|
+
extra: { protocolVersion: MODERN_PROTOCOL_VERSION },
|
|
687
|
+
});
|
|
408
688
|
let content;
|
|
409
689
|
try {
|
|
410
690
|
content = await resource.fetch(context, uri);
|
|
@@ -455,11 +735,9 @@ export class ModernProtocolAdapter {
|
|
|
455
735
|
}
|
|
456
736
|
const trace = extractTraceContext({ ...meta, ...envelope });
|
|
457
737
|
const inputResponses = mcpReq.inputResponses;
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
ctx?.authInfo ??
|
|
462
|
-
ctx?.auth;
|
|
738
|
+
// Envelope and `_meta` are the client request. A subject there must not
|
|
739
|
+
// become the isolation principal. Only auth the host or SDK attached.
|
|
740
|
+
const authInfo = this.trustedAuthInfo(ctx);
|
|
463
741
|
const rawHeaders = {};
|
|
464
742
|
const reqHeaders = ctx?.http?.req?.headers ||
|
|
465
743
|
ctx?.http?.headers ||
|
|
@@ -520,18 +798,33 @@ export class ModernProtocolAdapter {
|
|
|
520
798
|
metadata._oauth = rawToken;
|
|
521
799
|
metadata.jwtToken = rawToken;
|
|
522
800
|
}
|
|
523
|
-
|
|
801
|
+
// Header only. requestState is client-echoed MRTR state and must not select a session.
|
|
802
|
+
let sessionId = rawHeaders['mcp-session-id'] ||
|
|
803
|
+
rawHeaders['Mcp-Session-Id'] ||
|
|
804
|
+
ctx?.request?.headers?.get?.('mcp-session-id') ||
|
|
805
|
+
ctx?.req?.headers?.get?.('mcp-session-id') ||
|
|
806
|
+
undefined;
|
|
807
|
+
const hasHttpRequest = Boolean(reqHeaders || ctx?.http || ctx?.request || ctx?.req);
|
|
808
|
+
const usingStdioPeer = !hasHttpRequest && Boolean(this.stdioSessionId);
|
|
809
|
+
if (usingStdioPeer) {
|
|
810
|
+
// The stdio peer has one minted id. A header here is not a second session.
|
|
811
|
+
sessionId = this.stdioSessionId;
|
|
812
|
+
}
|
|
813
|
+
else {
|
|
814
|
+
sessionId = this.httpSessionOrThrow(sessionId);
|
|
815
|
+
this.bindIssuedSubject(sessionId, authInfo);
|
|
816
|
+
}
|
|
817
|
+
return this.executionContextFromRequest({
|
|
524
818
|
toolName: opts.toolName,
|
|
525
819
|
metadata,
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
},
|
|
820
|
+
sessionId,
|
|
821
|
+
authInfo,
|
|
822
|
+
protocolVersion,
|
|
823
|
+
clientInfo,
|
|
824
|
+
clientCapabilities,
|
|
825
|
+
requestState,
|
|
826
|
+
inputResponses,
|
|
827
|
+
trace,
|
|
535
828
|
});
|
|
536
829
|
}
|
|
537
830
|
mapAuthInfo(authInfo) {
|
|
@@ -581,7 +874,7 @@ export class ModernProtocolAdapter {
|
|
|
581
874
|
async getHttpHandler() {
|
|
582
875
|
if (!this.handler) {
|
|
583
876
|
const sdk = await this.loadServerSdk();
|
|
584
|
-
const rawHandler = sdk.createMcpHandler(() => this.buildServer(), {
|
|
877
|
+
const rawHandler = sdk.createMcpHandler((ctx) => this.buildServer(ctx, 'http'), {
|
|
585
878
|
legacy: this.options.legacyMode,
|
|
586
879
|
onerror: (error) => {
|
|
587
880
|
this.registry.logger.error('Modern MCP handler error', { error: error.message });
|
|
@@ -591,6 +884,11 @@ export class ModernProtocolAdapter {
|
|
|
591
884
|
this.handler = {
|
|
592
885
|
...rawHandler,
|
|
593
886
|
fetch: async (request, requestOptions) => {
|
|
887
|
+
if (this.visibilityRequiresSession()) {
|
|
888
|
+
const blocked = await this.visibilityHttpGate(request);
|
|
889
|
+
if (blocked)
|
|
890
|
+
return blocked;
|
|
891
|
+
}
|
|
594
892
|
if (request.headers.get('mcp-method') === 'ping') {
|
|
595
893
|
try {
|
|
596
894
|
const clone = request.clone();
|
|
@@ -624,23 +922,209 @@ export class ModernProtocolAdapter {
|
|
|
624
922
|
// Not JSON or cannot intercept; let rawFetch handle
|
|
625
923
|
}
|
|
626
924
|
}
|
|
627
|
-
|
|
925
|
+
const response = await rawFetch(request, requestOptions);
|
|
926
|
+
if (this.visibilityRequiresSession()) {
|
|
927
|
+
return this.attachIssuedSession(request, response);
|
|
928
|
+
}
|
|
929
|
+
return response;
|
|
628
930
|
},
|
|
629
931
|
};
|
|
630
932
|
}
|
|
631
933
|
return this.handler;
|
|
632
934
|
}
|
|
633
|
-
|
|
935
|
+
sessionRequiredResponse(id) {
|
|
936
|
+
return new Response(JSON.stringify({
|
|
937
|
+
jsonrpc: '2.0',
|
|
938
|
+
id,
|
|
939
|
+
error: { code: -32600, message: 'Session required' },
|
|
940
|
+
}), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Session gate for the modern HTTP fetch path.
|
|
944
|
+
* `initialize`, `ping`, and `server/discover` proceed without a session.
|
|
945
|
+
* `tools/list`, `tools/call`, and `resources/read` need an id this process minted.
|
|
946
|
+
* A POST whose method cannot be read fails closed.
|
|
947
|
+
*/
|
|
948
|
+
async visibilityHttpGate(request) {
|
|
949
|
+
const headerMethod = request.headers.get('mcp-method') || request.headers.get('Mcp-Method');
|
|
950
|
+
let bodyMethod;
|
|
951
|
+
let bodyId = null;
|
|
952
|
+
let bodyReadable = true;
|
|
953
|
+
if (request.method === 'POST') {
|
|
954
|
+
try {
|
|
955
|
+
const body = (await request.clone().json());
|
|
956
|
+
bodyMethod = typeof body?.method === 'string' ? body.method : undefined;
|
|
957
|
+
bodyId = body?.id ?? null;
|
|
958
|
+
}
|
|
959
|
+
catch {
|
|
960
|
+
bodyReadable = false;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
if (request.method === 'POST' && !bodyReadable && !headerMethod) {
|
|
964
|
+
return this.sessionRequiredResponse(null);
|
|
965
|
+
}
|
|
966
|
+
// The body is the method that will run. A session-free header must not
|
|
967
|
+
// hide tasks/get or tools/call.
|
|
968
|
+
if (headerMethod && bodyMethod && headerMethod !== bodyMethod) {
|
|
969
|
+
return this.sessionRequiredResponse(bodyId);
|
|
970
|
+
}
|
|
971
|
+
const method = bodyMethod ?? headerMethod;
|
|
972
|
+
this.gatedMethods.set(request, typeof method === 'string' ? method : undefined);
|
|
973
|
+
if (this.isSessionFreeMethod(method)) {
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
const session = request.headers.get('mcp-session-id') || request.headers.get('Mcp-Session-Id') || undefined;
|
|
977
|
+
const needsSession = method === 'tools/list' ||
|
|
978
|
+
method === 'tools/call' ||
|
|
979
|
+
method === 'resources/read' ||
|
|
980
|
+
method === 'tasks/get' ||
|
|
981
|
+
method === 'tasks/cancel' ||
|
|
982
|
+
method === 'tasks/update' ||
|
|
983
|
+
(!method && request.method === 'POST');
|
|
984
|
+
if ((needsSession && !session) || (session && !this.touchIssuedSession(session))) {
|
|
985
|
+
return this.sessionRequiredResponse(bodyId);
|
|
986
|
+
}
|
|
987
|
+
return null;
|
|
988
|
+
}
|
|
989
|
+
/** Stamp a freshly minted session id onto a successful initialize response. */
|
|
990
|
+
async attachIssuedSession(request, response) {
|
|
991
|
+
const headerMethod = request.headers.get('mcp-method') || request.headers.get('Mcp-Method');
|
|
992
|
+
const method = this.gatedMethods.get(request) ?? headerMethod;
|
|
993
|
+
if (method !== 'initialize' || !response.ok)
|
|
994
|
+
return response;
|
|
995
|
+
const contentType = response.headers.get('content-type');
|
|
996
|
+
const text = await this.readResponseText(response, contentType);
|
|
997
|
+
const parsed = this.extractRpcMessage(text, contentType);
|
|
998
|
+
const headers = new Headers(response.headers);
|
|
999
|
+
if (parsed && parsed.error == null && parsed.result) {
|
|
1000
|
+
headers.set('mcp-session-id', this.issueSession());
|
|
1001
|
+
}
|
|
1002
|
+
return new Response(text, { status: response.status, statusText: response.statusText, headers });
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Read a finite JSON body, or the first SSE frame.
|
|
1006
|
+
* A legacy initialize answer is a stream that stays open after the handshake.
|
|
1007
|
+
*/
|
|
1008
|
+
async readResponseText(response, contentType) {
|
|
1009
|
+
if (!contentType?.includes('text/event-stream') || !response.body) {
|
|
1010
|
+
return response.text();
|
|
1011
|
+
}
|
|
1012
|
+
const reader = response.body.getReader();
|
|
1013
|
+
const decoder = new TextDecoder();
|
|
1014
|
+
let buffer = '';
|
|
1015
|
+
const deadline = Date.now() + 5000;
|
|
1016
|
+
try {
|
|
1017
|
+
while (Date.now() < deadline) {
|
|
1018
|
+
const { value, done } = await reader.read();
|
|
1019
|
+
if (value)
|
|
1020
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1021
|
+
if (this.extractRpcMessage(buffer, contentType))
|
|
1022
|
+
break;
|
|
1023
|
+
if (done)
|
|
1024
|
+
break;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
finally {
|
|
1028
|
+
await reader.cancel().catch(() => undefined);
|
|
1029
|
+
}
|
|
1030
|
+
return buffer;
|
|
1031
|
+
}
|
|
1032
|
+
/** First JSON-RPC message from a JSON body or an SSE `data:` frame. */
|
|
1033
|
+
extractRpcMessage(text, contentType) {
|
|
1034
|
+
if (contentType?.includes('text/event-stream')) {
|
|
1035
|
+
for (const line of text.split('\n')) {
|
|
1036
|
+
const trimmed = line.trim();
|
|
1037
|
+
if (!trimmed.startsWith('data:'))
|
|
1038
|
+
continue;
|
|
1039
|
+
try {
|
|
1040
|
+
const parsed = JSON.parse(trimmed.slice(5).trim());
|
|
1041
|
+
if (parsed && (parsed.result !== undefined || parsed.error !== undefined))
|
|
1042
|
+
return parsed;
|
|
1043
|
+
}
|
|
1044
|
+
catch {
|
|
1045
|
+
/* keep scanning */
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
return undefined;
|
|
1049
|
+
}
|
|
1050
|
+
try {
|
|
1051
|
+
return JSON.parse(text);
|
|
1052
|
+
}
|
|
1053
|
+
catch {
|
|
1054
|
+
return undefined;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
/** Raw mcp-session-id from a Web Request or an Express request. Not an isolation key. */
|
|
1058
|
+
requestSessionId(req) {
|
|
634
1059
|
const reqAny = req;
|
|
635
|
-
const
|
|
1060
|
+
const headers = reqAny?.headers;
|
|
1061
|
+
const fromObject = headers?.['mcp-session-id'] || headers?.['Mcp-Session-Id'];
|
|
1062
|
+
const fromGetter = typeof headers?.get === 'function'
|
|
1063
|
+
? headers.get('mcp-session-id') || headers.get('Mcp-Session-Id')
|
|
1064
|
+
: undefined;
|
|
1065
|
+
const fromExpress = typeof reqAny?.get === 'function'
|
|
1066
|
+
? reqAny.get('mcp-session-id') || reqAny.get('Mcp-Session-Id')
|
|
1067
|
+
: undefined;
|
|
1068
|
+
const value = fromObject || fromGetter || fromExpress;
|
|
1069
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Principal attached by the host or the SDK.
|
|
1073
|
+
* `auth` on the request body and `_meta` are not verified and are ignored.
|
|
1074
|
+
*/
|
|
1075
|
+
trustedAuthInfo(source) {
|
|
1076
|
+
if (!source || typeof source !== 'object')
|
|
1077
|
+
return undefined;
|
|
1078
|
+
const mcpReq = (source.mcpReq ?? {});
|
|
1079
|
+
const candidates = [source.http?.authInfo, mcpReq.http?.authInfo, source.authInfo];
|
|
1080
|
+
for (const candidate of candidates) {
|
|
1081
|
+
if (candidate && typeof candidate === 'object')
|
|
1082
|
+
return candidate;
|
|
1083
|
+
}
|
|
1084
|
+
return undefined;
|
|
1085
|
+
}
|
|
1086
|
+
/** SDK authInfo when the host attached it. A bearer header is not verified here. */
|
|
1087
|
+
requestAuthInfo(req) {
|
|
1088
|
+
return this.trustedAuthInfo(req);
|
|
1089
|
+
}
|
|
1090
|
+
extractAccessContext(req, _parsedBody) {
|
|
1091
|
+
const reqAny = req;
|
|
1092
|
+
const auth = (reqAny?.authInfo || reqAny?.auth || reqAny?.user);
|
|
636
1093
|
const userId = auth?.sub || auth?.userId || auth?.id;
|
|
637
1094
|
const tenantId = auth?.tenantId || auth?.orgId;
|
|
638
|
-
const sessionId =
|
|
1095
|
+
const sessionId = this.requestSessionId(req);
|
|
639
1096
|
if (!userId && !tenantId && !sessionId) {
|
|
640
1097
|
return undefined;
|
|
641
1098
|
}
|
|
642
1099
|
return { userId, tenantId, sessionId };
|
|
643
1100
|
}
|
|
1101
|
+
/**
|
|
1102
|
+
* tasks/get, tasks/cancel, and tasks/update carry tool results. When visibility
|
|
1103
|
+
* is on they need the same minted session as tools/call. params.sessionId is
|
|
1104
|
+
* not a credential.
|
|
1105
|
+
*/
|
|
1106
|
+
taskAccessForRequest(req, id, accessContext) {
|
|
1107
|
+
if (!this.visibilityRequiresSession()) {
|
|
1108
|
+
return { access: accessContext };
|
|
1109
|
+
}
|
|
1110
|
+
try {
|
|
1111
|
+
const issued = this.httpSessionOrThrow(this.requestSessionId(req));
|
|
1112
|
+
this.bindIssuedSubject(issued, this.requestAuthInfo(req));
|
|
1113
|
+
return { access: { ...(accessContext ?? {}), sessionId: issued } };
|
|
1114
|
+
}
|
|
1115
|
+
catch (err) {
|
|
1116
|
+
return {
|
|
1117
|
+
error: {
|
|
1118
|
+
jsonrpc: '2.0',
|
|
1119
|
+
id: id ?? null,
|
|
1120
|
+
error: {
|
|
1121
|
+
code: -32600,
|
|
1122
|
+
message: err instanceof Error ? err.message : 'Session required',
|
|
1123
|
+
},
|
|
1124
|
+
},
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
644
1128
|
async handleTaskPreDispatch(body, req) {
|
|
645
1129
|
if (!this.taskManager || !body || typeof body !== 'object')
|
|
646
1130
|
return null;
|
|
@@ -652,8 +1136,11 @@ export class ModernProtocolAdapter {
|
|
|
652
1136
|
if (!taskId) {
|
|
653
1137
|
return { jsonrpc: '2.0', id: id ?? null, error: { code: -32602, message: 'Invalid params: taskId is required' } };
|
|
654
1138
|
}
|
|
1139
|
+
const gated = this.taskAccessForRequest(req, id, accessContext);
|
|
1140
|
+
if (gated.error)
|
|
1141
|
+
return gated.error;
|
|
655
1142
|
try {
|
|
656
|
-
const entry = this.taskManager.getEntry(taskId,
|
|
1143
|
+
const entry = this.taskManager.getEntry(taskId, gated.access);
|
|
657
1144
|
const resultPayload = { ...entry.data };
|
|
658
1145
|
if (entry.data.status === 'completed' && entry.result !== undefined) {
|
|
659
1146
|
resultPayload.result = entry.result;
|
|
@@ -673,8 +1160,11 @@ export class ModernProtocolAdapter {
|
|
|
673
1160
|
if (!taskId) {
|
|
674
1161
|
return { jsonrpc: '2.0', id: id ?? null, error: { code: -32602, message: 'Invalid params: taskId is required' } };
|
|
675
1162
|
}
|
|
1163
|
+
const gated = this.taskAccessForRequest(req, id, accessContext);
|
|
1164
|
+
if (gated.error)
|
|
1165
|
+
return gated.error;
|
|
676
1166
|
try {
|
|
677
|
-
const taskData = this.taskManager.cancelTask(taskId,
|
|
1167
|
+
const taskData = this.taskManager.cancelTask(taskId, gated.access);
|
|
678
1168
|
return { jsonrpc: '2.0', id: id ?? null, result: taskData };
|
|
679
1169
|
}
|
|
680
1170
|
catch (err) {
|
|
@@ -687,8 +1177,11 @@ export class ModernProtocolAdapter {
|
|
|
687
1177
|
if (!taskId) {
|
|
688
1178
|
return { jsonrpc: '2.0', id: id ?? null, error: { code: -32602, message: 'Invalid params: taskId is required' } };
|
|
689
1179
|
}
|
|
1180
|
+
const gated = this.taskAccessForRequest(req, id, accessContext);
|
|
1181
|
+
if (gated.error)
|
|
1182
|
+
return gated.error;
|
|
690
1183
|
try {
|
|
691
|
-
const taskData = this.taskManager.updateStatus(taskId, params.status || 'working', params.statusMessage,
|
|
1184
|
+
const taskData = this.taskManager.updateStatus(taskId, params.status || 'working', params.statusMessage, gated.access);
|
|
692
1185
|
return { jsonrpc: '2.0', id: id ?? null, result: taskData };
|
|
693
1186
|
}
|
|
694
1187
|
catch (err) {
|
|
@@ -717,14 +1210,55 @@ export class ModernProtocolAdapter {
|
|
|
717
1210
|
},
|
|
718
1211
|
};
|
|
719
1212
|
}
|
|
720
|
-
// 6. tools/call with task augmentation OR mandatory task support check
|
|
721
|
-
|
|
1213
|
+
// 6. tools/call with task augmentation OR mandatory task support check.
|
|
1214
|
+
// Resolution goes through the transform chain so session visibility still applies.
|
|
1215
|
+
// The raw catalog lookup used to execute hidden and disabled tools.
|
|
1216
|
+
if (method === 'tools/call' && params && typeof params.name === 'string') {
|
|
722
1217
|
const toolName = params.name;
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
1218
|
+
let issuedSession;
|
|
1219
|
+
const taskAuth = this.requestAuthInfo(req);
|
|
1220
|
+
try {
|
|
1221
|
+
issuedSession = this.httpSessionOrThrow(this.requestSessionId(req));
|
|
1222
|
+
this.bindIssuedSubject(issuedSession, taskAuth);
|
|
1223
|
+
}
|
|
1224
|
+
catch (err) {
|
|
1225
|
+
return {
|
|
1226
|
+
jsonrpc: '2.0',
|
|
1227
|
+
id: id ?? null,
|
|
1228
|
+
error: {
|
|
1229
|
+
code: -32600,
|
|
1230
|
+
message: err instanceof Error ? err.message : 'Session required',
|
|
1231
|
+
},
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
const executionContext = this.executionContextFromRequest({
|
|
1235
|
+
toolName,
|
|
1236
|
+
sessionId: issuedSession,
|
|
1237
|
+
authInfo: taskAuth,
|
|
1238
|
+
});
|
|
1239
|
+
let tool;
|
|
1240
|
+
try {
|
|
1241
|
+
tool = await this.registry.resolveTool(toolName, executionContext);
|
|
1242
|
+
}
|
|
1243
|
+
catch (err) {
|
|
1244
|
+
const coded = err;
|
|
1245
|
+
return {
|
|
1246
|
+
jsonrpc: '2.0',
|
|
1247
|
+
id: id ?? null,
|
|
1248
|
+
error: {
|
|
1249
|
+
code: typeof coded.code === 'number' ? coded.code : -32603,
|
|
1250
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1251
|
+
},
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
if (!tool) {
|
|
1255
|
+
return {
|
|
1256
|
+
jsonrpc: '2.0',
|
|
1257
|
+
id: id ?? null,
|
|
1258
|
+
error: { code: -32601, message: `Tool '${toolName}' not found` },
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
726
1261
|
const isTaskAugmented = params.task !== undefined;
|
|
727
|
-
// Enforcement: if taskSupport === 'required' and not task-augmented
|
|
728
1262
|
if (!isTaskAugmented && tool.taskSupport === 'required') {
|
|
729
1263
|
return {
|
|
730
1264
|
jsonrpc: '2.0',
|
|
@@ -732,58 +1266,54 @@ export class ModernProtocolAdapter {
|
|
|
732
1266
|
error: { code: -32600, message: `Task augmentation required for tools/call requests on tool '${toolName}'` },
|
|
733
1267
|
};
|
|
734
1268
|
}
|
|
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
|
|
1269
|
+
// Synchronous tools/call stays on the registered handler, which resolves again.
|
|
1270
|
+
if (!isTaskAugmented)
|
|
1271
|
+
return null;
|
|
1272
|
+
if (tool.taskSupport === 'forbidden') {
|
|
778
1273
|
return {
|
|
779
1274
|
jsonrpc: '2.0',
|
|
780
1275
|
id: id ?? null,
|
|
781
|
-
|
|
782
|
-
task: taskData,
|
|
783
|
-
resultType: 'task',
|
|
784
|
-
},
|
|
1276
|
+
error: { code: -32601, message: `Tool '${toolName}' does not support task augmentation` },
|
|
785
1277
|
};
|
|
786
1278
|
}
|
|
1279
|
+
const taskData = this.taskManager.createTask(params.task, toolName, accessContext);
|
|
1280
|
+
const taskId = taskData.taskId;
|
|
1281
|
+
const taskContext = new TaskContext(this.taskManager, taskId);
|
|
1282
|
+
executionContext.task = taskContext;
|
|
1283
|
+
const tm = this.taskManager;
|
|
1284
|
+
Promise.resolve().then(async () => {
|
|
1285
|
+
try {
|
|
1286
|
+
const argsRecord = (params.arguments || {});
|
|
1287
|
+
const { _meta: _, ...toolArgs } = argsRecord;
|
|
1288
|
+
const toolResult = await tool.execute(toolArgs, executionContext);
|
|
1289
|
+
if (tm.hasTask(taskId)) {
|
|
1290
|
+
const current = tm.getTask(taskId);
|
|
1291
|
+
if (current.status !== 'cancelled') {
|
|
1292
|
+
tm.completeTask(taskId, toolResult, undefined, accessContext);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
catch (err) {
|
|
1297
|
+
if (tm.hasTask(taskId)) {
|
|
1298
|
+
const current = tm.getTask(taskId);
|
|
1299
|
+
if (current.status !== 'cancelled') {
|
|
1300
|
+
const coded = err;
|
|
1301
|
+
tm.failTask(taskId, {
|
|
1302
|
+
code: typeof coded.code === 'number' ? coded.code : -32603,
|
|
1303
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1304
|
+
}, undefined, accessContext);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
});
|
|
1309
|
+
return {
|
|
1310
|
+
jsonrpc: '2.0',
|
|
1311
|
+
id: id ?? null,
|
|
1312
|
+
result: {
|
|
1313
|
+
task: taskData,
|
|
1314
|
+
resultType: 'task',
|
|
1315
|
+
},
|
|
1316
|
+
};
|
|
787
1317
|
}
|
|
788
1318
|
return null;
|
|
789
1319
|
}
|
|
@@ -865,7 +1395,8 @@ export class ModernProtocolAdapter {
|
|
|
865
1395
|
}
|
|
866
1396
|
/**
|
|
867
1397
|
* SEP-2243/SEP-2575 CORS: expose and allow the new required request headers
|
|
868
|
-
* (`MCP-Protocol-Version`, `Mcp-Method`, `Mcp-Name`,
|
|
1398
|
+
* (`MCP-Protocol-Version`, `Mcp-Method`, `Mcp-Name`, `Mcp-Param-*`, and
|
|
1399
|
+
* `Mcp-Session-Id`). Session visibility and spillover both read that header.
|
|
869
1400
|
*/
|
|
870
1401
|
applyCorsHeaders(req, res) {
|
|
871
1402
|
const origin = req.headers.origin;
|
|
@@ -879,20 +1410,26 @@ export class ModernProtocolAdapter {
|
|
|
879
1410
|
'Mcp-Method',
|
|
880
1411
|
'Mcp-Name',
|
|
881
1412
|
'Mcp-Param-*',
|
|
1413
|
+
'Mcp-Session-Id',
|
|
882
1414
|
'Last-Event-ID',
|
|
883
1415
|
].join(', '));
|
|
884
1416
|
res.setHeader('Access-Control-Expose-Headers', ['MCP-Protocol-Version', 'Mcp-Method', 'Mcp-Name'].join(', '));
|
|
885
1417
|
}
|
|
886
1418
|
async serveStdio() {
|
|
1419
|
+
this.stdioSessionId ??= crypto.randomUUID();
|
|
887
1420
|
const stdio = (await import('@modelcontextprotocol/server/stdio'));
|
|
888
|
-
this.stdioHandle = stdio.serveStdio(() => this.buildServer());
|
|
1421
|
+
this.stdioHandle = stdio.serveStdio((ctx) => this.buildServer(ctx, 'stdio'));
|
|
889
1422
|
this.registry.logger.info(`Modern MCP (${MODERN_PROTOCOL_VERSION}) serving over stdio`);
|
|
890
1423
|
}
|
|
891
1424
|
// ==========================================================================
|
|
892
1425
|
// Notifications (subscriptions/listen bus)
|
|
893
1426
|
// ==========================================================================
|
|
894
|
-
notifyToolsListChanged() {
|
|
895
|
-
this.
|
|
1427
|
+
notifyToolsListChanged(sessionId) {
|
|
1428
|
+
if (!sessionId || sessionId === this.stdioSessionId) {
|
|
1429
|
+
this.resyncStdioTools();
|
|
1430
|
+
}
|
|
1431
|
+
this.handler?.notify?.toolsChanged?.(sessionId);
|
|
1432
|
+
this.handler?.bus?.emit?.('tools_changed', sessionId ? { sessionId } : {});
|
|
896
1433
|
}
|
|
897
1434
|
notifyResourcesListChanged() {
|
|
898
1435
|
this.handler?.notify?.resourcesChanged?.();
|
|
@@ -927,6 +1464,7 @@ export class ModernProtocolAdapter {
|
|
|
927
1464
|
}
|
|
928
1465
|
this.handler = undefined;
|
|
929
1466
|
this.stdioHandle = undefined;
|
|
1467
|
+
this.stdioCatalog = undefined;
|
|
930
1468
|
}
|
|
931
1469
|
/** The extensions map this adapter would advertise (for diagnostics/notes). */
|
|
932
1470
|
advertisedExtensions() {
|