@nitrostack/core 1.0.16 → 1.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) 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 +688 -157
  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 +450 -44
  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 +166 -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 +9 -6
  200. package/dist/core/widget-bundle-paths.d.ts +0 -5
  201. package/dist/core/widget-bundle-paths.d.ts.map +0 -1
  202. package/dist/core/widget-bundle-paths.js +0 -25
  203. package/dist/core/widget-bundle-paths.js.map +0 -1
  204. package/dist/core/widget-html-mime.d.ts +0 -7
  205. package/dist/core/widget-html-mime.d.ts.map +0 -1
  206. package/dist/core/widget-html-mime.js +0 -7
  207. package/dist/core/widget-html-mime.js.map +0 -1
  208. package/dist/core/widget-mime.d.ts +0 -29
  209. package/dist/core/widget-mime.d.ts.map +0 -1
  210. package/dist/core/widget-mime.js +0 -41
  211. package/dist/core/widget-mime.js.map +0 -1
@@ -2,7 +2,7 @@ import { Server as McpServer } from '@modelcontextprotocol/sdk/server/index.js';
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
3
  import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
4
4
  import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourceTemplatesRequestSchema, SubscribeRequestSchema, UnsubscribeRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
5
- import { ResourceTemplate, createResource } from './resource.js';
5
+ import { Resource, ResourceTemplate, createResource } from './resource.js';
6
6
  import { buildResourceReadContentsMeta } from './widget-mcp-meta.js';
7
7
  import { getWidgetMimeType, isMcpAppMode, isOpenAiMode, getAppMode } from './app-mode.js';
8
8
  import { createLogger } from './logger.js';
@@ -16,6 +16,9 @@ import { triggerLifecycleHook } from './lifecycle.js';
16
16
  import { resolveProtocolEra, needsModernEngine, protocolVersionForEra, } from './protocol/version.js';
17
17
  import { isInputRequired } from './protocol/features/mrtr.js';
18
18
  import { extractBearerToken } from '../auth/token-validation.js';
19
+ import { SessionVisibilityStore } from './transforms/visibility/session-store.js';
20
+ import { MemorySpilloverStore } from './interceptors/spillover/memory-spillover.store.js';
21
+ import { FsSpilloverStore } from './interceptors/spillover/fs-spillover.store.js';
19
22
  /** Optional Streamable HTTP session limits from environment variables. */
20
23
  function getStreamableHttpEnvOptions() {
21
24
  const maxSessions = process.env.MCP_MAX_SESSIONS
@@ -29,15 +32,50 @@ function getStreamableHttpEnvOptions() {
29
32
  ...(sessionTimeout !== undefined && !Number.isNaN(sessionTimeout) ? { sessionTimeout } : {}),
30
33
  };
31
34
  }
35
+ /**
36
+ * Visibility and spillover key.
37
+ *
38
+ * The subject is the verified `extra.auth.subject` only. An unsigned bearer
39
+ * payload must not select a session. No transport session means no key.
40
+ *
41
+ * Anonymous and authenticated callers use disjoint prefixes, and both parts are
42
+ * encoded, so a client-supplied `mcp-session-id` cannot equal another
43
+ * principal's key.
44
+ */
45
+ export function sessionIsolationKey(transportSessionId, verifiedSubject) {
46
+ if (!transportSessionId)
47
+ return undefined;
48
+ const session = encodeURIComponent(transportSessionId);
49
+ if (!verifiedSubject)
50
+ return `anon:${session}`;
51
+ return `user:${encodeURIComponent(verifiedSubject)}:${session}`;
52
+ }
53
+ function createSpilloverStore(config) {
54
+ const spillover = config.spillover;
55
+ if (spillover?.driver === 'filesystem') {
56
+ return new FsSpilloverStore({
57
+ storageDir: spillover.storageDir,
58
+ maxSizeBytes: spillover.maxSizeBytes,
59
+ });
60
+ }
61
+ return new MemorySpilloverStore(spillover?.maxSizeBytes !== undefined ? { maxSizeBytes: spillover.maxSizeBytes } : {});
62
+ }
32
63
  /**
33
64
  * NitroStackServer - Main server class
34
65
  */
