@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.
Files changed (199) hide show
  1. package/dist/core/app-decorator.d.ts +5 -0
  2. package/dist/core/app-decorator.d.ts.map +1 -1
  3. package/dist/core/app-decorator.js +2 -1
  4. package/dist/core/app-decorator.js.map +1 -1
  5. package/dist/core/builders.d.ts.map +1 -1
  6. package/dist/core/builders.js +2 -0
  7. package/dist/core/builders.js.map +1 -1
  8. package/dist/core/decorators.d.ts +11 -0
  9. package/dist/core/decorators.d.ts.map +1 -1
  10. package/dist/core/decorators.js.map +1 -1
  11. package/dist/core/health/health-checks.resource.d.ts +4 -3
  12. package/dist/core/health/health-checks.resource.d.ts.map +1 -1
  13. package/dist/core/health/health-checks.resource.js +35 -8
  14. package/dist/core/health/health-checks.resource.js.map +1 -1
  15. package/dist/core/health/health.interface.d.ts +40 -0
  16. package/dist/core/health/health.interface.d.ts.map +1 -0
  17. package/dist/core/health/health.interface.js +2 -0
  18. package/dist/core/health/health.interface.js.map +1 -0
  19. package/dist/core/index.d.ts +11 -2
  20. package/dist/core/index.d.ts.map +1 -1
  21. package/dist/core/index.js +9 -1
  22. package/dist/core/index.js.map +1 -1
  23. package/dist/core/interceptors/data-spillover.interceptor.d.ts +30 -0
  24. package/dist/core/interceptors/data-spillover.interceptor.d.ts.map +1 -0
  25. package/dist/core/interceptors/data-spillover.interceptor.js +174 -0
  26. package/dist/core/interceptors/data-spillover.interceptor.js.map +1 -0
  27. package/dist/core/interceptors/interceptor.decorator.d.ts +17 -8
  28. package/dist/core/interceptors/interceptor.decorator.d.ts.map +1 -1
  29. package/dist/core/interceptors/interceptor.decorator.js +36 -11
  30. package/dist/core/interceptors/interceptor.decorator.js.map +1 -1
  31. package/dist/core/interceptors/interceptor.interface.d.ts +4 -0
  32. package/dist/core/interceptors/interceptor.interface.d.ts.map +1 -1
  33. package/dist/core/interceptors/spillover/fs-spillover.store.d.ts +54 -0
  34. package/dist/core/interceptors/spillover/fs-spillover.store.d.ts.map +1 -0
  35. package/dist/core/interceptors/spillover/fs-spillover.store.js +302 -0
  36. package/dist/core/interceptors/spillover/fs-spillover.store.js.map +1 -0
  37. package/dist/core/interceptors/spillover/index.d.ts +6 -0
  38. package/dist/core/interceptors/spillover/index.d.ts.map +1 -0
  39. package/dist/core/interceptors/spillover/index.js +6 -0
  40. package/dist/core/interceptors/spillover/index.js.map +1 -0
  41. package/dist/core/interceptors/spillover/memory-spillover.store.d.ts +25 -0
  42. package/dist/core/interceptors/spillover/memory-spillover.store.d.ts.map +1 -0
  43. package/dist/core/interceptors/spillover/memory-spillover.store.js +115 -0
  44. package/dist/core/interceptors/spillover/memory-spillover.store.js.map +1 -0
  45. package/dist/core/interceptors/spillover/preview-generator.d.ts +7 -0
  46. package/dist/core/interceptors/spillover/preview-generator.d.ts.map +1 -0
  47. package/dist/core/interceptors/spillover/preview-generator.js +79 -0
  48. package/dist/core/interceptors/spillover/preview-generator.js.map +1 -0
  49. package/dist/core/interceptors/spillover/spillover-store.interface.d.ts +20 -0
  50. package/dist/core/interceptors/spillover/spillover-store.interface.d.ts.map +1 -0
  51. package/dist/core/interceptors/spillover/spillover-store.interface.js +2 -0
  52. package/dist/core/interceptors/spillover/spillover-store.interface.js.map +1 -0
  53. package/dist/core/interceptors/spillover/types.d.ts +71 -0
  54. package/dist/core/interceptors/spillover/types.d.ts.map +1 -0
  55. package/dist/core/interceptors/spillover/types.js +12 -0
  56. package/dist/core/interceptors/spillover/types.js.map +1 -0
  57. package/dist/core/protocol/adapter.d.ts +7 -1
  58. package/dist/core/protocol/adapter.d.ts.map +1 -1
  59. package/dist/core/protocol/modern-v2.adapter.d.ts +95 -2
  60. package/dist/core/protocol/modern-v2.adapter.d.ts.map +1 -1
  61. package/dist/core/protocol/modern-v2.adapter.js +703 -165
  62. package/dist/core/protocol/modern-v2.adapter.js.map +1 -1
  63. package/dist/core/server.d.ts +104 -4
  64. package/dist/core/server.d.ts.map +1 -1
  65. package/dist/core/server.js +465 -52
  66. package/dist/core/server.js.map +1 -1
  67. package/dist/core/task.d.ts.map +1 -1
  68. package/dist/core/task.js +8 -7
  69. package/dist/core/task.js.map +1 -1
  70. package/dist/core/tool.d.ts +12 -3
  71. package/dist/core/tool.d.ts.map +1 -1
  72. package/dist/core/tool.js +21 -3
  73. package/dist/core/tool.js.map +1 -1
  74. package/dist/core/transforms/catalog.transform.d.ts +40 -0
  75. package/dist/core/transforms/catalog.transform.d.ts.map +1 -0
  76. package/dist/core/transforms/catalog.transform.js +52 -0
  77. package/dist/core/transforms/catalog.transform.js.map +1 -0
  78. package/dist/core/transforms/code-mode/code-mode.transform.d.ts +75 -0
  79. package/dist/core/transforms/code-mode/code-mode.transform.d.ts.map +1 -0
  80. package/dist/core/transforms/code-mode/code-mode.transform.js +226 -0
  81. package/dist/core/transforms/code-mode/code-mode.transform.js.map +1 -0
  82. package/dist/core/transforms/code-mode/destructive-guard.d.ts +7 -0
  83. package/dist/core/transforms/code-mode/destructive-guard.d.ts.map +1 -0
  84. package/dist/core/transforms/code-mode/destructive-guard.js +18 -0
  85. package/dist/core/transforms/code-mode/destructive-guard.js.map +1 -0
  86. package/dist/core/transforms/code-mode/guest-evaluator.d.ts +17 -0
  87. package/dist/core/transforms/code-mode/guest-evaluator.d.ts.map +1 -0
  88. package/dist/core/transforms/code-mode/guest-evaluator.js +277 -0
  89. package/dist/core/transforms/code-mode/guest-evaluator.js.map +1 -0
  90. package/dist/core/transforms/code-mode/index.d.ts +11 -0
  91. package/dist/core/transforms/code-mode/index.d.ts.map +1 -0
  92. package/dist/core/transforms/code-mode/index.js +11 -0
  93. package/dist/core/transforms/code-mode/index.js.map +1 -0
  94. package/dist/core/transforms/code-mode/ipc-messages.d.ts +31 -0
  95. package/dist/core/transforms/code-mode/ipc-messages.d.ts.map +1 -0
  96. package/dist/core/transforms/code-mode/ipc-messages.js +2 -0
  97. package/dist/core/transforms/code-mode/ipc-messages.js.map +1 -0
  98. package/dist/core/transforms/code-mode/legible-error.d.ts +8 -0
  99. package/dist/core/transforms/code-mode/legible-error.d.ts.map +1 -0
  100. package/dist/core/transforms/code-mode/legible-error.js +17 -0
  101. package/dist/core/transforms/code-mode/legible-error.js.map +1 -0
  102. package/dist/core/transforms/code-mode/quickjs-wasm.provider.d.ts +25 -0
  103. package/dist/core/transforms/code-mode/quickjs-wasm.provider.d.ts.map +1 -0
  104. package/dist/core/transforms/code-mode/quickjs-wasm.provider.js +31 -0
  105. package/dist/core/transforms/code-mode/quickjs-wasm.provider.js.map +1 -0
  106. package/dist/core/transforms/code-mode/sandbox.interface.d.ts +10 -0
  107. package/dist/core/transforms/code-mode/sandbox.interface.d.ts.map +1 -0
  108. package/dist/core/transforms/code-mode/sandbox.interface.js +2 -0
  109. package/dist/core/transforms/code-mode/sandbox.interface.js.map +1 -0
  110. package/dist/core/transforms/code-mode/sandbox.worker.d.ts +2 -0
  111. package/dist/core/transforms/code-mode/sandbox.worker.d.ts.map +1 -0
  112. package/dist/core/transforms/code-mode/sandbox.worker.js +69 -0
  113. package/dist/core/transforms/code-mode/sandbox.worker.js.map +1 -0
  114. package/dist/core/transforms/code-mode/synthetic-tools.d.ts +23 -0
  115. package/dist/core/transforms/code-mode/synthetic-tools.d.ts.map +1 -0
  116. package/dist/core/transforms/code-mode/synthetic-tools.js +112 -0
  117. package/dist/core/transforms/code-mode/synthetic-tools.js.map +1 -0
  118. package/dist/core/transforms/code-mode/types.d.ts +32 -0
  119. package/dist/core/transforms/code-mode/types.d.ts.map +1 -0
  120. package/dist/core/transforms/code-mode/types.js +2 -0
  121. package/dist/core/transforms/code-mode/types.js.map +1 -0
  122. package/dist/core/transforms/code-mode/worker-pool.d.ts +71 -0
  123. package/dist/core/transforms/code-mode/worker-pool.d.ts.map +1 -0
  124. package/dist/core/transforms/code-mode/worker-pool.js +435 -0
  125. package/dist/core/transforms/code-mode/worker-pool.js.map +1 -0
  126. package/dist/core/transforms/index.d.ts +6 -0
  127. package/dist/core/transforms/index.d.ts.map +1 -0
  128. package/dist/core/transforms/index.js +6 -0
  129. package/dist/core/transforms/index.js.map +1 -0
  130. package/dist/core/transforms/search/base-search.transform.d.ts +59 -0
  131. package/dist/core/transforms/search/base-search.transform.d.ts.map +1 -0
  132. package/dist/core/transforms/search/base-search.transform.js +169 -0
  133. package/dist/core/transforms/search/base-search.transform.js.map +1 -0
  134. package/dist/core/transforms/search/bm25-search.transform.d.ts +27 -0
  135. package/dist/core/transforms/search/bm25-search.transform.d.ts.map +1 -0
  136. package/dist/core/transforms/search/bm25-search.transform.js +37 -0
  137. package/dist/core/transforms/search/bm25-search.transform.js.map +1 -0
  138. package/dist/core/transforms/search/bm25.engine.d.ts +46 -0
  139. package/dist/core/transforms/search/bm25.engine.d.ts.map +1 -0
  140. package/dist/core/transforms/search/bm25.engine.js +160 -0
  141. package/dist/core/transforms/search/bm25.engine.js.map +1 -0
  142. package/dist/core/transforms/search/index.d.ts +9 -0
  143. package/dist/core/transforms/search/index.d.ts.map +1 -0
  144. package/dist/core/transforms/search/index.js +9 -0
  145. package/dist/core/transforms/search/index.js.map +1 -0
  146. package/dist/core/transforms/search/regex-match.worker.d.ts +2 -0
  147. package/dist/core/transforms/search/regex-match.worker.d.ts.map +1 -0
  148. package/dist/core/transforms/search/regex-match.worker.js +19 -0
  149. package/dist/core/transforms/search/regex-match.worker.js.map +1 -0
  150. package/dist/core/transforms/search/regex-search.transform.d.ts +41 -0
  151. package/dist/core/transforms/search/regex-search.transform.d.ts.map +1 -0
  152. package/dist/core/transforms/search/regex-search.transform.js +237 -0
  153. package/dist/core/transforms/search/regex-search.transform.js.map +1 -0
  154. package/dist/core/transforms/search/synthetic-tools.d.ts +11 -0
  155. package/dist/core/transforms/search/synthetic-tools.d.ts.map +1 -0
  156. package/dist/core/transforms/search/synthetic-tools.js +76 -0
  157. package/dist/core/transforms/search/synthetic-tools.js.map +1 -0
  158. package/dist/core/transforms/search/tokenizer.d.ts +11 -0
  159. package/dist/core/transforms/search/tokenizer.d.ts.map +1 -0
  160. package/dist/core/transforms/search/tokenizer.js +38 -0
  161. package/dist/core/transforms/search/tokenizer.js.map +1 -0
  162. package/dist/core/transforms/search/tool-serializer.d.ts +6 -0
  163. package/dist/core/transforms/search/tool-serializer.d.ts.map +1 -0
  164. package/dist/core/transforms/search/tool-serializer.js +41 -0
  165. package/dist/core/transforms/search/tool-serializer.js.map +1 -0
  166. package/dist/core/transforms/search/types.d.ts +36 -0
  167. package/dist/core/transforms/search/types.d.ts.map +1 -0
  168. package/dist/core/transforms/search/types.js +2 -0
  169. package/dist/core/transforms/search/types.js.map +1 -0
  170. package/dist/core/transforms/tool-cache-key.d.ts +15 -0
  171. package/dist/core/transforms/tool-cache-key.d.ts.map +1 -0
  172. package/dist/core/transforms/tool-cache-key.js +61 -0
  173. package/dist/core/transforms/tool-cache-key.js.map +1 -0
  174. package/dist/core/transforms/transform.interface.d.ts +34 -0
  175. package/dist/core/transforms/transform.interface.d.ts.map +1 -0
  176. package/dist/core/transforms/transform.interface.js +2 -0
  177. package/dist/core/transforms/transform.interface.js.map +1 -0
  178. package/dist/core/transforms/validate-tool-arguments.d.ts +10 -0
  179. package/dist/core/transforms/validate-tool-arguments.d.ts.map +1 -0
  180. package/dist/core/transforms/validate-tool-arguments.js +62 -0
  181. package/dist/core/transforms/validate-tool-arguments.js.map +1 -0
  182. package/dist/core/transforms/visibility/index.d.ts +3 -0
  183. package/dist/core/transforms/visibility/index.d.ts.map +1 -0
  184. package/dist/core/transforms/visibility/index.js +3 -0
  185. package/dist/core/transforms/visibility/index.js.map +1 -0
  186. package/dist/core/transforms/visibility/session-store.d.ts +132 -0
  187. package/dist/core/transforms/visibility/session-store.d.ts.map +1 -0
  188. package/dist/core/transforms/visibility/session-store.js +330 -0
  189. package/dist/core/transforms/visibility/session-store.js.map +1 -0
  190. package/dist/core/transforms/visibility/visibility.transform.d.ts +42 -0
  191. package/dist/core/transforms/visibility/visibility.transform.d.ts.map +1 -0
  192. package/dist/core/transforms/visibility/visibility.transform.js +109 -0
  193. package/dist/core/transforms/visibility/visibility.transform.js.map +1 -0
  194. package/dist/core/types.d.ts +60 -0
  195. package/dist/core/types.d.ts.map +1 -1
  196. package/dist/testing/index.d.ts.map +1 -1
  197. package/dist/testing/index.js +3 -0
  198. package/dist/testing/index.js.map +1 -1
  199. 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
