@silkweave/mcp 2.6.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.mjs CHANGED
@@ -1,241 +1,26 @@
1
- import { intro } from "@clack/prompts";
2
- import { Client } from "@modelcontextprotocol/sdk/client";
3
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4
- import { LoggingMessageNotificationSchema, ProgressNotificationSchema, isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
5
- import { createCLILogger, createLogger } from "@silkweave/logger";
6
- import { capitalCase, kebabCase, pascalCase } from "change-case";
7
- import { Command } from "commander";
8
- import { randomUUID } from "crypto";
9
- import { SilkweaveError, createContext, isStreamingAction, runStreamingAction } from "@silkweave/core";
10
- import { randomUUID as randomUUID$1 } from "node:crypto";
1
+ import { i as authStorage, n as requestFromExtra, r as authMiddleware, t as registerTools } from "./registerTools-DlguElKV.mjs";
2
+ import { a as smartToolResult, i as parseResourceMessage, n as handleToolError, r as jsonToolResult, t as errorToolResult } from "./result-B_9Eo1ey.mjs";
11
3
  import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
4
+ import { createContext } from "@silkweave/core";
12
5
  import express from "express";
13
- import { generateProtectedResourceMetadata, validateToken } from "@silkweave/auth";
14
- import { AsyncLocalStorage } from "node:async_hooks";
6
+ import { generateProtectedResourceMetadata } from "@silkweave/auth";
15
7
  import cors from "cors";
16
8
  import { readFile, writeFile } from "fs/promises";
17
- import { InMemoryEventStore } from "@modelcontextprotocol/sdk/examples/shared/inMemoryEventStore.js";
18
9
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
19
10
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
20
11
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
21
- //#region src/util/result.ts
22
- function smartToolResult(data) {
23
- const text = typeof data === "string" ? data : JSON.stringify(data);
24
- const mimeType = typeof data === "string" ? "text/plain" : "application/json";
25
- const ext = typeof data === "string" ? "txt" : "json";
26
- if (text.length > 4096) {
27
- const uri = `mcp://toolResult/${randomUUID$1()}.${ext}`;
28
- const buffer = Buffer.from(text);
29
- const blob = buffer.toString("base64");
30
- return { content: [{
31
- type: "text",
32
- text: `Received resource ${uri} with ${buffer.byteLength} bytes`
33
- }, {
34
- type: "resource",
35
- resource: {
36
- uri,
37
- mimeType,
38
- blob
39
- }
40
- }] };
41
- } else return { content: [{
42
- type: "text",
43
- text
44
- }] };
45
- }
46
- function jsonToolResult(data, isError = false) {
47
- const result = { content: [{
48
- type: "text",
49
- text: JSON.stringify(data)
50
- }] };
51
- if (isError) result.isError = true;
52
- return result;
53
- }
54
- function errorToolResult({ code, name, message }) {
55
- return {
56
- isError: true,
57
- content: [{
58
- type: "text",
59
- text: JSON.stringify({
60
- success: false,
61
- code,
62
- name,
63
- message
64
- })
65
- }]
66
- };
67
- }
68
- function handleToolError(error) {
69
- if (error instanceof SilkweaveError) return jsonToolResult({
70
- success: false,
71
- name: error.name,
72
- message: error.message,
73
- code: error.code
74
- }, true);
75
- else if (error instanceof Error) {
76
- console.error(error);
77
- return jsonToolResult({
78
- success: false,
79
- name: error.name,
80
- message: error.message
81
- }, true);
82
- } else {
83
- console.error("Unknown tool error:", error);
84
- return jsonToolResult({
85
- success: false,
86
- name: "Unknown error",
87
- message: "An unknown error occurred"
88
- }, true);
89
- }
90
- }
91
- function parseResourceMessage({ resource }) {
92
- return "blob" in resource ? Buffer.from(resource.blob, "base64").toString("utf-8") : resource.text;
93
- }
94
- //#endregion
95
- //#region src/adapter/cliProxy.ts
96
- function coerce(value, type) {
97
- if (value === void 0) return;
98
- if (type === "number" || type === "integer") {
99
- const n = Number(value);
100
- return Number.isNaN(n) ? value : n;
101
- }
102
- return value;
103
- }
104
- function addCliOption(command, key, prop) {
105
- const flag = kebabCase(key);
106
- const description = prop.description;
107
- const defaultValue = prop.default;
108
- const type = prop.type;
109
- if (type === "boolean") {
110
- command.option(`--${flag}`, description, defaultValue);
111
- command.option(`--no-${flag}`);
112
- return;
113
- }
114
- if (type === "number" || type === "integer") {
115
- command.option(`--${flag} <number>`, description, defaultValue);
116
- return;
117
- }
118
- if (type === "string" || prop.enum) {
119
- command.option(`--${flag} <string>`, description, defaultValue);
120
- return;
121
- }
122
- if (type === "object" || type === "array") {
123
- command.option(`--${flag} <json>`, description ?? "", JSON.parse, defaultValue);
124
- return;
125
- }
126
- throw new Error(`Unsupported JSON Schema type for CLI option "${key}": ${type ?? "undefined"}`);
127
- }
128
- const defaultFormatter = (message) => {
129
- if (message.type === "text" && !message.text.includes("mcp://toolResult/")) return `${message.text}`;
130
- else if (message.type === "resource") return parseResourceMessage(message);
131
- else return JSON.stringify(message);
132
- };
133
- const cliProxy = ({ url, formatter = defaultFormatter }) => {
134
- return (options, baseContext) => {
135
- const context = baseContext.fork({ adapter: "cliProxy" });
136
- const program = new Command().name(options.name).description(options.description).version(options.version).option("-s, --silent", "Silent mode, prevent log messages", false);
137
- return {
138
- context,
139
- start: async () => {
140
- const client = new Client({
141
- name: options.name,
142
- description: options.description,
143
- version: options.version
144
- });
145
- const transport = new StreamableHTTPClientTransport(url);
146
- await client.connect(transport);
147
- const { tools } = await client.listTools();
148
- for (const tool of tools) {
149
- const name = kebabCase(tool.name);
150
- const command = program.command(name);
151
- if (tool.description) command.description(tool.description);
152
- const properties = tool.inputSchema.properties ?? {};
153
- for (const key of Object.keys(properties)) addCliOption(command, key, properties[key]);
154
- command.action(async (args) => {
155
- const { silent } = program.opts();
156
- const logger = createCLILogger();
157
- if (!silent) {
158
- intro(`${options.name} - ${tool.name}`);
159
- client.setNotificationHandler(LoggingMessageNotificationSchema, ({ params: { level, data } }) => {
160
- logger[level](data);
161
- });
162
- client.setNotificationHandler(ProgressNotificationSchema, ({ params: { progress, total, message } }) => {
163
- logger.info({
164
- progress,
165
- total,
166
- message
167
- });
168
- });
169
- }
170
- const input = {};
171
- for (const key of Object.keys(properties)) {
172
- const value = args[key];
173
- if (value !== void 0) input[key] = coerce(value, properties[key]?.type);
174
- }
175
- (await client.callTool({
176
- name: tool.name,
177
- arguments: input,
178
- _meta: {
179
- progressToken: randomUUID(),
180
- disposition: "json"
181
- }
182
- })).content.forEach((message, index, messages) => {
183
- const text = formatter(message, index, messages);
184
- process.stdout.write(`${text}\n`);
185
- });
186
- });
187
- }
188
- await program.parseAsync();
189
- await transport.close();
190
- },
191
- stop: async () => {}
192
- };
193
- };
194
- };
195
- //#endregion
196
- //#region src/handlers/auth.ts
197
- /**
198
- * Per-request bearer-token storage used by tool handlers to read the resolved
199
- * `AuthInfo` for the currently-handled MCP call.
200
- */
201
- const authStorage = new AsyncLocalStorage();
202
- /**
203
- * Express middleware that validates the `Authorization: Bearer …` header via
204
- * the supplied `AuthConfig`. On success the resolved `AuthInfo` is placed in
205
- * `authStorage` for the duration of the downstream handler - `mcpTransport`'s
206
- * tool callbacks pick it up to populate the silkweave context's `auth` key.
207
- *
208
- * The middleware should NOT be applied to OAuth-discovery / token routes -
209
- * compose it only on the routes that require an authenticated caller (the MCP
210
- * transport itself, sideload, etc.).
211
- */
212
- function authMiddleware(auth, context) {
213
- return async (req, res, next) => {
214
- const result = await validateToken(req.headers.authorization, auth, context.fork({ request: req }));
215
- if (result.error) {
216
- for (const [key, value] of Object.entries(result.error.headers)) res.header(key, value);
217
- res.status(result.error.statusCode).json(result.error.body);
218
- return;
219
- }
220
- if (result.auth) authStorage.run(result.auth, () => {
221
- next();
222
- });
223
- else next();
224
- };
225
- }
226
- //#endregion
12
+ import { randomUUID } from "crypto";
227
13
  //#region src/handlers/cors.ts
228
14
  /** Headers required by the MCP protocol that must always be exposed when CORS is in use. */
229
15
  const MCP_REQUIRED_HEADERS = [
230
16
  "WWW-Authenticate",
231
- "Mcp-Session-Id",
232
17
  "Last-Event-Id",
233
18
  "Mcp-Protocol-Version"
234
19
  ];
235
20
  /**
236
21
  * CORS middleware preconfigured to expose the headers MCP clients need
237
- * (`Mcp-Session-Id`, `Last-Event-Id`, `Mcp-Protocol-Version`, `WWW-Authenticate`)
238
- * on top of any user-supplied options.
22
+ * (`Last-Event-Id`, `Mcp-Protocol-Version`, `WWW-Authenticate`) on top of any
23
+ * user-supplied options. (Stateless transport - no `Mcp-Session-Id`.)
239
24
  *
240
25
  * Pass `false` to disable, omit / pass `true` for permissive defaults, or pass
241
26
  * a `CorsOptions` object to override.
@@ -260,7 +45,7 @@ function mcpCors(corsConfig = true) {
260
45
  */
261
46
  function protectedResourceMetadata(auth) {
262
47
  if (!auth.resourceUrl || !auth.authorizationServers?.length) throw new Error("@silkweave/mcp protectedResourceMetadata(): auth.resourceUrl and auth.authorizationServers are required");
263
- const metadata = generateProtectedResourceMetadata(auth.resourceUrl, auth.authorizationServers);
48
+ const metadata = generateProtectedResourceMetadata(auth.resourceUrl, auth.authorizationServers, auth.requiredScopes);
264
49
  return (_req, res) => {
265
50
  res.json(metadata);
266
51
  };
@@ -334,85 +119,6 @@ function sideloadResource(options = {}) {
334
119
  }
335
120
  //#endregion
336
121
  //#region src/handlers/transport.ts
337
- /**
338
- * Build the generic `request` context value (the same key REST/tRPC populate)
339
- * from the MCP SDK's `extra.requestInfo`, so transport-agnostic consumers - e.g.
340
- * `@silkweave/nestjs` `@UseGuards` guards reading
341
- * `switchToHttp().getRequest().headers` - work over MCP. There is no path
342
- * `params`/`query` on an MCP call, so those are empty stand-ins for graceful
343
- * degradation. Returns `undefined` when no HTTP request info is available.
344
- */
345
- function requestFromExtra(requestInfo) {
346
- if (!requestInfo) return;
347
- return {
348
- headers: requestInfo.headers ?? {},
349
- url: requestInfo.url?.toString(),
350
- params: {},
351
- query: {}
352
- };
353
- }
354
- function registerTools$1(server, actions, context) {
355
- for (const action of actions) server.registerTool(pascalCase(action.name), {
356
- title: capitalCase(action.name),
357
- description: action.description,
358
- inputSchema: action.input
359
- }, async (input, extra) => {
360
- const logger = createLogger({
361
- stream: process.stderr,
362
- onLog: (level, data) => {
363
- extra.sendNotification({
364
- method: "notifications/message",
365
- params: {
366
- level,
367
- data
368
- }
369
- });
370
- },
371
- onProgress: ({ progress, total, message }) => {
372
- if (!extra._meta?.progressToken) return;
373
- extra.sendNotification({
374
- method: "notifications/progress",
375
- params: {
376
- progress,
377
- total,
378
- message,
379
- progressToken: extra._meta.progressToken
380
- }
381
- });
382
- }
383
- });
384
- const currentAuth = authStorage.getStore();
385
- const actionContext = context.fork({
386
- logger,
387
- extra,
388
- request: requestFromExtra(extra.requestInfo),
389
- ...currentAuth ? { auth: currentAuth } : {}
390
- });
391
- const disposition = extra._meta?.disposition ?? action.disposition;
392
- const progressToken = extra._meta?.progressToken;
393
- try {
394
- let result;
395
- if (isStreamingAction(action)) result = await runStreamingAction(action, input, actionContext, progressToken ? async (chunk, index) => {
396
- await extra.sendNotification({
397
- method: "notifications/progress",
398
- params: {
399
- progressToken,
400
- progress: index + 1,
401
- message: JSON.stringify(chunk)
402
- }
403
- });
404
- } : void 0);
405
- else result = await action.run(input, actionContext);
406
- if (action.toolResult) {
407
- const response = action.toolResult(result, actionContext);
408
- if (response) return response;
409
- }
410
- return disposition === "json" ? jsonToolResult(result) : smartToolResult(result);
411
- } catch (error) {
412
- return handleToolError(error);
413
- }
414
- });
415
- }
416
122
  function createMcpServer(options, actions, context) {
417
123
  const server = new McpServer({
418
124
  name: options.name,
@@ -422,56 +128,28 @@ function createMcpServer(options, actions, context) {
422
128
  tools: {},
423
129
  logging: {}
424
130
  } });
425
- registerTools$1(server, actions, context);
131
+ registerTools(server, actions, context);
426
132
  return server;
427
133
  }
428
- function createSessionTransport(transports) {
429
- const transport = new StreamableHTTPServerTransport({
430
- sessionIdGenerator: () => randomUUID(),
431
- enableJsonResponse: false,
432
- eventStore: new InMemoryEventStore(),
433
- onsessioninitialized: (sId) => {
434
- transports[sId] = transport;
435
- }
436
- });
437
- transport.onerror = (error) => {
438
- console.error(error);
439
- };
440
- transport.onclose = () => {
441
- const sid = transport.sessionId;
442
- if (sid && transports[sid]) delete transports[sid];
443
- };
444
- return transport;
445
- }
446
134
  /**
447
- * Build the MCP Streamable HTTP transport route handlers.
135
+ * Build the MCP Streamable HTTP transport route handler.
448
136
  *
449
- * Sessions are kept in a per-call closure (one map per `mcpTransport()` call),
450
- * so the same handler object should be registered for all three transport
451
- * routes - `POST /mcp`, `GET /mcp`, `DELETE /mcp` - to share session state.
137
+ * Stateless (per the 2026 spec direction): each `POST /mcp` mints a fresh
138
+ * transport + server with `sessionIdGenerator: undefined`, handles exactly that
139
+ * request (streaming progress over SSE when the call carries a `progressToken`),
140
+ * and tears down on response close. No `Mcp-Session-Id`, no session map, no
141
+ * `GET`/`DELETE` reconnect - any request can hit any instance.
452
142
  */
453
143
  function mcpTransport(silkweaveOptions, context, actions) {
454
- const transports = {};
455
144
  const post = async (req, res) => {
456
- const sessionId = req.headers["mcp-session-id"];
457
145
  try {
458
- if (sessionId && transports[sessionId]) {
459
- await transports[sessionId].handleRequest(req, res, req.body);
460
- return;
461
- }
462
- if (!isInitializeRequest(req.body)) {
463
- res.status(404).json({
464
- jsonrpc: "2.0",
465
- error: {
466
- code: -32e3,
467
- message: "Session not found"
468
- },
469
- id: null
470
- });
471
- return;
472
- }
473
- const transport = createSessionTransport(transports);
474
- await createMcpServer(silkweaveOptions, actions, context).connect(transport);
146
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
147
+ const server = createMcpServer(silkweaveOptions, actions, context);
148
+ res.on("close", () => {
149
+ transport.close();
150
+ server.close();
151
+ });
152
+ await server.connect(transport);
475
153
  await transport.handleRequest(req, res, req.body);
476
154
  } catch (error) {
477
155
  console.error("Error handling MCP request:", error);
@@ -485,18 +163,7 @@ function mcpTransport(silkweaveOptions, context, actions) {
485
163
  });
486
164
  }
487
165
  };
488
- const stream = async (req, res) => {
489
- const sessionId = req.headers["mcp-session-id"];
490
- if (!sessionId || !transports[sessionId]) {
491
- res.status(400).send("Invalid or missing session ID");
492
- return;
493
- }
494
- await transports[sessionId].handleRequest(req, res);
495
- };
496
- return {
497
- post,
498
- stream
499
- };
166
+ return { post };
500
167
  }
501
168
  //#endregion
502
169
  //#region src/adapter/http.ts
@@ -542,9 +209,6 @@ function buildMcpExpressApp(silkweaveOptions, context, actions, options) {
542
209
  if (sideloadResources) app.get("/resource/:id", sideloadResource({ resourceDir }));
543
210
  const transport = mcpTransport(silkweaveOptions, context, actions);
544
211
  app.post("/mcp", express.json(), transport.post);
545
- app.get("/mcp", transport.stream);
546
- app.delete("/mcp", transport.stream);
547
- app.get("/mcp/resource/:id", transport.stream);
548
212
  return app;
549
213
  }
550
214
  /**
@@ -601,60 +265,6 @@ const http = (options) => {
601
265
  };
602
266
  //#endregion
603
267
  //#region src/adapter/stdio.ts
604
- function registerTools(server, actions, context) {
605
- for (const action of actions) server.registerTool(pascalCase(action.name), {
606
- title: capitalCase(action.name),
607
- description: action.description,
608
- inputSchema: action.input
609
- }, async (input, extra) => {
610
- const logger = createLogger({
611
- stream: false,
612
- onLog: (level, data) => {
613
- extra.sendNotification({
614
- method: "notifications/message",
615
- params: {
616
- level,
617
- data
618
- }
619
- });
620
- },
621
- onProgress: ({ progress, total, message }) => {
622
- if (!extra._meta?.progressToken) return;
623
- extra.sendNotification({
624
- method: "notifications/progress",
625
- params: {
626
- progress,
627
- total,
628
- message,
629
- progressToken: extra._meta.progressToken
630
- }
631
- });
632
- }
633
- });
634
- const actionContext = context.fork({
635
- logger,
636
- extra
637
- });
638
- const progressToken = extra._meta?.progressToken;
639
- try {
640
- let result;
641
- if (isStreamingAction(action)) result = await runStreamingAction(action, input, actionContext, progressToken ? async (chunk, index) => {
642
- await extra.sendNotification({
643
- method: "notifications/progress",
644
- params: {
645
- progressToken,
646
- progress: index + 1,
647
- message: JSON.stringify(chunk)
648
- }
649
- });
650
- } : void 0);
651
- else result = await action.run(input, actionContext);
652
- return action.toolResult?.(result, actionContext) ?? smartToolResult(result);
653
- } catch (error) {
654
- return handleToolError(error);
655
- }
656
- });
657
- }
658
268
  const stdio = () => {
659
269
  return (options, baseContext) => {
660
270
  const context = baseContext.fork({ adapter: "stdio" });
@@ -669,7 +279,7 @@ const stdio = () => {
669
279
  return {
670
280
  context,
671
281
  start: async (actions) => {
672
- registerTools(server, actions, context);
282
+ registerTools(server, actions, context, { logStream: false });
673
283
  const transport = new StdioServerTransport();
674
284
  await server.connect(transport);
675
285
  },
@@ -693,6 +303,6 @@ async function createSideloadResource(buffer, { name, contentType }) {
693
303
  return resource;
694
304
  }
695
305
  //#endregion
696
- export { MCP_REQUIRED_HEADERS, authMiddleware, authStorage, buildMcpExpressApp, cliProxy, createSideloadResource, errorToolResult, handleToolError, http, jsonToolResult, mcpCors, mcpTransport, oauthRoutes, parseResourceMessage, protectedResourceMetadata, sideloadResource, smartToolResult, startMcpServer, stdio };
306
+ export { MCP_REQUIRED_HEADERS, authMiddleware, authStorage, buildMcpExpressApp, createSideloadResource, errorToolResult, handleToolError, http, jsonToolResult, mcpCors, mcpTransport, oauthRoutes, parseResourceMessage, protectedResourceMetadata, registerTools, requestFromExtra, sideloadResource, smartToolResult, startMcpServer, stdio };
697
307
 
698
308
  //# sourceMappingURL=index.mjs.map