35
66
  export class NitroStackServer {
36
67
  mcpServer;
68
+ /** Process-local session for the legacy stdio peer. Not taken from the client. */
69
+ legacyStdioSessionId;
37
70
  tools = new Map();
71
+ sessionVisibilityStore;
72
+ pendingListChangedSessions = new Set();
73
+ notificationScheduled = false;
38
74
  resources = new Map();
39
75
  resourceTemplates = new Map();
40
76
  templateResources = new Map();
77
+ defaultSpilloverStore;
78
+ startTime = Date.now();
41
79
  prompts = new Map();
42
80
  modules = [];
43
81
  config;
@@ -72,12 +110,23 @@ export class NitroStackServer {
72
110
  protocolEra;
73
111
  /** Lazily constructed modern (2026-07-28) adapter (only on modern/auto). */
74
112
  modernAdapter;
113
+ /** Registered MCP catalog transforms in pipeline order */
114
+ transforms = [];
75
115
  constructor(config) {
76
116
  // Default config if not provided (e.g., when instantiated by DI container)
77
117
  this.config = config || {
78
118
  name: 'nitrostack-server',
79
119
  version: '1.0.0',
80
120
  };
121
+ this.defaultSpilloverStore = createSpilloverStore(this.config);
122
+ this.transforms = this.config.transforms ? [...this.config.transforms] : [];
123
+ const existingVisibilityTransform = this.transforms.find((t) => t.name === 'visibility');
124
+ if (existingVisibilityTransform?.store) {
125
+ this.sessionVisibilityStore = existingVisibilityTransform.store;
126
+ }
127
+ else {
128
+ this.sessionVisibilityStore = new SessionVisibilityStore();
129
+ }
81
130
  // Register itself in DI container so modules can inject the server.
82
131
  // NOTE: DIContainer is a process-wide singleton, so this assumes a single
83
132
  // NitroStackServer instance per process. Multiple instances would overwrite
@@ -90,6 +139,7 @@ export class NitroStackServer {
90
139
  serviceName: this.config.name,
91
140
  enableConsole: false, // CRITICAL: Console disabled for MCP compatibility
92
141
  });
142
+ this.sessionVisibilityStore.setLogger(this.logger);
93
143
  // Initialize task manager for MCP Tasks support
94
144
  this.taskManager = new TaskManager({
95
145
  logger: this.logger,
@@ -112,6 +162,23 @@ export class NitroStackServer {
112
162
  version: this.config.version,
113
163
  }, NitroStackServer.mcpServerOptions);
114
164
  this.setupHandlersOn(this.mcpServer);
165
+ this.registerSpilloverResourceTemplate();
166
+ for (const transform of this.transforms) {
167
+ this.noteVisibilityBoundary(transform);
168
+ transform.onRegister?.(this);
169
+ }
170
+ }
171
+ /**
172
+ * Modern HTTP must present a session id this process minted before
173
+ * tools/list or tools/call. Visibility is not applied to a missing header.
174
+ */
175
+ noteVisibilityBoundary(transform) {
176
+ if (transform.name !== 'visibility')
177
+ return;
178
+ if (this.protocolEra !== 'auto' && this.protocolEra !== 'modern')
179
+ return;
180
+ this.logger.warn('VisibilityTransform requires a server-issued session on the modern HTTP path. ' +
181
+ 'tools/list and tools/call without a session issued by this process are rejected.');
115
182
  }
116
183
  /**
117
184
  * Build the read-only registry view the modern protocol adapter consumes.
@@ -122,12 +189,18 @@ export class NitroStackServer {
122
189
  config: this.config,
123
190
  logger: this.logger,
124
191
  getTools: () => this.tools,
192
+ getTransformedTools: async (ctx) => {
193
+ const tools = await this.runToolPipeline(ctx);
194
+ return new Map(tools.map((t) => [t.name, t]));
195
+ },
196
+ resolveTool: (name, ctx) => this.resolveTool(name, ctx),
125
197
  getResources: () => this.resources,
126
198
  getResourceTemplates: () => this.resourceTemplates,
127
199
  getTemplateResources: () => this.templateResources,
128
200
  getPrompts: () => this.prompts,
129
201
  getTaskManager: () => this.taskManager,
130
202
  createExecutionContext: (options) => this.createContext(options),
203
+ hasSessionVisibility: () => this.transforms.some((transform) => transform.name === 'visibility'),
131
204
  };
132
205
  }
133
206
  /**
@@ -307,6 +380,98 @@ export class NitroStackServer {
307
380
  }
308
381
  return this;
309
382
  }
383
+ /**
384
+ * Register a tool with the server (alias for tool()).
385
+ */
386
+ registerTool(tool) {
387
+ return this.tool(tool);
388
+ }
389
+ /**
390
+ * Get a registered tool by name.
391
+ */
392
+ getTool(name) {
393
+ return this.tools.get(name);
394
+ }
395
+ /**
396
+ * Get all registered raw tools, keyed by name.
397
+ */
398
+ getTools() {
399
+ return new Map(this.tools);
400
+ }
401
+ /**
402
+ * Register a transform into the MCP tool catalog pipeline.
403
+ */
404
+ addTransform(transform) {
405
+ this.transforms.push(transform);
406
+ this.noteVisibilityBoundary(transform);
407
+ if (transform.name === 'visibility' && transform.store) {
408
+ this.sessionVisibilityStore = transform.store;
409
+ }
410
+ transform.onRegister?.(this);
411
+ return this;
412
+ }
413
+ /**
414
+ * Get all registered transforms in the pipeline.
415
+ */
416
+ getTransforms() {
417
+ return [...this.transforms];
418
+ }
419
+ /**
420
+ * Returns live telemetry descriptors for all active transforms in pipeline order.
421
+ */
422
+ getTransformTelemetry() {
423
+ return this.transforms.map((transform, index) => {
424
+ const customTelemetry = typeof transform.getTelemetry === 'function'
425
+ ? transform.getTelemetry()
426
+ : {};
427
+ return {
428
+ name: transform.name,
429
+ type: transform.constructor.name,
430
+ order: index,
431
+ details: customTelemetry,
432
+ };
433
+ });
434
+ }
435
+ /**
436
+ * Get server uptime in seconds.
437
+ */
438
+ getUptimeSeconds() {
439
+ return Math.floor((Date.now() - this.startTime) / 1000);
440
+ }
441
+ /**
442
+ * Runs all registered transforms sequentially on the current tools catalog.
443
+ */
444
+ async runToolPipeline(context) {
445
+ let tools = Array.from(this.tools.values());
446
+ for (const transform of this.transforms) {
447
+ if (transform.transformTools) {
448
+ try {
449
+ tools = await transform.transformTools(tools, context);
450
+ }
451
+ catch (error) {
452
+ this.logger.error(`Transform '${transform.name}' failed during transformTools`, error instanceof Error ? error : new Error(String(error)));
453
+ throw error;
454
+ }
455
+ }
456
+ }
457
+ return tools;
458
+ }
459
+ /**
460
+ * Resolves a tool by traversing the transforms in onion-order.
461
+ */
462
+ async resolveTool(name, context) {
463
+ const dispatch = async (i, toolName, ctx) => {
464
+ if (i >= this.transforms.length) {
465
+ return this.tools.get(toolName);
466
+ }
467
+ const transform = this.transforms[i];
468
+ if (transform.resolveTool) {
469
+ return transform.resolveTool(toolName, (nextName, nextCtx) => dispatch(i + 1, nextName, nextCtx ?? ctx), ctx);
470
+ }
471
+ return dispatch(i + 1, toolName, ctx);
472
+ };
473
+ return dispatch(0, name, context);
474
+ }
310
475
  /**
311
476
  * Register a component as an MCP resource
312
477
  */
@@ -400,6 +565,70 @@ export class NitroStackServer {
400
565
  this.logger.info(`Resource template registered: ${template.uriTemplate}`);
401
566
  return this;
402
567
  }
568
+ /**
569
+ * Initializes the built-in spillover resource template.
570
+ * Called during server initialization.
571
+ */
572
+ registerSpilloverResourceTemplate() {
573
+ const templateUri = 'resource://data-spillover/{id}';
574
+ const spilloverResource = new Resource({
575
+ uri: templateUri,
576
+ name: 'Data Spillover Storage',
577
+ title: 'Spillover Dataset Storage',
578
+ description: 'Parameterized retrieval endpoint for large tool output spillover payloads',
579
+ mimeType: 'application/json',
580
+ handler: async (uri, context) => {
581
+ // Extract ID from URI (e.g. resource://data-spillover/spill-12345)
582
+ const id = uri.split('/').pop() || uri.replace('resource://data-spillover/', '');
583
+ const record = await this.defaultSpilloverStore.get(id);
584
+ if (!record) {
585
+ throw new ResourceNotFoundError(uri);
586
+ }
587
+ const visibilityOn = this.transforms.some((transform) => transform.name === 'visibility');
588
+ // With visibility installed, a row that has no session is not readable.
589
+ // A row written for a session is readable only by that isolation key.
590
+ if (visibilityOn && !record.sessionId) {
591
+ throw new ResourceNotFoundError(uri);
592
+ }
593
+ if (record.sessionId && record.sessionId !== context.sessionId) {
594
+ throw new ResourceNotFoundError(uri);
595
+ }
596
+ if (record.mimeType === 'application/json') {
597
+ return {
598
+ type: 'json',
599
+ data: JSON.parse(record.data),
600
+ };
601
+ }
602
+ if (record.mimeType === 'application/octet-stream') {
603
+ return {
604
+ type: 'binary',
605
+ data: Buffer.from(record.data, 'base64'),
606
+ };
607
+ }
608
+ return {
609
+ type: 'text',
610
+ data: record.data,
611
+ };
612
+ },
613
+ });
614
+ this.resource(spilloverResource);
615
+ }
616
+ /**
617
+ * Get the default spillover store used for payload offloading.
618
+ */
619
+ getSpilloverStore() {
620
+ return this.defaultSpilloverStore;
621
+ }
622
+ /**
623
+ * Set the default spillover store used for payload offloading.
624
+ */
625
+ setSpilloverStore(store) {
626
+ if (this.defaultSpilloverStore && this.defaultSpilloverStore !== store) {
627
+ this.defaultSpilloverStore.dispose().catch(() => { });
628
+ }
629
+ this.defaultSpilloverStore = store;
630
+ return this;
631
+ }
403
632
  /**
404
633
  * Notify clients that the list of resources has changed
405
634
  */
@@ -436,20 +665,91 @@ export class NitroStackServer {
436
665
  }
437
666
  }
438
667
  /**
439
- * Notify clients that the list of tools has changed
668
+ * Get the session visibility store
440
669
  */
441
- notifyToolsListChanged() {
442
- this.modernAdapter?.notifyToolsListChanged();
670
+ getSessionVisibilityStore() {
671
+ return this.sessionVisibilityStore;
672
+ }
673
+ /** True when a VisibilityTransform is in the pipeline. */
674
+ hasSessionVisibility() {
675
+ return this.transforms.some((transform) => transform.name === 'visibility');
676
+ }
677
+ /**
678
+ * Dispatches notifications/tools/list_changed.
679
+ * If sessionId is provided, targets the specific session; if omitted, broadcasts globally.
680
+ * Coalesced via queueMicrotask to avoid duplicate notifications in a single tick.
681
+ */
682
+ notifyToolsListChanged(sessionId) {
683
+ if (sessionId) {
684
+ this.pendingListChangedSessions.add(sessionId);
685
+ }
686
+ else {
687
+ this.pendingListChangedSessions.add('*'); // Global broadcast flag
688
+ }
689
+ if (!this.notificationScheduled) {
690
+ this.notificationScheduled = true;
691
+ queueMicrotask(() => this.flushListChangedNotifications());
692
+ }
693
+ }
694
+ async flushListChangedNotifications() {
695
+ this.notificationScheduled = false;
696
+ const targetSessions = new Set(this.pendingListChangedSessions);
697
+ this.pendingListChangedSessions.clear();
698
+ const isGlobal = targetSessions.has('*');
699
+ // 1. Modern Protocol Adapter (2026-07-28)
700
+ if (this.modernAdapter) {
701
+ const modernTargets = isGlobal ? [undefined] : [...targetSessions];
702
+ for (const target of modernTargets) {
703
+ try {
704
+ this.modernAdapter.notifyToolsListChanged(target);
705
+ }
706
+ catch (err) {
707
+ this.logger.error('Failed to dispatch modern notifyToolsListChanged', { error: String(err) });
708
+ }
709
+ }
710
+ }
711
+ // 2. Legacy HTTP+SSE Sessions (2025-06-18)
712
+ if (isGlobal) {
713
+ for (const [sid, session] of this.legacySdkSseSessions.entries()) {
714
+ this.sendSessionNotification(session.server, 'notifications/tools/list_changed', sid);
715
+ }
716
+ }
717
+ else {
718
+ for (const sid of targetSessions) {
719
+ const session = this.legacySdkSseSessions.get(sid);
720
+ if (session) {
721
+ this.sendSessionNotification(session.server, 'notifications/tools/list_changed', sid);
722
+ }
723
+ }
724
+ }
725
+ // 3. Legacy stdio (the standalone McpServer instance). Its single peer has a
726
+ // process-local session; a change for any other session is not its concern.
727
+ const stdioTargeted = this.legacyStdioSessionId !== undefined && targetSessions.has(this.legacyStdioSessionId);
728
+ if (isGlobal || stdioTargeted) {
729
+ try {
730
+ const serverWithNotify = this.mcpServer;
731
+ if (serverWithNotify.notification) {
732
+ await serverWithNotify.notification({ method: 'notifications/tools/list_changed' });
733
+ }
734
+ }
735
+ catch {
736
+ /* ignore if stdio client is disconnected */
737
+ }
738
+ }
739
+ }
740
+ sendSessionNotification(server, method, sessionId) {
443
741
  try {
444
- const mcpServerWithNotification = this.mcpServer;
445
- if (mcpServerWithNotification.notification) {
446
- mcpServerWithNotification.notification({ method: 'notifications/tools/list_changed' })
447
- .catch(err => this.logger.error('Failed to send tools list changed notification', { error: err instanceof Error ? err.message : String(err) }));
742
+ if (typeof server?.notification === 'function') {
743
+ server.notification({ method }).catch((err) => {
744
+ this.logger.debug('Failed to deliver session list_changed notification', {
745
+ sessionId,
746
+ error: err instanceof Error ? err.message : String(err),
747
+ });
748
+ });
448
749
  }
449
750
  }
450
- catch (error) {
451
- const errorMessage = error instanceof Error ? error.message : String(error);
452
- this.logger.error('Error sending tools list changed notification', { error: errorMessage });
751
+ catch (err) {
752
+ this.logger.debug('Error dispatching session notification', { sessionId, error: String(err) });
453
753
  }
454
754
  }
455
755
  /**
@@ -553,7 +853,7 @@ export class NitroStackServer {
553
853
  /**
554
854
  * Create execution context
555
855
  */
556
- createContext(options) {
856
+ createContext(options, sessionContext) {
557
857
  const metadata = options?.metadata || {};
558
858
  let auth = options?.extra?.auth;
559
859
  if (!auth) {
@@ -584,25 +884,105 @@ export class NitroStackServer {
584
884
  };
585
885
  }
586
886
  }
887
+ const extra = options?.extra;
888
+ // Transport session wins. extra is applied below, but must not replace this
889
+ // value — a client-supplied extra.sessionId would otherwise read another session.
890
+ const transportSessionId = sessionContext?.sessionId || extra?.sessionId;
891
+ // Verified subject only. The unsigned bearer decode above fills `auth` for
892
+ // existing consumers and must not choose the isolation key.
893
+ const verifiedSubject = typeof extra?.auth?.subject === 'string' ? extra.auth.subject : undefined;
894
+ const sessionId = sessionIsolationKey(transportSessionId, verifiedSubject);
895
+ const enableTools = async (names) => {
896
+ if (!sessionId) {
897
+ this.logger.warn('ctx.enableTools called without an active sessionId; no-op');
898
+ return;
899
+ }
900
+ // Validation: Warn on unregistered tools
901
+ for (const name of names) {
902
+ if (!this.tools.has(name)) {
903
+ this.logger.warn(`ctx.enableTools: tool '${name}' is not registered in the catalog`);
904
+ }
905
+ }
906
+ if (verifiedSubject) {
907
+ this.sessionVisibilityStore.enableSubject(verifiedSubject, names);
908
+ }
909
+ this.sessionVisibilityStore.enableTools(sessionId, names);
910
+ if (transportSessionId)
911
+ this.notifyToolsListChanged(transportSessionId);
912
+ };
913
+ const disableTools = async (names) => {
914
+ if (!sessionId) {
915
+ this.logger.warn('ctx.disableTools called without an active sessionId; no-op');
916
+ return;
917
+ }
918
+ this.sessionVisibilityStore.disableTools(sessionId, names);
919
+ if (verifiedSubject) {
920
+ this.sessionVisibilityStore.disableSubject(verifiedSubject, names);
921
+ }
922
+ if (transportSessionId)
923
+ this.notifyToolsListChanged(transportSessionId);
924
+ };
925
+ const getVisibleTools = () => {
926
+ if (!sessionId)
927
+ return undefined;
928
+ const session = this.sessionVisibilityStore.getSession(sessionId);
929
+ const subjectRestricted = verifiedSubject
930
+ ? this.sessionVisibilityStore.hasSubjectDenies(verifiedSubject)
931
+ : false;
932
+ if (!session && !this.sessionVisibilityStore.hasRevocations(sessionId) && !subjectRestricted) {
933
+ return undefined;
934
+ }
935
+ const allowed = new Set();
936
+ for (const [name, tool] of this.tools.entries()) {
937
+ if (verifiedSubject && this.sessionVisibilityStore.hasSubjectDisabled(verifiedSubject, name)) {
938
+ continue;
939
+ }
940
+ if (this.sessionVisibilityStore.hasDisabled(sessionId, name))
941
+ continue;
942
+ if (session?.enabledTools.has(name) || tool.visibility !== 'hidden') {
943
+ allowed.add(name);
944
+ }
945
+ }
946
+ return allowed;
947
+ };
587
948
  return {
949
+ // Additive 2026-07-28 fields (protocolVersion, requestState, inputResponses,
950
+ // trace, clientInfo, clientCapabilities, auth) supplied by the modern adapter.
951
+ ...(extra || {}),
588
952
  logger: this.logger,
589
953
  requestId: uuidv4(),
590
- toolName: options?.toolName,
954
+ toolName: options?.toolName ?? extra?.toolName,
591
955
  metadata,
592
- auth,
593
- // Additive 2026-07-28 fields (protocolVersion, requestState, inputResponses,
594
- // trace, clientInfo, clientCapabilities, auth) supplied by the modern adapter.
595
- ...(options?.extra || {}),
956
+ auth: extra?.auth ?? auth,
957
+ sessionId,
958
+ verifiedSubject,
959
+ enableTools,
960
+ disableTools,
961
+ getVisibleTools,
596
962
  };
597
963
  }
964
+ /**
965
+ * Build an execution context (alias for createContext()).
966
+ */
967
+ createExecutionContext(options, sessionContext) {
968
+ return this.createContext(options, sessionContext);
969
+ }
598
970
  /**
599
971
  * Register MCP protocol handlers on the given server instance (main or per legacy SSE session).
600
972
  */
601
973
  setupHandlersOn(mcp, sessionContext) {
974
+ // The standalone server only ever serves the stdio peer, whose session starts at connect.
975
+ const sessionId = () => sessionContext?.sessionId ?? (mcp === this.mcpServer ? this.legacyStdioSessionId : undefined);
602
976
  // List tools
603
977
  mcp.setRequestHandler(ListToolsRequestSchema, async () => {
604
978
  this.logger.debug('Listing tools');
605
- const tools = await Promise.all(Array.from(this.tools.values()).map((tool) => tool.toMcpTool()));
979
+ const context = this.createContext({
980
+ extra: {
981
+ sessionId: sessionId(),
982
+ },
983
+ });
984
+ const rawTools = await this.runToolPipeline(context);
985
+ const tools = await Promise.all(rawTools.map((tool) => tool.toMcpTool()));
606
986
  return {
607
987
  tools,
608
988
  };
@@ -610,28 +990,7 @@ export class NitroStackServer {
610
990
  // Call tool
611
991
  mcp.setRequestHandler(CallToolRequestSchema, async (request) => {
612
992
  const { name, arguments: args } = request.params;
613
- const tool = this.tools.get(name);
614
- if (!tool) {
615
- throw new ToolExecutionError(name, new Error('Tool not found'));
616
- }
617
- // ----------------------------------------------------------------
618
- // MCP Tasks: detect task-augmented requests
619
- // The client sends `task: { ttl?: number }` in params to request
620
- // async task execution.
621
- // ----------------------------------------------------------------
622
993
  const requestParams = request.params;
623
- const taskParam = requestParams['task'];
624
- const isTaskAugmented = taskParam !== undefined;
625
- // Enforce tool-level task support negotiation
626
- if (isTaskAugmented && tool.taskSupport === 'forbidden') {
627
- throw {
628
- code: -32601,
629
- message: `Tool '${name}' does not support task augmentation`,
630
- };
631
- }
632
- if (!isTaskAugmented && tool.taskSupport === 'required') {
633
- throw new TaskAugmentationRequiredError();
634
- }
635
994
  // Extract _meta from request params (MCP spec) and from arguments
636
995
  // (legacy Studio clients); params._meta takes precedence.
637
996
  const argsRecord = (args || {});
@@ -654,8 +1013,33 @@ export class NitroStackServer {
654
1013
  }
655
1014
  const context = this.createContext({
656
1015
  metadata: combinedMeta,
657
- toolName: name
1016
+ toolName: name,
1017
+ extra: {
1018
+ sessionId: sessionId(),
1019
+ },
658
1020
  });
1021
+ // Resolve tool through the pipeline with context
1022
+ const tool = await this.resolveTool(name, context);
1023
+ if (!tool) {
1024
+ throw new ToolExecutionError(name, new Error(`Tool '${name}' not found`));
1025
+ }
1026
+ // ----------------------------------------------------------------
1027
+ // MCP Tasks: detect task-augmented requests
1028
+ // The client sends `task: { ttl?: number }` in params to request
1029
+ // async task execution.
1030
+ // ----------------------------------------------------------------
1031
+ const taskParam = requestParams['task'];
1032
+ const isTaskAugmented = taskParam !== undefined;
1033
+ // Enforce tool-level task support negotiation
1034
+ if (isTaskAugmented && tool.taskSupport === 'forbidden') {
1035
+ throw {
1036
+ code: -32601,
1037
+ message: `Tool '${name}' does not support task augmentation`,
1038
+ };
1039
+ }
1040
+ if (!isTaskAugmented && tool.taskSupport === 'required') {
1041
+ throw new TaskAugmentationRequiredError();
1042
+ }
659
1043
  // ----------------------------------------------------------------
660
1044
  // Task-augmented path: create task, run async, return immediately
661
1045
  // ----------------------------------------------------------------
@@ -900,7 +1284,9 @@ export class NitroStackServer {
900
1284
  if (!resource) {
901
1285
  throw new ResourceNotFoundError(uri);
902
1286
  }
903
- const context = this.createContext();
1287
+ const context = this.createContext({
1288
+ extra: { sessionId: sessionId() },
1289
+ });
904
1290
  try {
905
1291
  const content = await resource.fetch(context, uri);
906
1292
  let responseContent;
@@ -994,7 +1380,9 @@ export class NitroStackServer {
994
1380
  if (!prompt) {
995
1381
  throw new PromptNotFoundError(name);
996
1382
  }
997
- const context = this.createContext();
1383
+ const context = this.createContext({
1384
+ extra: { sessionId: sessionId() },
1385
+ });
998
1386
  try {
999
1387
  const result = await prompt.execute(args || {}, context);
1000
1388
  this.stats.promptExecutions++;
@@ -1087,7 +1475,8 @@ export class NitroStackServer {
1087
1475
  await this.configureTransportForProtocol(httpTransport);
1088
1476
  // Set up tools callback and server config for documentation page
1089
1477
  httpTransport.setToolsCallback(async () => {
1090
- const tools = await Promise.all(Array.from(this.tools.values()).map((tool) => tool.toMcpTool()));
1478
+ const rawTools = await this.runToolPipeline();
1479
+ const tools = await Promise.all(rawTools.map((tool) => tool.toMcpTool()));
1091
1480
  return tools;
1092
1481
  });
1093
1482
  httpTransport.setServerConfig({
@@ -1172,6 +1561,7 @@ export class NitroStackServer {
1172
1561
  await (await this.getModernAdapter()).serveStdio();
1173
1562
  }
1174
1563
  else {
1564
+ this.legacyStdioSessionId ??= uuidv4();
1175
1565
  const stdioTransport = new StdioServerTransport();
1176
1566
  await this.mcpServer.connect(stdioTransport);
1177
1567
  }
@@ -1198,7 +1588,8 @@ export class NitroStackServer {
1198
1588
  await this.configureTransportForProtocol(transport);
1199
1589
  // Set up tools callback and server config for documentation page
1200
1590
  transport.setToolsCallback(async () => {
1201
- const tools = await Promise.all(Array.from(this.tools.values()).map((tool) => tool.toMcpTool()));
1591
+ const rawTools = await this.runToolPipeline();
1592
+ const tools = await Promise.all(rawTools.map((tool) => tool.toMcpTool()));
1202
1593
  return tools;
1203
1594
  });
1204
1595
  transport.setServerConfig({
@@ -1225,6 +1616,7 @@ export class NitroStackServer {
1225
1616
  await (await this.getModernAdapter()).serveStdio();
1226
1617
  }
1227
1618
  else {
1619
+ this.legacyStdioSessionId ??= uuidv4();
1228
1620
  const transport = new StdioServerTransport();
1229
1621
  await this.mcpServer.connect(transport);
1230
1622
  }
@@ -1459,6 +1851,20 @@ export class NitroStackServer {
1459
1851
  }
1460
1852
  // Destroy task manager (stops cleanup interval)
1461
1853
  this.taskManager.destroy();
1854
+ // Destroy session visibility store (stops cleanup interval)
1855
+ this.sessionVisibilityStore.destroy();
1856
+ // Dispose transforms (terminates sandbox worker threads)
1857
+ for (const transform of this.transforms) {
1858
+ try {
1859
+ await transform.dispose?.();
1860
+ }
1861
+ catch (error) {
1862
+ const errorMessage = error instanceof Error ? error.message : String(error);
1863
+ this.logger.error(`Error disposing transform ${transform.name}`, { error: errorMessage });
1864
+ }
1865
+ }
1866
+ // Dispose default spillover store (stops sweep interval)
1867
+ await this.defaultSpilloverStore.dispose();
1462
1868
  // Close the modern protocol adapter (aborts in-flight modern exchanges).
1463
1869
  if (this.modernAdapter) {
1464
1870
  await this.modernAdapter.close();