@silkweave/mcp 1.8.0 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +8 -11
- package/.turbo/turbo-check.log +3 -13
- package/.turbo/turbo-lint.log +1 -4
- package/.turbo/turbo-prepack.log +10 -19
- package/README.md +31 -0
- package/build/index.d.mts +127 -6
- package/build/index.d.mts.map +1 -1
- package/build/index.mjs +309 -133
- package/build/index.mjs.map +1 -1
- package/package.json +5 -4
- package/tsconfig.tsbuildinfo +1 -1
package/build/index.mjs
CHANGED
|
@@ -2,22 +2,21 @@ import { intro } from "@clack/prompts";
|
|
|
2
2
|
import { Client } from "@modelcontextprotocol/sdk/client";
|
|
3
3
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
4
4
|
import { LoggingMessageNotificationSchema, ProgressNotificationSchema, isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
-
import { SilkweaveError, unwrap } from "@silkweave/core";
|
|
6
5
|
import { createCLILogger, createLogger } from "@silkweave/logger";
|
|
7
6
|
import { capitalCase, kebabCase, pascalCase } from "change-case";
|
|
8
7
|
import { Command } from "commander";
|
|
9
8
|
import { randomUUID } from "crypto";
|
|
10
|
-
import {
|
|
9
|
+
import { SilkweaveError, createContext, isStreamingAction, runStreamingAction } from "@silkweave/core";
|
|
11
10
|
import { randomUUID as randomUUID$1 } from "node:crypto";
|
|
12
|
-
import { InMemoryEventStore } from "@modelcontextprotocol/sdk/examples/shared/inMemoryEventStore.js";
|
|
13
11
|
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
14
|
-
import
|
|
15
|
-
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
12
|
+
import express from "express";
|
|
16
13
|
import { generateProtectedResourceMetadata, validateToken } from "@silkweave/auth";
|
|
14
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
17
15
|
import cors from "cors";
|
|
18
|
-
import express from "express";
|
|
19
16
|
import { readFile, writeFile } from "fs/promises";
|
|
20
|
-
import {
|
|
17
|
+
import { InMemoryEventStore } from "@modelcontextprotocol/sdk/examples/shared/inMemoryEventStore.js";
|
|
18
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
19
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
21
20
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
22
21
|
//#region src/util/result.ts
|
|
23
22
|
function smartToolResult(data) {
|
|
@@ -91,16 +90,37 @@ function parseResourceMessage({ resource }) {
|
|
|
91
90
|
}
|
|
92
91
|
//#endregion
|
|
93
92
|
//#region src/adapter/cliProxy.ts
|
|
94
|
-
function
|
|
95
|
-
|
|
93
|
+
function coerce(value, type) {
|
|
94
|
+
if (value === void 0) return;
|
|
95
|
+
if (type === "number" || type === "integer") {
|
|
96
|
+
const n = Number(value);
|
|
97
|
+
return Number.isNaN(n) ? value : n;
|
|
98
|
+
}
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
function addCliOption(command, key, prop) {
|
|
96
102
|
const flag = kebabCase(key);
|
|
97
|
-
|
|
103
|
+
const description = prop.description;
|
|
104
|
+
const defaultValue = prop.default;
|
|
105
|
+
const type = prop.type;
|
|
106
|
+
if (type === "boolean") {
|
|
98
107
|
command.option(`--${flag}`, description, defaultValue);
|
|
99
108
|
command.option(`--no-${flag}`);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (type === "number" || type === "integer") {
|
|
112
|
+
command.option(`--${flag} <number>`, description, defaultValue);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (type === "string" || prop.enum) {
|
|
116
|
+
command.option(`--${flag} <string>`, description, defaultValue);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (type === "object" || type === "array") {
|
|
120
|
+
command.option(`--${flag} <json>`, description ?? "", JSON.parse, defaultValue);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
throw new Error(`Unsupported JSON Schema type for CLI option "${key}": ${type ?? "undefined"}`);
|
|
104
124
|
}
|
|
105
125
|
const defaultFormatter = (message) => {
|
|
106
126
|
if (message.type === "text" && !message.text.includes("mcp://toolResult/")) return `${message.text}`;
|
|
@@ -126,13 +146,8 @@ const cliProxy = ({ url, formatter = defaultFormatter }) => {
|
|
|
126
146
|
const name = kebabCase(tool.name);
|
|
127
147
|
const command = program.command(name);
|
|
128
148
|
if (tool.description) command.description(tool.description);
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
const shape = schema.shape;
|
|
132
|
-
for (const key of Object.keys(shape)) {
|
|
133
|
-
const [type, { defaultValue }] = unwrap(shape[key]);
|
|
134
|
-
addCliOption(command, key, type, defaultValue);
|
|
135
|
-
}
|
|
149
|
+
const properties = tool.inputSchema.properties ?? {};
|
|
150
|
+
for (const key of Object.keys(properties)) addCliOption(command, key, properties[key]);
|
|
136
151
|
command.action(async (args) => {
|
|
137
152
|
const { silent } = program.opts();
|
|
138
153
|
const logger = createCLILogger();
|
|
@@ -149,7 +164,11 @@ const cliProxy = ({ url, formatter = defaultFormatter }) => {
|
|
|
149
164
|
});
|
|
150
165
|
});
|
|
151
166
|
}
|
|
152
|
-
const input =
|
|
167
|
+
const input = {};
|
|
168
|
+
for (const key of Object.keys(properties)) {
|
|
169
|
+
const value = args[key];
|
|
170
|
+
if (value !== void 0) input[key] = coerce(value, properties[key]?.type);
|
|
171
|
+
}
|
|
153
172
|
(await client.callTool({
|
|
154
173
|
name: tool.name,
|
|
155
174
|
arguments: input,
|
|
@@ -171,67 +190,147 @@ const cliProxy = ({ url, formatter = defaultFormatter }) => {
|
|
|
171
190
|
};
|
|
172
191
|
};
|
|
173
192
|
//#endregion
|
|
174
|
-
//#region src/
|
|
193
|
+
//#region src/handlers/auth.ts
|
|
194
|
+
/**
|
|
195
|
+
* Per-request bearer-token storage used by tool handlers to read the resolved
|
|
196
|
+
* `AuthInfo` for the currently-handled MCP call.
|
|
197
|
+
*/
|
|
175
198
|
const authStorage = new AsyncLocalStorage();
|
|
176
|
-
/**
|
|
199
|
+
/**
|
|
200
|
+
* Express middleware that validates the `Authorization: Bearer …` header via
|
|
201
|
+
* the supplied `AuthConfig`. On success the resolved `AuthInfo` is placed in
|
|
202
|
+
* `authStorage` for the duration of the downstream handler - `mcpTransport`'s
|
|
203
|
+
* tool callbacks pick it up to populate the silkweave context's `auth` key.
|
|
204
|
+
*
|
|
205
|
+
* The middleware should NOT be applied to OAuth-discovery / token routes -
|
|
206
|
+
* compose it only on the routes that require an authenticated caller (the MCP
|
|
207
|
+
* transport itself, sideload, etc.).
|
|
208
|
+
*/
|
|
209
|
+
function authMiddleware(auth, context) {
|
|
210
|
+
return async (req, res, next) => {
|
|
211
|
+
const result = await validateToken(req.headers.authorization, auth, context.fork({ request: req }));
|
|
212
|
+
if (result.error) {
|
|
213
|
+
for (const [key, value] of Object.entries(result.error.headers)) res.header(key, value);
|
|
214
|
+
res.status(result.error.statusCode).json(result.error.body);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (result.auth) authStorage.run(result.auth, () => {
|
|
218
|
+
next();
|
|
219
|
+
});
|
|
220
|
+
else next();
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
//#endregion
|
|
224
|
+
//#region src/handlers/cors.ts
|
|
225
|
+
/** Headers required by the MCP protocol that must always be exposed when CORS is in use. */
|
|
177
226
|
const MCP_REQUIRED_HEADERS = [
|
|
178
227
|
"WWW-Authenticate",
|
|
179
228
|
"Mcp-Session-Id",
|
|
180
229
|
"Last-Event-Id",
|
|
181
230
|
"Mcp-Protocol-Version"
|
|
182
231
|
];
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
232
|
+
/**
|
|
233
|
+
* CORS middleware preconfigured to expose the headers MCP clients need
|
|
234
|
+
* (`Mcp-Session-Id`, `Last-Event-Id`, `Mcp-Protocol-Version`, `WWW-Authenticate`)
|
|
235
|
+
* on top of any user-supplied options.
|
|
236
|
+
*
|
|
237
|
+
* Pass `false` to disable, omit / pass `true` for permissive defaults, or pass
|
|
238
|
+
* a `CorsOptions` object to override.
|
|
239
|
+
*/
|
|
240
|
+
function mcpCors(corsConfig = true) {
|
|
241
|
+
if (corsConfig === false) return null;
|
|
242
|
+
const userConfig = corsConfig === true ? {} : corsConfig;
|
|
243
|
+
const userExposed = userConfig.exposedHeaders;
|
|
244
|
+
const exposedHeaders = [...MCP_REQUIRED_HEADERS, ...Array.isArray(userExposed) ? userExposed : userExposed ? [userExposed] : []];
|
|
245
|
+
return cors({
|
|
246
|
+
origin: "*",
|
|
247
|
+
...userConfig,
|
|
248
|
+
exposedHeaders
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
//#endregion
|
|
252
|
+
//#region src/handlers/metadata.ts
|
|
253
|
+
/**
|
|
254
|
+
* Handler for `GET /.well-known/oauth-protected-resource` (RFC 9728). Returns
|
|
255
|
+
* the resource server's metadata pointing at the configured authorization
|
|
256
|
+
* servers. Requires `auth.resourceUrl` and a non-empty `auth.authorizationServers`.
|
|
257
|
+
*/
|
|
258
|
+
function protectedResourceMetadata(auth) {
|
|
259
|
+
if (!auth.resourceUrl || !auth.authorizationServers?.length) throw new Error("@silkweave/mcp protectedResourceMetadata(): auth.resourceUrl and auth.authorizationServers are required");
|
|
260
|
+
const metadata = generateProtectedResourceMetadata(auth.resourceUrl, auth.authorizationServers);
|
|
261
|
+
return (_req, res) => {
|
|
262
|
+
res.json(metadata);
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
//#endregion
|
|
266
|
+
//#region src/handlers/oauth.ts
|
|
267
|
+
function toOAuthReq(req) {
|
|
268
|
+
return {
|
|
187
269
|
method: req.method,
|
|
188
270
|
url: new URL(req.url, `${req.protocol}://${req.get("host")}`),
|
|
189
271
|
headers: Object.fromEntries(Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v[0] : v])),
|
|
190
272
|
body: req.body
|
|
191
|
-
});
|
|
192
|
-
const sendOAuth = (res, oauthRes) => {
|
|
193
|
-
for (const [key, value] of Object.entries(oauthRes.headers)) res.header(key, value);
|
|
194
|
-
if (oauthRes.body) res.status(oauthRes.status).send(typeof oauthRes.body === "string" ? oauthRes.body : JSON.stringify(oauthRes.body));
|
|
195
|
-
else res.status(oauthRes.status).end();
|
|
196
273
|
};
|
|
197
|
-
app.get("/.well-known/oauth-authorization-server", (_req, res) => {
|
|
198
|
-
sendOAuth(res, provider.metadata());
|
|
199
|
-
});
|
|
200
|
-
app.get("/authorize", async (req, res) => {
|
|
201
|
-
sendOAuth(res, await provider.authorize(toOAuthReq(req)));
|
|
202
|
-
});
|
|
203
|
-
app.get(callbackPath, async (req, res) => {
|
|
204
|
-
sendOAuth(res, await provider.callback(toOAuthReq(req)));
|
|
205
|
-
});
|
|
206
|
-
app.post("/token", express.urlencoded({ extended: false }), async (req, res) => {
|
|
207
|
-
sendOAuth(res, await provider.token(toOAuthReq(req)));
|
|
208
|
-
});
|
|
209
|
-
app.post("/register", express.json(), async (req, res) => {
|
|
210
|
-
sendOAuth(res, await provider.register(toOAuthReq(req)));
|
|
211
|
-
});
|
|
212
|
-
return new Set([
|
|
213
|
-
"/.well-known/oauth-authorization-server",
|
|
214
|
-
"/authorize",
|
|
215
|
-
callbackPath,
|
|
216
|
-
"/token",
|
|
217
|
-
"/register"
|
|
218
|
-
]);
|
|
219
274
|
}
|
|
220
|
-
function
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
275
|
+
function sendOAuth(res, oauthRes) {
|
|
276
|
+
for (const [key, value] of Object.entries(oauthRes.headers)) res.header(key, value);
|
|
277
|
+
if (oauthRes.body) res.status(oauthRes.status).send(typeof oauthRes.body === "string" ? oauthRes.body : JSON.stringify(oauthRes.body));
|
|
278
|
+
else res.status(oauthRes.status).end();
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Build the OAuth 2.1 proxy route handlers (authorize, callback, token,
|
|
282
|
+
* register, well-known) backed by the configured `auth.provider`. Returns the
|
|
283
|
+
* handlers as individual `RequestHandler`s so callers can register them
|
|
284
|
+
* wherever they like - under `/mcp`, at the server root, etc.
|
|
285
|
+
*
|
|
286
|
+
* `token` and `register` are returned as middleware arrays because the
|
|
287
|
+
* appropriate body parser must run before the handler. Apply with
|
|
288
|
+
* `app.post(path, ...token)`.
|
|
289
|
+
*/
|
|
290
|
+
function oauthRoutes(auth) {
|
|
291
|
+
if (!auth.provider) throw new Error("@silkweave/mcp oauthRoutes(): auth.provider is required");
|
|
292
|
+
const provider = auth.provider;
|
|
293
|
+
return {
|
|
294
|
+
callbackPath: auth.callbackPath ?? "/auth/callback",
|
|
295
|
+
wellKnownAuthServer: (_req, res) => {
|
|
296
|
+
sendOAuth(res, provider.metadata());
|
|
297
|
+
},
|
|
298
|
+
authorize: async (req, res) => {
|
|
299
|
+
sendOAuth(res, await provider.authorize(toOAuthReq(req)));
|
|
300
|
+
},
|
|
301
|
+
callback: async (req, res) => {
|
|
302
|
+
sendOAuth(res, await provider.callback(toOAuthReq(req)));
|
|
303
|
+
},
|
|
304
|
+
token: [express.urlencoded({ extended: false }), async (req, res) => {
|
|
305
|
+
sendOAuth(res, await provider.token(toOAuthReq(req)));
|
|
306
|
+
}],
|
|
307
|
+
register: [express.json(), async (req, res) => {
|
|
308
|
+
sendOAuth(res, await provider.register(toOAuthReq(req)));
|
|
309
|
+
}]
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
//#endregion
|
|
313
|
+
//#region src/handlers/sideload.ts
|
|
314
|
+
/**
|
|
315
|
+
* Handler for `GET /resource/:id` - serves a large MCP response that was
|
|
316
|
+
* sideloaded to disk as a `{id}` payload with a `{id}.json` metadata sidecar.
|
|
317
|
+
*
|
|
318
|
+
* The route param `id` is provided by the host framework (Express, Nest, etc.).
|
|
319
|
+
*/
|
|
320
|
+
function sideloadResource(options = {}) {
|
|
321
|
+
const { resourceDir = "resources" } = options;
|
|
322
|
+
return async (req, res) => {
|
|
323
|
+
const id = req.params["id"];
|
|
324
|
+
if (!id || typeof id !== "string") throw new Error("Invalid ID");
|
|
325
|
+
const resourceMeta = JSON.parse(await readFile(`${resourceDir}/${id}.json`, "utf-8"));
|
|
326
|
+
const buffer = await readFile(`${resourceDir}/${id}`);
|
|
327
|
+
res.status(200);
|
|
328
|
+
res.header("Content-Type", resourceMeta.contentType);
|
|
329
|
+
res.send(buffer);
|
|
330
|
+
};
|
|
234
331
|
}
|
|
332
|
+
//#endregion
|
|
333
|
+
//#region src/handlers/transport.ts
|
|
235
334
|
function registerTools$1(server, actions, context) {
|
|
236
335
|
for (const action of actions) server.registerTool(pascalCase(action.name), {
|
|
237
336
|
title: capitalCase(action.name),
|
|
@@ -269,14 +368,28 @@ function registerTools$1(server, actions, context) {
|
|
|
269
368
|
...currentAuth ? { auth: currentAuth } : {}
|
|
270
369
|
});
|
|
271
370
|
const disposition = extra._meta?.disposition;
|
|
272
|
-
|
|
371
|
+
const progressToken = extra._meta?.progressToken;
|
|
372
|
+
try {
|
|
373
|
+
let result;
|
|
374
|
+
if (isStreamingAction(action)) result = await runStreamingAction(action, input, actionContext, progressToken ? async (chunk, index) => {
|
|
375
|
+
await extra.sendNotification({
|
|
376
|
+
method: "notifications/progress",
|
|
377
|
+
params: {
|
|
378
|
+
progressToken,
|
|
379
|
+
progress: index + 1,
|
|
380
|
+
message: JSON.stringify(chunk)
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
} : void 0);
|
|
384
|
+
else result = await action.run(input, actionContext);
|
|
273
385
|
if (action.toolResult) {
|
|
274
386
|
const response = action.toolResult(result, actionContext);
|
|
275
387
|
if (response) return response;
|
|
276
388
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
389
|
+
return disposition === "json" ? jsonToolResult(result) : smartToolResult(result);
|
|
390
|
+
} catch (error) {
|
|
391
|
+
return handleToolError(error);
|
|
392
|
+
}
|
|
280
393
|
});
|
|
281
394
|
}
|
|
282
395
|
function createMcpServer(options, actions, context) {
|
|
@@ -297,7 +410,6 @@ function createSessionTransport(transports) {
|
|
|
297
410
|
enableJsonResponse: false,
|
|
298
411
|
eventStore: new InMemoryEventStore(),
|
|
299
412
|
onsessioninitialized: (sId) => {
|
|
300
|
-
console.log(`Session initialized with ID: ${sId}`);
|
|
301
413
|
transports[sId] = transport;
|
|
302
414
|
}
|
|
303
415
|
});
|
|
@@ -306,15 +418,20 @@ function createSessionTransport(transports) {
|
|
|
306
418
|
};
|
|
307
419
|
transport.onclose = () => {
|
|
308
420
|
const sid = transport.sessionId;
|
|
309
|
-
if (sid && transports[sid])
|
|
310
|
-
console.log(`Transport closed for session ${sid}, removing from transports map`);
|
|
311
|
-
delete transports[sid];
|
|
312
|
-
}
|
|
421
|
+
if (sid && transports[sid]) delete transports[sid];
|
|
313
422
|
};
|
|
314
423
|
return transport;
|
|
315
424
|
}
|
|
316
|
-
|
|
317
|
-
|
|
425
|
+
/**
|
|
426
|
+
* Build the MCP Streamable HTTP transport route handlers.
|
|
427
|
+
*
|
|
428
|
+
* Sessions are kept in a per-call closure (one map per `mcpTransport()` call),
|
|
429
|
+
* so the same handler object should be registered for all three transport
|
|
430
|
+
* routes - `POST /mcp`, `GET /mcp`, `DELETE /mcp` - to share session state.
|
|
431
|
+
*/
|
|
432
|
+
function mcpTransport(silkweaveOptions, context, actions) {
|
|
433
|
+
const transports = {};
|
|
434
|
+
const post = async (req, res) => {
|
|
318
435
|
const sessionId = req.headers["mcp-session-id"];
|
|
319
436
|
try {
|
|
320
437
|
if (sessionId && transports[sessionId]) {
|
|
@@ -333,7 +450,7 @@ function mountMcpTransport(app, transports, createServer) {
|
|
|
333
450
|
return;
|
|
334
451
|
}
|
|
335
452
|
const transport = createSessionTransport(transports);
|
|
336
|
-
await
|
|
453
|
+
await createMcpServer(silkweaveOptions, actions, context).connect(transport);
|
|
337
454
|
await transport.handleRequest(req, res, req.body);
|
|
338
455
|
} catch (error) {
|
|
339
456
|
console.error("Error handling MCP request:", error);
|
|
@@ -346,8 +463,8 @@ function mountMcpTransport(app, transports, createServer) {
|
|
|
346
463
|
id: null
|
|
347
464
|
});
|
|
348
465
|
}
|
|
349
|
-
}
|
|
350
|
-
const
|
|
466
|
+
};
|
|
467
|
+
const stream = async (req, res) => {
|
|
351
468
|
const sessionId = req.headers["mcp-session-id"];
|
|
352
469
|
if (!sessionId || !transports[sessionId]) {
|
|
353
470
|
res.status(400).send("Invalid or missing session ID");
|
|
@@ -355,64 +472,106 @@ function mountMcpTransport(app, transports, createServer) {
|
|
|
355
472
|
}
|
|
356
473
|
await transports[sessionId].handleRequest(req, res);
|
|
357
474
|
};
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
475
|
+
return {
|
|
476
|
+
post,
|
|
477
|
+
stream
|
|
478
|
+
};
|
|
361
479
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
480
|
+
//#endregion
|
|
481
|
+
//#region src/adapter/http.ts
|
|
482
|
+
/**
|
|
483
|
+
* Build a fully-wired Express app that exposes the MCP Streamable HTTP
|
|
484
|
+
* transport (plus OAuth / sideload / well-known routes when configured).
|
|
485
|
+
*
|
|
486
|
+
* Pass the resulting `app` to `app.listen(port, host)` yourself, or use the
|
|
487
|
+
* top-level `startMcpServer()` / `http()` adapter conveniences.
|
|
488
|
+
*/
|
|
489
|
+
function buildMcpExpressApp(silkweaveOptions, context, actions, options) {
|
|
490
|
+
const { host, auth, cors: corsConfig, sideloadResources = true, resourceDir, ...mcpAppOptions } = options;
|
|
491
|
+
const app = createMcpExpressApp({
|
|
492
|
+
...mcpAppOptions,
|
|
493
|
+
host
|
|
494
|
+
});
|
|
495
|
+
const corsHandler = mcpCors(corsConfig ?? true);
|
|
496
|
+
if (corsHandler) app.use(corsHandler);
|
|
497
|
+
if (auth?.authorizationServers?.length && auth.resourceUrl) app.get("/.well-known/oauth-protected-resource", protectedResourceMetadata(auth));
|
|
498
|
+
let oauthPaths = /* @__PURE__ */ new Set();
|
|
499
|
+
if (auth?.provider) {
|
|
500
|
+
const oauth = oauthRoutes(auth);
|
|
501
|
+
app.get("/.well-known/oauth-authorization-server", oauth.wellKnownAuthServer);
|
|
502
|
+
app.get("/authorize", oauth.authorize);
|
|
503
|
+
app.get(oauth.callbackPath, oauth.callback);
|
|
504
|
+
app.post("/token", ...oauth.token);
|
|
505
|
+
app.post("/register", ...oauth.register);
|
|
506
|
+
oauthPaths = new Set([
|
|
507
|
+
"/.well-known/oauth-authorization-server",
|
|
508
|
+
"/authorize",
|
|
509
|
+
oauth.callbackPath,
|
|
510
|
+
"/token",
|
|
511
|
+
"/register"
|
|
512
|
+
]);
|
|
513
|
+
}
|
|
514
|
+
if (auth) {
|
|
515
|
+
const guard = authMiddleware(auth, context);
|
|
516
|
+
app.use((req, res, next) => {
|
|
517
|
+
if (req.path.startsWith("/.well-known/") || oauthPaths.has(req.path)) return next();
|
|
518
|
+
return guard(req, res, next);
|
|
382
519
|
});
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
520
|
+
}
|
|
521
|
+
if (sideloadResources) app.get("/resource/:id", sideloadResource({ resourceDir }));
|
|
522
|
+
const transport = mcpTransport(silkweaveOptions, context, actions);
|
|
523
|
+
app.post("/mcp", express.json(), transport.post);
|
|
524
|
+
app.get("/mcp", transport.stream);
|
|
525
|
+
app.delete("/mcp", transport.stream);
|
|
526
|
+
app.get("/mcp/resource/:id", transport.stream);
|
|
527
|
+
return app;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Spin up a standalone MCP Streamable HTTP server on `host:port` for the
|
|
531
|
+
* given `actions`. Returns the underlying `Server` so callers can close it.
|
|
532
|
+
*
|
|
533
|
+
* Convenience for use cases that don't go through the `silkweave()` builder.
|
|
534
|
+
*/
|
|
535
|
+
async function startMcpServer(silkweaveOptions, actions, options, context) {
|
|
536
|
+
const app = buildMcpExpressApp(silkweaveOptions, context ?? createContext({ adapter: "http" }), actions, options);
|
|
537
|
+
return new Promise((resolve, reject) => {
|
|
538
|
+
const server = app.listen(options.port, options.host, (error) => {
|
|
539
|
+
if (error) {
|
|
540
|
+
reject(error);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
console.log(`MCP Streamable HTTP Server listening on http://${options.host}:${options.port}/mcp`);
|
|
544
|
+
resolve(server);
|
|
395
545
|
});
|
|
396
|
-
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Silkweave adapter that owns its own HTTP server. Composes the MCP handler
|
|
550
|
+
* primitives into a fully-wired Express app and listens on `host:port`.
|
|
551
|
+
*/
|
|
552
|
+
const http = (options) => {
|
|
553
|
+
return (silkweaveOptions, baseContext) => {
|
|
554
|
+
const context = baseContext.fork({ adapter: "http" });
|
|
397
555
|
let httpServer;
|
|
398
556
|
return {
|
|
399
557
|
context,
|
|
400
558
|
start: async (actions) => {
|
|
401
|
-
|
|
402
|
-
httpServer =
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
559
|
+
const app = buildMcpExpressApp(silkweaveOptions, context, actions, options);
|
|
560
|
+
httpServer = await new Promise((resolve, reject) => {
|
|
561
|
+
const s = app.listen(options.port, options.host, (error) => {
|
|
562
|
+
if (error) {
|
|
563
|
+
reject(error);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
console.log(`MCP Streamable HTTP Server listening on http://${options.host}:${options.port}/mcp`);
|
|
567
|
+
resolve(s);
|
|
568
|
+
});
|
|
408
569
|
});
|
|
409
570
|
},
|
|
410
571
|
stop: async () => {
|
|
411
|
-
if (httpServer)
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
else resolve();
|
|
415
|
-
});
|
|
572
|
+
if (!httpServer) return;
|
|
573
|
+
await new Promise((resolve, reject) => {
|
|
574
|
+
httpServer.close((err) => err ? reject(err) : resolve());
|
|
416
575
|
});
|
|
417
576
|
httpServer = void 0;
|
|
418
577
|
}
|
|
@@ -455,7 +614,24 @@ function registerTools(server, actions, context) {
|
|
|
455
614
|
logger,
|
|
456
615
|
extra
|
|
457
616
|
});
|
|
458
|
-
|
|
617
|
+
const progressToken = extra._meta?.progressToken;
|
|
618
|
+
try {
|
|
619
|
+
let result;
|
|
620
|
+
if (isStreamingAction(action)) result = await runStreamingAction(action, input, actionContext, progressToken ? async (chunk, index) => {
|
|
621
|
+
await extra.sendNotification({
|
|
622
|
+
method: "notifications/progress",
|
|
623
|
+
params: {
|
|
624
|
+
progressToken,
|
|
625
|
+
progress: index + 1,
|
|
626
|
+
message: JSON.stringify(chunk)
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
} : void 0);
|
|
630
|
+
else result = await action.run(input, actionContext);
|
|
631
|
+
return action.toolResult?.(result, actionContext) ?? smartToolResult(result);
|
|
632
|
+
} catch (error) {
|
|
633
|
+
return handleToolError(error);
|
|
634
|
+
}
|
|
459
635
|
});
|
|
460
636
|
}
|
|
461
637
|
const stdio = () => {
|
|
@@ -496,6 +672,6 @@ async function createSideloadResource(buffer, { name, contentType }) {
|
|
|
496
672
|
return resource;
|
|
497
673
|
}
|
|
498
674
|
//#endregion
|
|
499
|
-
export { cliProxy, createSideloadResource, errorToolResult, handleToolError, http, jsonToolResult, parseResourceMessage, smartToolResult, stdio };
|
|
675
|
+
export { MCP_REQUIRED_HEADERS, authMiddleware, authStorage, buildMcpExpressApp, cliProxy, createSideloadResource, errorToolResult, handleToolError, http, jsonToolResult, mcpCors, mcpTransport, oauthRoutes, parseResourceMessage, protectedResourceMetadata, sideloadResource, smartToolResult, startMcpServer, stdio };
|
|
500
676
|
|
|
501
677
|
//# sourceMappingURL=index.mjs.map
|