- // Advertise the SEP-2133 extensions map on server/discover capabilities.
70
- serverOptions.capabilities = { extensions };
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
- await this.registerTools(server, sdk);
74
- await this.registerResources(server, sdk);
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
- for (const tool of this.registry.getTools().values()) {
85
- const inputSchema = await this.toModernSchema(tool.inputSchema, 'input', sdk);
86
- const outputSchema = tool.outputSchema
87
- ? await this.toModernSchema(tool.outputSchema, 'output', sdk)
88
- : undefined;
89
- const config = {
90
- description: tool.description,
91
- inputSchema,
92
- };
93
- if (tool.title)
94
- config.title = tool.title;
95
- if (outputSchema)
96
- config.outputSchema = outputSchema;
97
- if (tool.annotations)
98
- config.annotations = tool.annotations;
99
- const meta = {};
100
- const cacheHint = resolveToolCacheHint(tool);
101
- if (cacheHint)
102
- meta['io.modelcontextprotocol/cacheHint'] = cacheHint;
103
- if (tool.hasComponent && tool.hasComponent()) {
104
- const component = tool.getComponent();
105
- const resourceUri = component.getResourceUri();
106
- const componentMeta = component.getResourceMetadata();
107
- meta['ui/template'] = resourceUri;
108
- meta['openai/outputTemplate'] = resourceUri;
109
- meta['ui'] = { resourceUri };
110
- if (componentMeta) {
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
- if (tool.isInitial) {
137
- meta['tool/initial'] = true;
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
- if (Object.keys(meta).length > 0) {
140
- config._meta = meta;
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
- for (const [uri, res] of rawResources.entries()) {
215
- if (uri === reqUri || uri.endsWith(reqUri) || reqUri.endsWith(uri)) {
216
- return this.readResource(reqUri, res, sdk);
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 response = {
339
- content: [
340
- {
341
- type: 'text',
342
- text: typeof result === 'string' ? result : JSON.stringify(result, null, 2),
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
- const context = this.registry.createExecutionContext({
406
- extra: { protocolVersion: MODERN_PROTOCOL_VERSION },
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
- const authInfo = readEnvelope('auth', 'io.modelcontextprotocol/auth') ??
459
- ctx?.http?.authInfo ??
460
- mcpReq?.http?.authInfo ??
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
- return this.registry.createExecutionContext({
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
- extra: {
527
- protocolVersion,
528
- clientInfo,
529
- clientCapabilities,
530
- requestState,
531
- inputResponses,
532
- trace,
533
- auth: authInfo ? this.mapAuthInfo(authInfo) : undefined,
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
- return rawFetch(request, requestOptions);
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
- extractAccessContext(req, parsedBody) {
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 auth = reqAny?.auth || reqAny?.user;
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 = reqAny?.headers?.['mcp-session-id'] || reqAny?.get?.('mcp-session-id');
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, accessContext);
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, accessContext);
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, accessContext);
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
- if (method === 'tools/call' && params) {
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
- const tool = this.registry.getTools().get(toolName);
724
- if (!tool)
725
- return null; // Let standard flow handle tool not found
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
- // If task-augmented:
736
- if (isTaskAugmented) {
737
- if (tool.taskSupport === 'forbidden') {
738
- return {
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
- result: {
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`, and `Mcp-Param-*`).
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.handler?.notify?.toolsChanged?.();
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() {