@silkweave/mcp 3.0.0 → 3.2.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/README.md +96 -8
- package/build/cliProxy.d.mts.map +1 -1
- package/build/cliProxy.mjs +4 -5
- package/build/cliProxy.mjs.map +1 -1
- package/build/index.d.mts +29 -5
- package/build/index.d.mts.map +1 -1
- package/build/index.mjs +32 -12
- package/build/index.mjs.map +1 -1
- package/build/{registerTools-DlguElKV.mjs → registerTools-DkaraOyx.mjs} +120 -9
- package/build/registerTools-DkaraOyx.mjs.map +1 -0
- package/build/result-BN0lxM6Q.d.mts +136 -0
- package/build/result-BN0lxM6Q.d.mts.map +1 -0
- package/build/{result-B_9Eo1ey.mjs → result-CXgeE8ca.mjs} +19 -2
- package/build/result-CXgeE8ca.mjs.map +1 -0
- package/build/tools.d.mts +2 -2
- package/build/tools.mjs +3 -3
- package/package.json +3 -9
- package/build/registerTools-DlguElKV.mjs.map +0 -1
- package/build/result-BVpEX7jU.d.mts +0 -64
- package/build/result-BVpEX7jU.d.mts.map +0 -1
- package/build/result-B_9Eo1ey.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -59,10 +59,56 @@ Exposes a single stateless `POST /mcp` (each call mints a fresh transport with `
|
|
|
59
59
|
| `port` | `number` | Listen port |
|
|
60
60
|
| `allowedHosts` | `string[]` | Allowed hosts for DNS rebinding protection |
|
|
61
61
|
| `cors` | `CorsOptions \| boolean` | CORS config. `false` to disable, `true`/omit for permissive defaults (`origin: '*'`), or a [cors](https://www.npmjs.com/package/cors) options object. MCP-required headers are always exposed. |
|
|
62
|
+
| `filterActions` | `FilterActions` | Per-request tool filter - see [Per-request tool filtering](#per-request-tool-filtering-filteractions) |
|
|
63
|
+
| `onToolCall` | `OnToolCall` | Telemetry hook - see [Telemetry](#telemetry-ontoolcall) |
|
|
64
|
+
|
|
65
|
+
#### Per-request tool filtering (`filterActions`)
|
|
66
|
+
|
|
67
|
+
Because the transport is stateless, the tool list is recomputed on **every** request - which makes per-request scoping (per-API-key permissions, tool groups, read-only keys) a single callback:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
http({
|
|
71
|
+
host: 'localhost', port: 8080,
|
|
72
|
+
filterActions: async (actions, request) => {
|
|
73
|
+
// request: { headers, url, method, toolName? }
|
|
74
|
+
if (request.method === 'initialize' || request.method === 'ping') { return actions } // skip the DB lookup
|
|
75
|
+
const key = await lookupApiKey(request.headers.authorization)
|
|
76
|
+
if (!key) { throw new SilkweaveError('invalid api key', 'invalid_key', 401) }
|
|
77
|
+
return actions.filter((action) => action.tags?.some((tag) => key.allowedTags.includes(tag)))
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- Applies to `tools/list` **and** `tools/call` alike - a client that cached a wider list is still denied.
|
|
83
|
+
- `request.method` is the JSON-RPC method of the POSTed message; `request.toolName` is `params.name` on `tools/call`. Both double as an observability tap (e.g. counting `tools/list`).
|
|
84
|
+
- **Error semantics**: a thrown `SilkweaveError` propagates as its `statusCode` (401/403/...) with a JSON-RPC error body - SDK clients surface it as an auth failure. Any other throw maps to 500. A throw never produces an empty tool list; return `[]` explicitly if "no tools" is the intended answer.
|
|
85
|
+
- Permission changes apply on the next `tools/list` (clients refetch on reconnect) - no `listChanged` session machinery needed or offered.
|
|
86
|
+
- The same option exists on `mcpTransport()`, `@silkweave/edge`'s `edge()`, and `@silkweave/nestjs`'s `mcp()`. Actions carry optional `tags: string[]` as the natural thing to filter on.
|
|
87
|
+
|
|
88
|
+
#### Telemetry (`onToolCall`)
|
|
89
|
+
|
|
90
|
+
One hook observes every tool call - available on `stdio()`, `http()`, `mcpTransport()`, and `@silkweave/edge`'s `edge()`:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
http({
|
|
94
|
+
host: 'localhost', port: 8080,
|
|
95
|
+
onToolCall: (event) => {
|
|
96
|
+
// { action, tool, transport: 'mcp', durationMs, ok, errorCode?, errorMessage?, resultBytes?, sideloaded?, context }
|
|
97
|
+
const auth = event.context.getOptional('auth')
|
|
98
|
+
console.log(JSON.stringify({ event: 'tool_call', ...event, context: undefined, userId: auth?.userId }))
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
- **Fire-and-forget**: never awaited on the result path; sync throws and async rejections are logged and swallowed - the hook can never fail, slow, or reorder a call.
|
|
104
|
+
- Fires after result formatting, so events carry `resultBytes` (serialized raw-result size) and `sideloaded` (whether `smartToolResult` offloaded to an embedded resource).
|
|
105
|
+
- `ok` is `false` when the action threw (with `errorCode`/`errorMessage` - a `SilkweaveError`'s `code`, else the error's name) or when the formatted result is an `isError` tool result.
|
|
106
|
+
- Streaming actions report `durationMs` across full generator consumption.
|
|
107
|
+
- `@silkweave/nestjs` wires this through DI instead - `forRoot({ telemetry: MyTelemetryService })` covers MCP **and** tRPC calls.
|
|
62
108
|
|
|
63
109
|
### cliProxy
|
|
64
110
|
|
|
65
|
-
MCP CLI proxy client - connects to a running HTTP MCP server and invokes tools from the command line. Imported from the dedicated `@silkweave/mcp/cli-proxy` subpath (kept out of the package root so importing the `stdio`/`http` servers does not pull the CLI client's `commander`
|
|
111
|
+
MCP CLI proxy client - connects to a running HTTP MCP server and invokes tools from the command line. Imported from the dedicated `@silkweave/mcp/cli-proxy` subpath (kept out of the package root so importing the `stdio`/`http` servers does not pull the CLI client's `commander` into the server path). `commander` is an **optional peer dependency** - install it alongside `@silkweave/mcp` when you use the CLI proxy.
|
|
66
112
|
|
|
67
113
|
```typescript
|
|
68
114
|
import { silkweave } from '@silkweave/core'
|
|
@@ -163,21 +209,62 @@ const MyAction = createAction({
|
|
|
163
209
|
|
|
164
210
|
Return `undefined` from `toolResult` to fall through to the default `smartToolResult` behavior.
|
|
165
211
|
|
|
166
|
-
###
|
|
212
|
+
### `disposition`
|
|
167
213
|
|
|
168
|
-
|
|
214
|
+
Tool results default to compact JSON (`jsonToolResult`). Set `disposition` on the action to change the format:
|
|
169
215
|
|
|
170
216
|
```typescript
|
|
171
217
|
createAction({
|
|
172
218
|
name: 'my-action',
|
|
173
|
-
description: '
|
|
219
|
+
description: 'Sideloads large payloads to an embedded resource',
|
|
174
220
|
input: z.object({}),
|
|
175
|
-
disposition: '
|
|
221
|
+
disposition: 'smart', // 'json' (default) ⇒ jsonToolResult, 'smart' ⇒ smartToolResult
|
|
176
222
|
run: async () => fetchData()
|
|
177
223
|
})
|
|
178
224
|
```
|
|
179
225
|
|
|
180
|
-
|
|
226
|
+
`'json'` and `'smart'` are only **defaults** - a client that sends `_meta.disposition` on the tool call always wins. Resolution order: client `_meta.disposition` → action `disposition` → `'json'`. (`@silkweave/nestjs` exposes this as `@Mcp({ result })`.)
|
|
227
|
+
|
|
228
|
+
> **Breaking change in 3.2:** the fallback default flipped from `'smart'` to `'json'`. Set `disposition: 'smart'` per action (or `defaultResult: 'smart'` in `@silkweave/nestjs`) to restore payload sideloading.
|
|
229
|
+
|
|
230
|
+
### Structured output (`disposition: 'structured'`)
|
|
231
|
+
|
|
232
|
+
A structured action declares its `output` Zod schema as the tool's MCP `outputSchema` - visible to agents in `tools/list` before they call, and returned as `structuredContent`:
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
createAction({
|
|
236
|
+
name: 'users.get',
|
|
237
|
+
description: 'Get a user by id',
|
|
238
|
+
input: z.object({ id: z.string() }),
|
|
239
|
+
output: z.object({ id: z.string(), name: z.string() }),
|
|
240
|
+
disposition: 'structured',
|
|
241
|
+
run: async ({ id }) => getUser(id) // may return a wider object - extra fields are stripped
|
|
242
|
+
})
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Important semantics (the MCP SDK enforces output schemas on **both** sides - the server validates before responding and SDK clients validate independently against `tools/list` - so the schema is a hard contract, not a hint):
|
|
246
|
+
|
|
247
|
+
- The result is **parsed through `output` before shipping**: `structuredContent` is the parsed (extra-fields-stripped) data, plus a JSON text mirror in `content`. Returning a wider object than the schema is therefore safe by construction.
|
|
248
|
+
- A genuine mismatch (missing required field, wrong type) returns an **`isError` tool result** naming the failing fields - not an opaque protocol error - since `isError` results are exempt from SDK output validation.
|
|
249
|
+
- `_meta.disposition` is **ignored** for structured actions; the contract is fixed at `tools/list` time.
|
|
250
|
+
- `'structured'` requires a non-streaming action with an `output` schema - validated at registration (`validateActionDisposition()`), so misconfiguration fails at boot.
|
|
251
|
+
- Fields that can be `null` at runtime must be declared `.nullable()` (zod's `.optional()` does not accept `null`).
|
|
252
|
+
|
|
253
|
+
### Tool annotations
|
|
254
|
+
|
|
255
|
+
Every tool is registered with MCP `annotations` - behavior hints clients use to group and permission-gate tools. The registrar derives `readOnlyHint` from the action's `kind` (`'query'` ⇒ `true`, otherwise `false`) and merges the action's explicit `annotations` over that base:
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
createAction({
|
|
259
|
+
name: 'campaigns.delete',
|
|
260
|
+
description: 'Delete a campaign permanently',
|
|
261
|
+
input: z.object({ id: z.string() }),
|
|
262
|
+
annotations: { destructiveHint: true, idempotentHint: true }, // merged over { readOnlyHint: false }
|
|
263
|
+
run: async ({ id }) => remove(id)
|
|
264
|
+
})
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
All hints are advisory (`title`, `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint` - see `ToolAnnotations` in `@silkweave/core`). `@silkweave/nestjs` derives them from the HTTP verb instead (`@Get` ⇒ read-only + idempotent, `@Delete` ⇒ destructive + idempotent) with an `@Mcp({ annotations })` override.
|
|
181
268
|
|
|
182
269
|
## MCP Result Utilities
|
|
183
270
|
|
|
@@ -185,8 +272,9 @@ All result utilities are exported from `@silkweave/mcp`:
|
|
|
185
272
|
|
|
186
273
|
| Function | Description |
|
|
187
274
|
|----------|-------------|
|
|
188
|
-
| `smartToolResult(data)` |
|
|
189
|
-
| `jsonToolResult(data, isError?)` | Simple inline `TextContent` JSON (no splitting) |
|
|
275
|
+
| `smartToolResult(data)` | Formatter with automatic embedded resource splitting at 4096 chars (`disposition: 'smart'`) |
|
|
276
|
+
| `jsonToolResult(data, isError?)` | Simple inline `TextContent` JSON (no splitting) - the default formatter |
|
|
277
|
+
| `structuredToolResult(data)` | `structuredContent` + JSON text mirror for `disposition: 'structured'` actions. Pass output-schema-**parsed** data, never the raw result |
|
|
190
278
|
| `errorToolResult(error)` | Format a `SilkweaveError` as an error result |
|
|
191
279
|
| `handleToolError(error)` | Catch-all error handler used by all MCP adapters |
|
|
192
280
|
|
package/build/cliProxy.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cliProxy.d.mts","names":[],"sources":["../src/adapter/cliProxy.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"cliProxy.d.mts","names":[],"sources":["../src/adapter/cliProxy.ts"],"mappings":";;;;KASY,cAAA,IAAkB,OAAA,EAAS,YAAA,EAAc,KAAA,UAAe,QAAA,EAAU,YAAA;AAAA,UAE7D,eAAA;EACf,GAAA,EAAK,GAAA;EACL,SAAA,GAAY,cAAA;AAAA;AAAA,cA4DD,QAAA,EAAU,cAAA,CAAe,eAAA"}
|
package/build/cliProxy.mjs
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { i as parseResourceMessage } from "./result-
|
|
2
|
-
import {
|
|
1
|
+
import { i as parseResourceMessage } from "./result-CXgeE8ca.mjs";
|
|
2
|
+
import { createConsoleLogger } from "@silkweave/core";
|
|
3
3
|
import { kebabCase } from "change-case";
|
|
4
4
|
import { randomUUID } from "crypto";
|
|
5
|
-
import { intro } from "@clack/prompts";
|
|
6
5
|
import { Client } from "@modelcontextprotocol/sdk/client";
|
|
7
6
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
8
7
|
import { LoggingMessageNotificationSchema, ProgressNotificationSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
@@ -68,9 +67,9 @@ const cliProxy = ({ url, formatter = defaultFormatter }) => {
|
|
|
68
67
|
for (const key of Object.keys(properties)) addCliOption(command, key, properties[key]);
|
|
69
68
|
command.action(async (args) => {
|
|
70
69
|
const { silent } = program.opts();
|
|
71
|
-
const logger =
|
|
70
|
+
const logger = createConsoleLogger();
|
|
72
71
|
if (!silent) {
|
|
73
|
-
|
|
72
|
+
console.info(`${options.name} - ${tool.name}`);
|
|
74
73
|
client.setNotificationHandler(LoggingMessageNotificationSchema, ({ params: { level, data } }) => {
|
|
75
74
|
logger[level](data);
|
|
76
75
|
});
|
package/build/cliProxy.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cliProxy.mjs","names":[],"sources":["../src/adapter/cliProxy.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"cliProxy.mjs","names":[],"sources":["../src/adapter/cliProxy.ts"],"sourcesContent":["import { Client } from '@modelcontextprotocol/sdk/client'\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'\nimport { ContentBlock, LoggingMessageNotificationSchema, ProgressNotificationSchema, ToolResultContent } from '@modelcontextprotocol/sdk/types.js'\nimport { AdapterFactory, createConsoleLogger } from '@silkweave/core'\nimport { kebabCase } from 'change-case'\nimport { Command } from 'commander'\nimport { randomUUID } from 'crypto'\nimport { parseResourceMessage } from '../util/result.js'\n\nexport type CLIFormatterFn = (message: ContentBlock, index: number, messages: ContentBlock[]) => string | undefined\n\nexport interface CliProxyOptions {\n url: URL\n formatter?: CLIFormatterFn\n}\n\ninterface JsonSchemaProperty {\n type?: 'string' | 'number' | 'integer' | 'boolean' | 'object' | 'array' | string\n description?: string\n default?: unknown\n enum?: unknown[]\n}\n\ninterface JsonSchemaObject {\n type?: 'object'\n properties?: Record<string, JsonSchemaProperty>\n required?: string[]\n}\n\nfunction coerce(value: unknown, type: JsonSchemaProperty['type']): unknown {\n if (value === undefined) { return undefined }\n if (type === 'number' || type === 'integer') {\n const n = Number(value)\n return Number.isNaN(n) ? value : n\n }\n return value\n}\n\nfunction addCliOption(command: Command, key: string, prop: JsonSchemaProperty) {\n const flag = kebabCase(key)\n const description = prop.description\n const defaultValue = prop.default\n const type = prop.type\n if (type === 'boolean') {\n command.option(`--${flag}`, description, defaultValue as boolean | undefined)\n command.option(`--no-${flag}`)\n return\n }\n if (type === 'number' || type === 'integer') {\n command.option(`--${flag} <number>`, description, defaultValue as string | undefined)\n return\n }\n if (type === 'string' || prop.enum) {\n command.option(`--${flag} <string>`, description, defaultValue as string | undefined)\n return\n }\n if (type === 'object' || type === 'array') {\n command.option(`--${flag} <json>`, description ?? '', JSON.parse, defaultValue as never)\n return\n }\n throw new Error(`Unsupported JSON Schema type for CLI option \"${key}\": ${type ?? 'undefined'}`)\n}\n\nconst defaultFormatter: CLIFormatterFn = (message) => {\n if (message.type === 'text' && !message.text.includes('mcp://toolResult/')) {\n return `${message.text}`\n } else if (message.type === 'resource') {\n return parseResourceMessage(message)\n } else {\n return JSON.stringify(message)\n }\n}\n\nexport const cliProxy: AdapterFactory<CliProxyOptions> = ({ url, formatter = defaultFormatter }) => {\n return (options, baseContext) => {\n const context = baseContext.fork({ adapter: 'cliProxy' })\n const program = new Command()\n .name(options.name)\n .description(options.description)\n .version(options.version)\n .option('-s, --silent', 'Silent mode, prevent log messages', false)\n\n return {\n context,\n start: async () => {\n const client = new Client({\n name: options.name,\n description: options.description,\n version: options.version\n })\n const transport = new StreamableHTTPClientTransport(url)\n await client.connect(transport)\n const { tools } = await client.listTools()\n for (const tool of tools) {\n const name = kebabCase(tool.name)\n const command = program.command(name)\n if (tool.description) { command.description(tool.description) }\n const schema = tool.inputSchema as JsonSchemaObject\n const properties = schema.properties ?? {}\n for (const key of Object.keys(properties)) {\n addCliOption(command, key, properties[key])\n }\n command.action(async (args: Record<string, unknown>) => {\n const { silent } = program.opts<{ silent: boolean }>()\n const logger = createConsoleLogger()\n if (!silent) {\n console.info(`${options.name} - ${tool.name}`)\n client.setNotificationHandler(LoggingMessageNotificationSchema, ({ params: { level, data } }) => {\n logger[level](data)\n })\n client.setNotificationHandler(ProgressNotificationSchema, ({ params: { progress, total, message } }) => {\n logger.info({ progress, total, message })\n })\n }\n const input: Record<string, unknown> = {}\n for (const key of Object.keys(properties)) {\n const value = args[key]\n if (value !== undefined) { input[key] = coerce(value, properties[key]?.type) }\n }\n const response = await client.callTool({\n name: tool.name,\n arguments: input,\n _meta: { progressToken: randomUUID(), disposition: 'json' }\n }) as ToolResultContent\n response.content.forEach((message, index, messages) => {\n const text = formatter(message, index, messages)\n process.stdout.write(`${text}\\n`)\n })\n })\n }\n await program.parseAsync()\n await transport.close()\n },\n stop: async () => { /* noop */ }\n }\n }\n}\n"],"mappings":"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"}
|
package/build/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as smartToolResult, c as
|
|
1
|
+
import { a as smartToolResult, c as registerTools, d as FilterRequest, f as filterErrorResponse, i as parseResourceMessage, l as requestFromExtra, n as handleToolError, o as structuredToolResult, p as rpcInfo, r as jsonToolResult, s as RegisterToolsOptions, t as errorToolResult, u as FilterActions } from "./result-BN0lxM6Q.mjs";
|
|
2
2
|
import { CreateMcpExpressAppOptions } from "@modelcontextprotocol/sdk/server/express.js";
|
|
3
|
-
import { Action, AdapterFactory, SilkweaveContext, SilkweaveOptions } from "@silkweave/core";
|
|
3
|
+
import { Action, AdapterFactory, OnToolCall, SilkweaveContext, SilkweaveOptions } from "@silkweave/core";
|
|
4
4
|
import { Express, RequestHandler } from "express";
|
|
5
5
|
import { AuthConfig, AuthInfo } from "@silkweave/auth";
|
|
6
6
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
@@ -18,6 +18,17 @@ interface StartMcpHttpOptions extends CreateMcpExpressAppOptions {
|
|
|
18
18
|
sideloadResources?: boolean;
|
|
19
19
|
/** Directory the sideload route reads from. Default `'resources'`. */
|
|
20
20
|
resourceDir?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Per-request tool filter, applied before `registerTools()` on every
|
|
23
|
+
* `POST /mcp` (the stateless transport recomputes the tool list per request,
|
|
24
|
+
* so permission changes apply on the next `tools/list`). See `FilterActions`
|
|
25
|
+
* for the request stand-in (`headers`/`url`/`method`/`toolName`) and error
|
|
26
|
+
* semantics (a throw surfaces as its `SilkweaveError.statusCode` or 500 -
|
|
27
|
+
* never an empty tool list).
|
|
28
|
+
*/
|
|
29
|
+
filterActions?: FilterActions;
|
|
30
|
+
/** Telemetry hook invoked once per tool call (fire-and-forget). */
|
|
31
|
+
onToolCall?: OnToolCall;
|
|
21
32
|
}
|
|
22
33
|
/**
|
|
23
34
|
* Build a fully-wired Express app that exposes the MCP Streamable HTTP
|
|
@@ -41,7 +52,11 @@ declare function startMcpServer(silkweaveOptions: SilkweaveOptions, actions: Act
|
|
|
41
52
|
declare const http: AdapterFactory<StartMcpHttpOptions>;
|
|
42
53
|
//#endregion
|
|
43
54
|
//#region src/adapter/stdio.d.ts
|
|
44
|
-
|
|
55
|
+
interface StdioAdapterOptions {
|
|
56
|
+
/** Telemetry hook invoked once per tool call (fire-and-forget). */
|
|
57
|
+
onToolCall?: OnToolCall;
|
|
58
|
+
}
|
|
59
|
+
declare const stdio: AdapterFactory<StdioAdapterOptions | void>;
|
|
45
60
|
//#endregion
|
|
46
61
|
//#region src/handlers/auth.d.ts
|
|
47
62
|
/**
|
|
@@ -127,6 +142,15 @@ interface McpTransportHandlers {
|
|
|
127
142
|
/** `POST /mcp` - handle a single stateless MCP request/response (SSE per call). */
|
|
128
143
|
post: RequestHandler;
|
|
129
144
|
}
|
|
145
|
+
interface McpTransportOptions {
|
|
146
|
+
/**
|
|
147
|
+
* Per-request tool filter, applied before `registerTools()` on every POST.
|
|
148
|
+
* See `FilterActions` for the request stand-in and error semantics.
|
|
149
|
+
*/
|
|
150
|
+
filterActions?: FilterActions;
|
|
151
|
+
/** Telemetry hook invoked once per tool call (fire-and-forget). */
|
|
152
|
+
onToolCall?: OnToolCall;
|
|
153
|
+
}
|
|
130
154
|
/**
|
|
131
155
|
* Build the MCP Streamable HTTP transport route handler.
|
|
132
156
|
*
|
|
@@ -136,7 +160,7 @@ interface McpTransportHandlers {
|
|
|
136
160
|
* and tears down on response close. No `Mcp-Session-Id`, no session map, no
|
|
137
161
|
* `GET`/`DELETE` reconnect - any request can hit any instance.
|
|
138
162
|
*/
|
|
139
|
-
declare function mcpTransport(silkweaveOptions: SilkweaveOptions, context: SilkweaveContext, actions: Action[]): McpTransportHandlers;
|
|
163
|
+
declare function mcpTransport(silkweaveOptions: SilkweaveOptions, context: SilkweaveContext, actions: Action[], options?: McpTransportOptions): McpTransportHandlers;
|
|
140
164
|
//#endregion
|
|
141
165
|
//#region src/util/sideload.d.ts
|
|
142
166
|
interface SideloadResource {
|
|
@@ -150,5 +174,5 @@ declare function createSideloadResource(buffer: Buffer, {
|
|
|
150
174
|
contentType
|
|
151
175
|
}: Pick<SideloadResource, 'name' | 'contentType'>): Promise<SideloadResource>;
|
|
152
176
|
//#endregion
|
|
153
|
-
export { type CorsOptions, MCP_REQUIRED_HEADERS, McpTransportHandlers, OAuthRouteHandlers, RegisterToolsOptions, SideloadResource, SideloadResourceOptions, StartMcpHttpOptions, authMiddleware, authStorage, buildMcpExpressApp, createSideloadResource, errorToolResult, handleToolError, http, jsonToolResult, mcpCors, mcpTransport, oauthRoutes, parseResourceMessage, protectedResourceMetadata, registerTools, requestFromExtra, sideloadResource, smartToolResult, startMcpServer, stdio };
|
|
177
|
+
export { type CorsOptions, FilterActions, FilterRequest, MCP_REQUIRED_HEADERS, McpTransportHandlers, McpTransportOptions, OAuthRouteHandlers, RegisterToolsOptions, SideloadResource, SideloadResourceOptions, StartMcpHttpOptions, StdioAdapterOptions, authMiddleware, authStorage, buildMcpExpressApp, createSideloadResource, errorToolResult, filterErrorResponse, handleToolError, http, jsonToolResult, mcpCors, mcpTransport, oauthRoutes, parseResourceMessage, protectedResourceMetadata, registerTools, requestFromExtra, rpcInfo, sideloadResource, smartToolResult, startMcpServer, stdio, structuredToolResult };
|
|
154
178
|
//# sourceMappingURL=index.d.mts.map
|
package/build/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/adapter/http.ts","../src/adapter/stdio.ts","../src/handlers/auth.ts","../src/handlers/cors.ts","../src/handlers/metadata.ts","../src/handlers/oauth.ts","../src/handlers/sideload.ts","../src/handlers/transport.ts","../src/util/sideload.ts"],"mappings":";;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/adapter/http.ts","../src/adapter/stdio.ts","../src/handlers/auth.ts","../src/handlers/cors.ts","../src/handlers/metadata.ts","../src/handlers/oauth.ts","../src/handlers/sideload.ts","../src/handlers/transport.ts","../src/util/sideload.ts"],"mappings":";;;;;;;;;;UAciB,mBAAA,SAA4B,0BAAA;EAC3C,IAAA;EACA,IAAA;EACA,IAAA,GAAO,UAAA;EAH4B;EAKnC,IAAA,GAAO,aAAA;EAFA;EAIP,iBAAA;EAWgB;EAThB,WAAA;EAT2C;;;;;;;;EAkB3C,aAAA,GAAgB,aAAA;EAbT;EAeP,UAAA,GAAa,UAAA;AAAA;;;;;;;AAUf;iBAAgB,kBAAA,CACd,gBAAA,EAAkB,gBAAA,EAClB,OAAA,EAAS,gBAAA,EACT,OAAA,EAAS,MAAA,IACT,OAAA,EAAS,mBAAA,GACR,OAAA;;;;;;;iBA8CmB,cAAA,CACpB,gBAAA,EAAkB,gBAAA,EAClB,OAAA,EAAS,MAAA,IACT,OAAA,EAAS,mBAAA,EACT,OAAA,GAAU,gBAAA,GACT,OAAA,CAAQ,MAAA;;;;;cAgBE,IAAA,EAAM,cAAA,CAAe,mBAAA;;;UC/GjB,mBAAA;;EAEf,UAAA,GAAa,UAAA;AAAA;AAAA,cAGF,KAAA,EAAO,cAAA,CAAe,mBAAA;;;;;;;cCDtB,WAAA,EAAW,iBAAA,CAAA,QAAA;;AFKxB;;;;;;;;;iBEOgB,cAAA,CAAe,IAAA,EAAM,UAAA,EAAY,OAAA,EAAS,gBAAA,GAAmB,cAAA;;;;cCjBhE,oBAAA;;;;;;;AHUb;;iBGAgB,OAAA,CAAQ,UAAA,GAAY,aAAA,aAA+B,cAAA;;;;;;;;iBCNnD,yBAAA,CAA0B,IAAA,EAAM,UAAA,GAAa,cAAA;;;UCa5C,kBAAA;;EAEf,mBAAA,EAAqB,cAAA;;EAErB,SAAA,EAAW,cAAA;;EAEX,QAAA,EAAU,cAAA;;EAEV,YAAA;ELfmC;EKiBnC,KAAA,EAAO,cAAA;ELdA;EKgBP,QAAA,EAAU,cAAA;AAAA;;;;;;;;;;;iBAaI,WAAA,CAAY,IAAA,EAAM,UAAA,GAAa,kBAAA;;;UC1C9B,uBAAA;;EAEf,WAAA;AAAA;;;;;;ANQF;iBMCgB,gBAAA,CAAiB,OAAA,GAAS,uBAAA,GAA+B,cAAA;;;UCIxD,oBAAA;;EAEf,IAAA,EAAM,cAAA;AAAA;AAAA,UAGS,mBAAA;;;APVjB;;EOeE,aAAA,GAAgB,aAAA;EPZT;EOcP,UAAA,GAAa,UAAA;AAAA;;;;;;;;;;iBAYC,YAAA,CACd,gBAAA,EAAkB,gBAAA,EAClB,OAAA,EAAS,gBAAA,EACT,OAAA,EAAS,MAAA,IACT,OAAA,GAAS,mBAAA,GACR,oBAAA;;;UC7Cc,gBAAA;EACf,EAAA;EACA,IAAA;EACA,WAAA;EACA,IAAA;AAAA;AAAA,iBAGoB,sBAAA,CAAuB,MAAA,EAAQ,MAAA;EAAU,IAAA;EAAM;AAAA,GAAe,IAAA,CAAK,gBAAA,4BAAyC,OAAA,CAAA,gBAAA"}
|
package/build/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { i as
|
|
2
|
-
import { a as smartToolResult, i as parseResourceMessage, n as handleToolError, r as jsonToolResult, t as errorToolResult } from "./result-
|
|
1
|
+
import { a as authMiddleware, i as rpcInfo, n as requestFromExtra, o as authStorage, r as filterErrorResponse, t as registerTools } from "./registerTools-DkaraOyx.mjs";
|
|
2
|
+
import { a as smartToolResult, i as parseResourceMessage, n as handleToolError, o as structuredToolResult, r as jsonToolResult, t as errorToolResult } from "./result-CXgeE8ca.mjs";
|
|
3
3
|
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
4
|
-
import { createContext } from "@silkweave/core";
|
|
4
|
+
import { createContext, validateActionDisposition } from "@silkweave/core";
|
|
5
5
|
import express from "express";
|
|
6
6
|
import { generateProtectedResourceMetadata } from "@silkweave/auth";
|
|
7
7
|
import cors from "cors";
|
|
@@ -119,7 +119,7 @@ function sideloadResource(options = {}) {
|
|
|
119
119
|
}
|
|
120
120
|
//#endregion
|
|
121
121
|
//#region src/handlers/transport.ts
|
|
122
|
-
function createMcpServer(options, actions, context) {
|
|
122
|
+
function createMcpServer(options, actions, context, onToolCall) {
|
|
123
123
|
const server = new McpServer({
|
|
124
124
|
name: options.name,
|
|
125
125
|
description: options.description,
|
|
@@ -128,7 +128,7 @@ function createMcpServer(options, actions, context) {
|
|
|
128
128
|
tools: {},
|
|
129
129
|
logging: {}
|
|
130
130
|
} });
|
|
131
|
-
registerTools(server, actions, context);
|
|
131
|
+
registerTools(server, actions, context, { onToolCall });
|
|
132
132
|
return server;
|
|
133
133
|
}
|
|
134
134
|
/**
|
|
@@ -140,11 +140,24 @@ function createMcpServer(options, actions, context) {
|
|
|
140
140
|
* and tears down on response close. No `Mcp-Session-Id`, no session map, no
|
|
141
141
|
* `GET`/`DELETE` reconnect - any request can hit any instance.
|
|
142
142
|
*/
|
|
143
|
-
function mcpTransport(silkweaveOptions, context, actions) {
|
|
143
|
+
function mcpTransport(silkweaveOptions, context, actions, options = {}) {
|
|
144
|
+
actions.forEach(validateActionDisposition);
|
|
144
145
|
const post = async (req, res) => {
|
|
146
|
+
let active = actions;
|
|
147
|
+
if (options.filterActions) try {
|
|
148
|
+
active = await options.filterActions(actions, {
|
|
149
|
+
headers: req.headers,
|
|
150
|
+
url: req.originalUrl ?? req.url,
|
|
151
|
+
...rpcInfo(req.body)
|
|
152
|
+
});
|
|
153
|
+
} catch (error) {
|
|
154
|
+
const { status, body } = filterErrorResponse(error, req.body);
|
|
155
|
+
res.status(status).json(body);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
145
158
|
try {
|
|
146
159
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
|
|
147
|
-
const server = createMcpServer(silkweaveOptions,
|
|
160
|
+
const server = createMcpServer(silkweaveOptions, active, context, options.onToolCall);
|
|
148
161
|
res.on("close", () => {
|
|
149
162
|
transport.close();
|
|
150
163
|
server.close();
|
|
@@ -175,7 +188,7 @@ function mcpTransport(silkweaveOptions, context, actions) {
|
|
|
175
188
|
* top-level `startMcpServer()` / `http()` adapter conveniences.
|
|
176
189
|
*/
|
|
177
190
|
function buildMcpExpressApp(silkweaveOptions, context, actions, options) {
|
|
178
|
-
const { host, auth, cors: corsConfig, sideloadResources = true, resourceDir, ...mcpAppOptions } = options;
|
|
191
|
+
const { host, auth, cors: corsConfig, sideloadResources = true, resourceDir, filterActions, onToolCall, ...mcpAppOptions } = options;
|
|
179
192
|
const app = createMcpExpressApp({
|
|
180
193
|
...mcpAppOptions,
|
|
181
194
|
host
|
|
@@ -207,7 +220,10 @@ function buildMcpExpressApp(silkweaveOptions, context, actions, options) {
|
|
|
207
220
|
});
|
|
208
221
|
}
|
|
209
222
|
if (sideloadResources) app.get("/resource/:id", sideloadResource({ resourceDir }));
|
|
210
|
-
const transport = mcpTransport(silkweaveOptions, context, actions
|
|
223
|
+
const transport = mcpTransport(silkweaveOptions, context, actions, {
|
|
224
|
+
filterActions,
|
|
225
|
+
onToolCall
|
|
226
|
+
});
|
|
211
227
|
app.post("/mcp", express.json(), transport.post);
|
|
212
228
|
return app;
|
|
213
229
|
}
|
|
@@ -265,7 +281,7 @@ const http = (options) => {
|
|
|
265
281
|
};
|
|
266
282
|
//#endregion
|
|
267
283
|
//#region src/adapter/stdio.ts
|
|
268
|
-
const stdio = () => {
|
|
284
|
+
const stdio = (adapterOptions) => {
|
|
269
285
|
return (options, baseContext) => {
|
|
270
286
|
const context = baseContext.fork({ adapter: "stdio" });
|
|
271
287
|
const server = new McpServer({
|
|
@@ -279,7 +295,11 @@ const stdio = () => {
|
|
|
279
295
|
return {
|
|
280
296
|
context,
|
|
281
297
|
start: async (actions) => {
|
|
282
|
-
|
|
298
|
+
actions.forEach(validateActionDisposition);
|
|
299
|
+
registerTools(server, actions, context, {
|
|
300
|
+
logStream: false,
|
|
301
|
+
onToolCall: adapterOptions?.onToolCall
|
|
302
|
+
});
|
|
283
303
|
const transport = new StdioServerTransport();
|
|
284
304
|
await server.connect(transport);
|
|
285
305
|
},
|
|
@@ -303,6 +323,6 @@ async function createSideloadResource(buffer, { name, contentType }) {
|
|
|
303
323
|
return resource;
|
|
304
324
|
}
|
|
305
325
|
//#endregion
|
|
306
|
-
export { MCP_REQUIRED_HEADERS, authMiddleware, authStorage, buildMcpExpressApp, createSideloadResource, errorToolResult, handleToolError, http, jsonToolResult, mcpCors, mcpTransport, oauthRoutes, parseResourceMessage, protectedResourceMetadata, registerTools, requestFromExtra, sideloadResource, smartToolResult, startMcpServer, stdio };
|
|
326
|
+
export { MCP_REQUIRED_HEADERS, authMiddleware, authStorage, buildMcpExpressApp, createSideloadResource, errorToolResult, filterErrorResponse, handleToolError, http, jsonToolResult, mcpCors, mcpTransport, oauthRoutes, parseResourceMessage, protectedResourceMetadata, registerTools, requestFromExtra, rpcInfo, sideloadResource, smartToolResult, startMcpServer, stdio, structuredToolResult };
|
|
307
327
|
|
|
308
328
|
//# sourceMappingURL=index.mjs.map
|
package/build/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/handlers/cors.ts","../src/handlers/metadata.ts","../src/handlers/oauth.ts","../src/handlers/sideload.ts","../src/handlers/transport.ts","../src/adapter/http.ts","../src/adapter/stdio.ts","../src/util/sideload.ts"],"sourcesContent":["import cors, { CorsOptions } from 'cors'\nimport { type RequestHandler } from 'express'\n\n/** Headers required by the MCP protocol that must always be exposed when CORS is in use. */\nexport const MCP_REQUIRED_HEADERS = ['WWW-Authenticate', 'Last-Event-Id', 'Mcp-Protocol-Version']\n\n/**\n * CORS middleware preconfigured to expose the headers MCP clients need\n * (`Last-Event-Id`, `Mcp-Protocol-Version`, `WWW-Authenticate`) on top of any\n * user-supplied options. (Stateless transport - no `Mcp-Session-Id`.)\n *\n * Pass `false` to disable, omit / pass `true` for permissive defaults, or pass\n * a `CorsOptions` object to override.\n */\nexport function mcpCors(corsConfig: CorsOptions | boolean = true): RequestHandler | null {\n if (corsConfig === false) { return null }\n const userConfig = corsConfig === true ? {} : corsConfig\n const userExposed = userConfig.exposedHeaders\n const exposedHeaders = [\n ...MCP_REQUIRED_HEADERS,\n ...(Array.isArray(userExposed) ? userExposed : userExposed ? [userExposed] : [])\n ]\n return cors({ origin: '*', ...userConfig, exposedHeaders })\n}\n","import { AuthConfig, generateProtectedResourceMetadata } from '@silkweave/auth'\nimport { type RequestHandler } from 'express'\n\n/**\n * Handler for `GET /.well-known/oauth-protected-resource` (RFC 9728). Returns\n * the resource server's metadata pointing at the configured authorization\n * servers. Requires `auth.resourceUrl` and a non-empty `auth.authorizationServers`.\n */\nexport function protectedResourceMetadata(auth: AuthConfig): RequestHandler {\n if (!auth.resourceUrl || !auth.authorizationServers?.length) {\n throw new Error('@silkweave/mcp protectedResourceMetadata(): auth.resourceUrl and auth.authorizationServers are required')\n }\n const metadata = generateProtectedResourceMetadata(auth.resourceUrl, auth.authorizationServers, auth.requiredScopes)\n return (_req, res) => { res.json(metadata) }\n}\n","import { AuthConfig, OAuthRequest, OAuthResponse } from '@silkweave/auth'\nimport express, { type Request, type RequestHandler, type Response } from 'express'\n\nfunction toOAuthReq(req: Request): OAuthRequest {\n return {\n method: req.method,\n url: new URL(req.url, `${req.protocol}://${req.get('host')}`),\n headers: Object.fromEntries(Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v[0] : v])),\n body: req.body as Record<string, string> | undefined\n }\n}\n\nfunction sendOAuth(res: Response, oauthRes: OAuthResponse) {\n for (const [key, value] of Object.entries(oauthRes.headers)) { res.header(key, value) }\n if (oauthRes.body) {\n res.status(oauthRes.status).send(typeof oauthRes.body === 'string' ? oauthRes.body : JSON.stringify(oauthRes.body))\n } else {\n res.status(oauthRes.status).end()\n }\n}\n\nexport interface OAuthRouteHandlers {\n /** `GET /.well-known/oauth-authorization-server` - RFC 8414 discovery. */\n wellKnownAuthServer: RequestHandler\n /** `GET /authorize` - start the OAuth flow. */\n authorize: RequestHandler\n /** `GET {callbackPath}` - provider callback. */\n callback: RequestHandler\n /** Path the provider should redirect to (defaults to `/auth/callback`). */\n callbackPath: string\n /** `POST /token` - exchange code / refresh token. Includes urlencoded body parser. */\n token: RequestHandler[]\n /** `POST /register` - dynamic client registration. Includes JSON body parser. */\n register: RequestHandler[]\n}\n\n/**\n * Build the OAuth 2.1 proxy route handlers (authorize, callback, token,\n * register, well-known) backed by the configured `auth.provider`. Returns the\n * handlers as individual `RequestHandler`s so callers can register them\n * wherever they like - under `/mcp`, at the server root, etc.\n *\n * `token` and `register` are returned as middleware arrays because the\n * appropriate body parser must run before the handler. Apply with\n * `app.post(path, ...token)`.\n */\nexport function oauthRoutes(auth: AuthConfig): OAuthRouteHandlers {\n if (!auth.provider) { throw new Error('@silkweave/mcp oauthRoutes(): auth.provider is required') }\n const provider = auth.provider\n const callbackPath = auth.callbackPath ?? '/auth/callback'\n\n return {\n callbackPath,\n wellKnownAuthServer: (_req, res) => { sendOAuth(res, provider.metadata()) },\n authorize: async (req, res) => { sendOAuth(res, await provider.authorize(toOAuthReq(req))) },\n callback: async (req, res) => { sendOAuth(res, await provider.callback(toOAuthReq(req))) },\n token: [\n express.urlencoded({ extended: false }),\n async (req, res) => { sendOAuth(res, await provider.token(toOAuthReq(req))) }\n ],\n register: [\n express.json(),\n async (req, res) => { sendOAuth(res, await provider.register(toOAuthReq(req))) }\n ]\n }\n}\n","import { type RequestHandler } from 'express'\nimport { readFile } from 'fs/promises'\nimport { type SideloadResource } from '../util/sideload.js'\n\nexport interface SideloadResourceOptions {\n /** Directory to read sideload resources from. Defaults to `resources/` (cwd-relative). */\n resourceDir?: string\n}\n\n/**\n * Handler for `GET /resource/:id` - serves a large MCP response that was\n * sideloaded to disk as a `{id}` payload with a `{id}.json` metadata sidecar.\n *\n * The route param `id` is provided by the host framework (Express, Nest, etc.).\n */\nexport function sideloadResource(options: SideloadResourceOptions = {}): RequestHandler {\n const { resourceDir = 'resources' } = options\n return async (req, res) => {\n const id = req.params['id']\n if (!id || typeof id !== 'string') { throw new Error('Invalid ID') }\n const resourceMeta: SideloadResource = JSON.parse(await readFile(`${resourceDir}/${id}.json`, 'utf-8'))\n const buffer = await readFile(`${resourceDir}/${id}`)\n res.status(200)\n res.header('Content-Type', resourceMeta.contentType)\n res.send(buffer)\n }\n}\n","import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'\nimport { Action, SilkweaveContext, SilkweaveOptions } from '@silkweave/core'\nimport { type RequestHandler } from 'express'\nimport { registerTools } from './registerTools.js'\n\nfunction createMcpServer(options: SilkweaveOptions, actions: Action[], context: SilkweaveContext): McpServer {\n const server = new McpServer({\n name: options.name,\n description: options.description,\n version: options.version\n }, {\n capabilities: { tools: {}, logging: {} }\n })\n registerTools(server, actions, context)\n return server\n}\n\nexport interface McpTransportHandlers {\n /** `POST /mcp` - handle a single stateless MCP request/response (SSE per call). */\n post: RequestHandler\n}\n\n/**\n * Build the MCP Streamable HTTP transport route handler.\n *\n * Stateless (per the 2026 spec direction): each `POST /mcp` mints a fresh\n * transport + server with `sessionIdGenerator: undefined`, handles exactly that\n * request (streaming progress over SSE when the call carries a `progressToken`),\n * and tears down on response close. No `Mcp-Session-Id`, no session map, no\n * `GET`/`DELETE` reconnect - any request can hit any instance.\n */\nexport function mcpTransport(\n silkweaveOptions: SilkweaveOptions,\n context: SilkweaveContext,\n actions: Action[]\n): McpTransportHandlers {\n const post: RequestHandler = async (req, res) => {\n try {\n const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined })\n const server = createMcpServer(silkweaveOptions, actions, context)\n res.on('close', () => { void transport.close(); void server.close() })\n await server.connect(transport)\n await transport.handleRequest(req, res, req.body)\n } catch (error) {\n console.error('Error handling MCP request:', error)\n if (!res.headersSent) {\n res.status(500).json({ jsonrpc: '2.0', error: { code: -32_603, message: 'Internal server error' }, id: null })\n }\n }\n }\n\n return { post }\n}\n","import { createMcpExpressApp, type CreateMcpExpressAppOptions } from '@modelcontextprotocol/sdk/server/express.js'\nimport { AuthConfig } from '@silkweave/auth'\nimport { Action, AdapterFactory, createContext, SilkweaveContext, SilkweaveOptions } from '@silkweave/core'\nimport { CorsOptions } from 'cors'\nimport express, { type Express } from 'express'\nimport { Server } from 'http'\nimport { authMiddleware } from '../handlers/auth.js'\nimport { mcpCors } from '../handlers/cors.js'\nimport { protectedResourceMetadata } from '../handlers/metadata.js'\nimport { oauthRoutes } from '../handlers/oauth.js'\nimport { sideloadResource } from '../handlers/sideload.js'\nimport { mcpTransport } from '../handlers/transport.js'\n\nexport interface StartMcpHttpOptions extends CreateMcpExpressAppOptions {\n host: string\n port: number\n auth?: AuthConfig\n /** CORS configuration. `false` to disable, omitted/`true` for permissive defaults, or a `CorsOptions` object. */\n cors?: CorsOptions | boolean\n /** Mount the `/resource/:id` sideload route. Default `true`. */\n sideloadResources?: boolean\n /** Directory the sideload route reads from. Default `'resources'`. */\n resourceDir?: string\n}\n\n/**\n * Build a fully-wired Express app that exposes the MCP Streamable HTTP\n * transport (plus OAuth / sideload / well-known routes when configured).\n *\n * Pass the resulting `app` to `app.listen(port, host)` yourself, or use the\n * top-level `startMcpServer()` / `http()` adapter conveniences.\n */\nexport function buildMcpExpressApp(\n silkweaveOptions: SilkweaveOptions,\n context: SilkweaveContext,\n actions: Action[],\n options: StartMcpHttpOptions\n): Express {\n const { host, auth, cors: corsConfig, sideloadResources = true, resourceDir, ...mcpAppOptions } = options\n const app = createMcpExpressApp({ ...mcpAppOptions, host })\n\n const corsHandler = mcpCors(corsConfig ?? true)\n if (corsHandler) { app.use(corsHandler) }\n\n if (auth?.authorizationServers?.length && auth.resourceUrl) {\n app.get('/.well-known/oauth-protected-resource', protectedResourceMetadata(auth))\n }\n\n let oauthPaths = new Set<string>()\n if (auth?.provider) {\n const oauth = oauthRoutes(auth)\n app.get('/.well-known/oauth-authorization-server', oauth.wellKnownAuthServer)\n app.get('/authorize', oauth.authorize)\n app.get(oauth.callbackPath, oauth.callback)\n app.post('/token', ...oauth.token)\n app.post('/register', ...oauth.register)\n oauthPaths = new Set(['/.well-known/oauth-authorization-server', '/authorize', oauth.callbackPath, '/token', '/register'])\n }\n\n if (auth) {\n const guard = authMiddleware(auth, context)\n app.use((req, res, next) => {\n if (req.path.startsWith('/.well-known/') || oauthPaths.has(req.path)) { return next() }\n return guard(req, res, next)\n })\n }\n\n if (sideloadResources) {\n app.get('/resource/:id', sideloadResource({ resourceDir }))\n }\n\n const transport = mcpTransport(silkweaveOptions, context, actions)\n app.post('/mcp', express.json(), transport.post)\n\n return app\n}\n\n/**\n * Spin up a standalone MCP Streamable HTTP server on `host:port` for the\n * given `actions`. Returns the underlying `Server` so callers can close it.\n *\n * Convenience for use cases that don't go through the `silkweave()` builder.\n */\nexport async function startMcpServer(\n silkweaveOptions: SilkweaveOptions,\n actions: Action[],\n options: StartMcpHttpOptions,\n context?: SilkweaveContext\n): Promise<Server> {\n const ctx = context ?? createContext({ adapter: 'http' })\n const app = buildMcpExpressApp(silkweaveOptions, ctx, actions, options)\n return new Promise<Server>((resolve, reject) => {\n const server = app.listen(options.port, options.host, (error) => {\n if (error) { reject(error); return }\n console.log(`MCP Streamable HTTP Server listening on http://${options.host}:${options.port}/mcp`)\n resolve(server)\n })\n })\n}\n\n/**\n * Silkweave adapter that owns its own HTTP server. Composes the MCP handler\n * primitives into a fully-wired Express app and listens on `host:port`.\n */\nexport const http: AdapterFactory<StartMcpHttpOptions> = (options) => {\n return (silkweaveOptions, baseContext) => {\n const context = baseContext.fork({ adapter: 'http' })\n let httpServer: Server | undefined\n return {\n context,\n start: async (actions) => {\n const app = buildMcpExpressApp(silkweaveOptions, context, actions, options)\n httpServer = await new Promise<Server>((resolve, reject) => {\n const s = app.listen(options.port, options.host, (error) => {\n if (error) { reject(error); return }\n console.log(`MCP Streamable HTTP Server listening on http://${options.host}:${options.port}/mcp`)\n resolve(s)\n })\n })\n },\n stop: async () => {\n if (!httpServer) { return }\n await new Promise<void>((resolve, reject) => {\n httpServer!.close((err) => err ? reject(err) : resolve())\n })\n httpServer = undefined\n }\n }\n }\n}\n","import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\nimport { AdapterFactory } from '@silkweave/core'\nimport { registerTools } from '../handlers/registerTools.js'\n\nexport const stdio: AdapterFactory = () => {\n return (options, baseContext) => {\n const context = baseContext.fork({ adapter: 'stdio' })\n const server = new McpServer({\n name: options.name,\n description: options.description,\n version: options.version\n }, {\n capabilities: { tools: {}, logging: {} }\n })\n return {\n context,\n start: async (actions) => {\n registerTools(server, actions, context, { logStream: false })\n const transport = new StdioServerTransport()\n await server.connect(transport)\n },\n stop: async () => {\n await server?.close()\n }\n }\n }\n}\n","import { randomUUID } from 'crypto'\nimport { writeFile } from 'fs/promises'\n\nexport interface SideloadResource {\n id: string\n name: string\n contentType: string\n size: number\n}\n\nexport async function createSideloadResource(buffer: Buffer, { name, contentType }: Pick<SideloadResource, 'name' | 'contentType'>) {\n const id = randomUUID()\n const resource: SideloadResource = { id, name, contentType, size: buffer.length }\n await Promise.all([\n writeFile(`resources/${id}`, buffer),\n writeFile(`resources/${id}.json`, JSON.stringify(resource), 'utf-8')\n ])\n return resource\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/handlers/cors.ts","../src/handlers/metadata.ts","../src/handlers/oauth.ts","../src/handlers/sideload.ts","../src/handlers/transport.ts","../src/adapter/http.ts","../src/adapter/stdio.ts","../src/util/sideload.ts"],"sourcesContent":["import cors, { CorsOptions } from 'cors'\nimport { type RequestHandler } from 'express'\n\n/** Headers required by the MCP protocol that must always be exposed when CORS is in use. */\nexport const MCP_REQUIRED_HEADERS = ['WWW-Authenticate', 'Last-Event-Id', 'Mcp-Protocol-Version']\n\n/**\n * CORS middleware preconfigured to expose the headers MCP clients need\n * (`Last-Event-Id`, `Mcp-Protocol-Version`, `WWW-Authenticate`) on top of any\n * user-supplied options. (Stateless transport - no `Mcp-Session-Id`.)\n *\n * Pass `false` to disable, omit / pass `true` for permissive defaults, or pass\n * a `CorsOptions` object to override.\n */\nexport function mcpCors(corsConfig: CorsOptions | boolean = true): RequestHandler | null {\n if (corsConfig === false) { return null }\n const userConfig = corsConfig === true ? {} : corsConfig\n const userExposed = userConfig.exposedHeaders\n const exposedHeaders = [\n ...MCP_REQUIRED_HEADERS,\n ...(Array.isArray(userExposed) ? userExposed : userExposed ? [userExposed] : [])\n ]\n return cors({ origin: '*', ...userConfig, exposedHeaders })\n}\n","import { AuthConfig, generateProtectedResourceMetadata } from '@silkweave/auth'\nimport { type RequestHandler } from 'express'\n\n/**\n * Handler for `GET /.well-known/oauth-protected-resource` (RFC 9728). Returns\n * the resource server's metadata pointing at the configured authorization\n * servers. Requires `auth.resourceUrl` and a non-empty `auth.authorizationServers`.\n */\nexport function protectedResourceMetadata(auth: AuthConfig): RequestHandler {\n if (!auth.resourceUrl || !auth.authorizationServers?.length) {\n throw new Error('@silkweave/mcp protectedResourceMetadata(): auth.resourceUrl and auth.authorizationServers are required')\n }\n const metadata = generateProtectedResourceMetadata(auth.resourceUrl, auth.authorizationServers, auth.requiredScopes)\n return (_req, res) => { res.json(metadata) }\n}\n","import { AuthConfig, OAuthRequest, OAuthResponse } from '@silkweave/auth'\nimport express, { type Request, type RequestHandler, type Response } from 'express'\n\nfunction toOAuthReq(req: Request): OAuthRequest {\n return {\n method: req.method,\n url: new URL(req.url, `${req.protocol}://${req.get('host')}`),\n headers: Object.fromEntries(Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v[0] : v])),\n body: req.body as Record<string, string> | undefined\n }\n}\n\nfunction sendOAuth(res: Response, oauthRes: OAuthResponse) {\n for (const [key, value] of Object.entries(oauthRes.headers)) { res.header(key, value) }\n if (oauthRes.body) {\n res.status(oauthRes.status).send(typeof oauthRes.body === 'string' ? oauthRes.body : JSON.stringify(oauthRes.body))\n } else {\n res.status(oauthRes.status).end()\n }\n}\n\nexport interface OAuthRouteHandlers {\n /** `GET /.well-known/oauth-authorization-server` - RFC 8414 discovery. */\n wellKnownAuthServer: RequestHandler\n /** `GET /authorize` - start the OAuth flow. */\n authorize: RequestHandler\n /** `GET {callbackPath}` - provider callback. */\n callback: RequestHandler\n /** Path the provider should redirect to (defaults to `/auth/callback`). */\n callbackPath: string\n /** `POST /token` - exchange code / refresh token. Includes urlencoded body parser. */\n token: RequestHandler[]\n /** `POST /register` - dynamic client registration. Includes JSON body parser. */\n register: RequestHandler[]\n}\n\n/**\n * Build the OAuth 2.1 proxy route handlers (authorize, callback, token,\n * register, well-known) backed by the configured `auth.provider`. Returns the\n * handlers as individual `RequestHandler`s so callers can register them\n * wherever they like - under `/mcp`, at the server root, etc.\n *\n * `token` and `register` are returned as middleware arrays because the\n * appropriate body parser must run before the handler. Apply with\n * `app.post(path, ...token)`.\n */\nexport function oauthRoutes(auth: AuthConfig): OAuthRouteHandlers {\n if (!auth.provider) { throw new Error('@silkweave/mcp oauthRoutes(): auth.provider is required') }\n const provider = auth.provider\n const callbackPath = auth.callbackPath ?? '/auth/callback'\n\n return {\n callbackPath,\n wellKnownAuthServer: (_req, res) => { sendOAuth(res, provider.metadata()) },\n authorize: async (req, res) => { sendOAuth(res, await provider.authorize(toOAuthReq(req))) },\n callback: async (req, res) => { sendOAuth(res, await provider.callback(toOAuthReq(req))) },\n token: [\n express.urlencoded({ extended: false }),\n async (req, res) => { sendOAuth(res, await provider.token(toOAuthReq(req))) }\n ],\n register: [\n express.json(),\n async (req, res) => { sendOAuth(res, await provider.register(toOAuthReq(req))) }\n ]\n }\n}\n","import { type RequestHandler } from 'express'\nimport { readFile } from 'fs/promises'\nimport { type SideloadResource } from '../util/sideload.js'\n\nexport interface SideloadResourceOptions {\n /** Directory to read sideload resources from. Defaults to `resources/` (cwd-relative). */\n resourceDir?: string\n}\n\n/**\n * Handler for `GET /resource/:id` - serves a large MCP response that was\n * sideloaded to disk as a `{id}` payload with a `{id}.json` metadata sidecar.\n *\n * The route param `id` is provided by the host framework (Express, Nest, etc.).\n */\nexport function sideloadResource(options: SideloadResourceOptions = {}): RequestHandler {\n const { resourceDir = 'resources' } = options\n return async (req, res) => {\n const id = req.params['id']\n if (!id || typeof id !== 'string') { throw new Error('Invalid ID') }\n const resourceMeta: SideloadResource = JSON.parse(await readFile(`${resourceDir}/${id}.json`, 'utf-8'))\n const buffer = await readFile(`${resourceDir}/${id}`)\n res.status(200)\n res.header('Content-Type', resourceMeta.contentType)\n res.send(buffer)\n }\n}\n","import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'\nimport { Action, OnToolCall, SilkweaveContext, SilkweaveOptions, validateActionDisposition } from '@silkweave/core'\nimport { type RequestHandler } from 'express'\nimport { filterErrorResponse, rpcInfo, type FilterActions } from './filter.js'\nimport { registerTools } from './registerTools.js'\n\nfunction createMcpServer(options: SilkweaveOptions, actions: Action[], context: SilkweaveContext, onToolCall?: OnToolCall): McpServer {\n const server = new McpServer({\n name: options.name,\n description: options.description,\n version: options.version\n }, {\n capabilities: { tools: {}, logging: {} }\n })\n registerTools(server, actions, context, { onToolCall })\n return server\n}\n\nexport interface McpTransportHandlers {\n /** `POST /mcp` - handle a single stateless MCP request/response (SSE per call). */\n post: RequestHandler\n}\n\nexport interface McpTransportOptions {\n /**\n * Per-request tool filter, applied before `registerTools()` on every POST.\n * See `FilterActions` for the request stand-in and error semantics.\n */\n filterActions?: FilterActions\n /** Telemetry hook invoked once per tool call (fire-and-forget). */\n onToolCall?: OnToolCall\n}\n\n/**\n * Build the MCP Streamable HTTP transport route handler.\n *\n * Stateless (per the 2026 spec direction): each `POST /mcp` mints a fresh\n * transport + server with `sessionIdGenerator: undefined`, handles exactly that\n * request (streaming progress over SSE when the call carries a `progressToken`),\n * and tears down on response close. No `Mcp-Session-Id`, no session map, no\n * `GET`/`DELETE` reconnect - any request can hit any instance.\n */\nexport function mcpTransport(\n silkweaveOptions: SilkweaveOptions,\n context: SilkweaveContext,\n actions: Action[],\n options: McpTransportOptions = {}\n): McpTransportHandlers {\n // Fail at boot, not per request - the factory runs once when the app is built.\n actions.forEach(validateActionDisposition)\n\n const post: RequestHandler = async (req, res) => {\n let active = actions\n if (options.filterActions) {\n try {\n active = await options.filterActions(actions, { headers: req.headers, url: req.originalUrl ?? req.url, ...rpcInfo(req.body) })\n } catch (error) {\n // A throw never degrades to an empty tool list - it surfaces as its\n // statusCode (SilkweaveError) or a 500, so a bad key reads as an auth\n // failure rather than \"server has no tools\".\n const { status, body } = filterErrorResponse(error, req.body)\n res.status(status).json(body)\n return\n }\n }\n try {\n const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined })\n const server = createMcpServer(silkweaveOptions, active, context, options.onToolCall)\n res.on('close', () => { void transport.close(); void server.close() })\n await server.connect(transport)\n await transport.handleRequest(req, res, req.body)\n } catch (error) {\n console.error('Error handling MCP request:', error)\n if (!res.headersSent) {\n res.status(500).json({ jsonrpc: '2.0', error: { code: -32_603, message: 'Internal server error' }, id: null })\n }\n }\n }\n\n return { post }\n}\n","import { createMcpExpressApp, type CreateMcpExpressAppOptions } from '@modelcontextprotocol/sdk/server/express.js'\nimport { AuthConfig } from '@silkweave/auth'\nimport { Action, AdapterFactory, createContext, OnToolCall, SilkweaveContext, SilkweaveOptions } from '@silkweave/core'\nimport { CorsOptions } from 'cors'\nimport express, { type Express } from 'express'\nimport { Server } from 'http'\nimport { authMiddleware } from '../handlers/auth.js'\nimport { mcpCors } from '../handlers/cors.js'\nimport { protectedResourceMetadata } from '../handlers/metadata.js'\nimport { oauthRoutes } from '../handlers/oauth.js'\nimport { type FilterActions } from '../handlers/filter.js'\nimport { sideloadResource } from '../handlers/sideload.js'\nimport { mcpTransport } from '../handlers/transport.js'\n\nexport interface StartMcpHttpOptions extends CreateMcpExpressAppOptions {\n host: string\n port: number\n auth?: AuthConfig\n /** CORS configuration. `false` to disable, omitted/`true` for permissive defaults, or a `CorsOptions` object. */\n cors?: CorsOptions | boolean\n /** Mount the `/resource/:id` sideload route. Default `true`. */\n sideloadResources?: boolean\n /** Directory the sideload route reads from. Default `'resources'`. */\n resourceDir?: string\n /**\n * Per-request tool filter, applied before `registerTools()` on every\n * `POST /mcp` (the stateless transport recomputes the tool list per request,\n * so permission changes apply on the next `tools/list`). See `FilterActions`\n * for the request stand-in (`headers`/`url`/`method`/`toolName`) and error\n * semantics (a throw surfaces as its `SilkweaveError.statusCode` or 500 -\n * never an empty tool list).\n */\n filterActions?: FilterActions\n /** Telemetry hook invoked once per tool call (fire-and-forget). */\n onToolCall?: OnToolCall\n}\n\n/**\n * Build a fully-wired Express app that exposes the MCP Streamable HTTP\n * transport (plus OAuth / sideload / well-known routes when configured).\n *\n * Pass the resulting `app` to `app.listen(port, host)` yourself, or use the\n * top-level `startMcpServer()` / `http()` adapter conveniences.\n */\nexport function buildMcpExpressApp(\n silkweaveOptions: SilkweaveOptions,\n context: SilkweaveContext,\n actions: Action[],\n options: StartMcpHttpOptions\n): Express {\n const { host, auth, cors: corsConfig, sideloadResources = true, resourceDir, filterActions, onToolCall, ...mcpAppOptions } = options\n const app = createMcpExpressApp({ ...mcpAppOptions, host })\n\n const corsHandler = mcpCors(corsConfig ?? true)\n if (corsHandler) { app.use(corsHandler) }\n\n if (auth?.authorizationServers?.length && auth.resourceUrl) {\n app.get('/.well-known/oauth-protected-resource', protectedResourceMetadata(auth))\n }\n\n let oauthPaths = new Set<string>()\n if (auth?.provider) {\n const oauth = oauthRoutes(auth)\n app.get('/.well-known/oauth-authorization-server', oauth.wellKnownAuthServer)\n app.get('/authorize', oauth.authorize)\n app.get(oauth.callbackPath, oauth.callback)\n app.post('/token', ...oauth.token)\n app.post('/register', ...oauth.register)\n oauthPaths = new Set(['/.well-known/oauth-authorization-server', '/authorize', oauth.callbackPath, '/token', '/register'])\n }\n\n if (auth) {\n const guard = authMiddleware(auth, context)\n app.use((req, res, next) => {\n if (req.path.startsWith('/.well-known/') || oauthPaths.has(req.path)) { return next() }\n return guard(req, res, next)\n })\n }\n\n if (sideloadResources) {\n app.get('/resource/:id', sideloadResource({ resourceDir }))\n }\n\n const transport = mcpTransport(silkweaveOptions, context, actions, { filterActions, onToolCall })\n app.post('/mcp', express.json(), transport.post)\n\n return app\n}\n\n/**\n * Spin up a standalone MCP Streamable HTTP server on `host:port` for the\n * given `actions`. Returns the underlying `Server` so callers can close it.\n *\n * Convenience for use cases that don't go through the `silkweave()` builder.\n */\nexport async function startMcpServer(\n silkweaveOptions: SilkweaveOptions,\n actions: Action[],\n options: StartMcpHttpOptions,\n context?: SilkweaveContext\n): Promise<Server> {\n const ctx = context ?? createContext({ adapter: 'http' })\n const app = buildMcpExpressApp(silkweaveOptions, ctx, actions, options)\n return new Promise<Server>((resolve, reject) => {\n const server = app.listen(options.port, options.host, (error) => {\n if (error) { reject(error); return }\n console.log(`MCP Streamable HTTP Server listening on http://${options.host}:${options.port}/mcp`)\n resolve(server)\n })\n })\n}\n\n/**\n * Silkweave adapter that owns its own HTTP server. Composes the MCP handler\n * primitives into a fully-wired Express app and listens on `host:port`.\n */\nexport const http: AdapterFactory<StartMcpHttpOptions> = (options) => {\n return (silkweaveOptions, baseContext) => {\n const context = baseContext.fork({ adapter: 'http' })\n let httpServer: Server | undefined\n return {\n context,\n start: async (actions) => {\n const app = buildMcpExpressApp(silkweaveOptions, context, actions, options)\n httpServer = await new Promise<Server>((resolve, reject) => {\n const s = app.listen(options.port, options.host, (error) => {\n if (error) { reject(error); return }\n console.log(`MCP Streamable HTTP Server listening on http://${options.host}:${options.port}/mcp`)\n resolve(s)\n })\n })\n },\n stop: async () => {\n if (!httpServer) { return }\n await new Promise<void>((resolve, reject) => {\n httpServer!.close((err) => err ? reject(err) : resolve())\n })\n httpServer = undefined\n }\n }\n }\n}\n","import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\nimport { AdapterFactory, OnToolCall, validateActionDisposition } from '@silkweave/core'\nimport { registerTools } from '../handlers/registerTools.js'\n\nexport interface StdioAdapterOptions {\n /** Telemetry hook invoked once per tool call (fire-and-forget). */\n onToolCall?: OnToolCall\n}\n\nexport const stdio: AdapterFactory<StdioAdapterOptions | void> = (adapterOptions) => {\n return (options, baseContext) => {\n const context = baseContext.fork({ adapter: 'stdio' })\n const server = new McpServer({\n name: options.name,\n description: options.description,\n version: options.version\n }, {\n capabilities: { tools: {}, logging: {} }\n })\n return {\n context,\n start: async (actions) => {\n actions.forEach(validateActionDisposition)\n registerTools(server, actions, context, { logStream: false, onToolCall: adapterOptions?.onToolCall })\n const transport = new StdioServerTransport()\n await server.connect(transport)\n },\n stop: async () => {\n await server?.close()\n }\n }\n }\n}\n","import { randomUUID } from 'crypto'\nimport { writeFile } from 'fs/promises'\n\nexport interface SideloadResource {\n id: string\n name: string\n contentType: string\n size: number\n}\n\nexport async function createSideloadResource(buffer: Buffer, { name, contentType }: Pick<SideloadResource, 'name' | 'contentType'>) {\n const id = randomUUID()\n const resource: SideloadResource = { id, name, contentType, size: buffer.length }\n await Promise.all([\n writeFile(`resources/${id}`, buffer),\n writeFile(`resources/${id}.json`, JSON.stringify(resource), 'utf-8')\n ])\n return resource\n}\n"],"mappings":"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"}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { a as smartToolResult, n as handleToolError, r as jsonToolResult } from "./result-
|
|
2
|
-
import { isStreamingAction, runStreamingAction } from "@silkweave/core";
|
|
1
|
+
import { a as smartToolResult, n as handleToolError, o as structuredToolResult, r as jsonToolResult, t as errorToolResult } from "./result-CXgeE8ca.mjs";
|
|
2
|
+
import { SilkweaveError, createLogger, emitToolCall, isStreamingAction, runStreamingAction } from "@silkweave/core";
|
|
3
3
|
import { validateToken } from "@silkweave/auth";
|
|
4
4
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
|
-
import { createLogger } from "@silkweave/logger";
|
|
6
5
|
import { capitalCase, pascalCase } from "change-case";
|
|
7
6
|
//#region src/handlers/auth.ts
|
|
8
7
|
/**
|
|
@@ -35,6 +34,54 @@ function authMiddleware(auth, context) {
|
|
|
35
34
|
};
|
|
36
35
|
}
|
|
37
36
|
//#endregion
|
|
37
|
+
//#region src/handlers/filter.ts
|
|
38
|
+
/**
|
|
39
|
+
* Extract the JSON-RPC `method` (and `toolName` for `tools/call`) from a
|
|
40
|
+
* parsed request body. Tolerates a legacy batch (first request wins) and
|
|
41
|
+
* unrecognizable bodies (empty `method`).
|
|
42
|
+
*/
|
|
43
|
+
function rpcInfo(body) {
|
|
44
|
+
const message = Array.isArray(body) ? body[0] : body;
|
|
45
|
+
const method = typeof message?.method === "string" ? message.method : "";
|
|
46
|
+
const toolName = method === "tools/call" && typeof message?.params?.name === "string" ? message.params.name : void 0;
|
|
47
|
+
return {
|
|
48
|
+
method,
|
|
49
|
+
...toolName !== void 0 ? { toolName } : {}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Map a `filterActions` throw to an HTTP status + JSON-RPC error body: a
|
|
54
|
+
* `SilkweaveError` keeps its `statusCode` (so an invalid key surfaces as an
|
|
55
|
+
* auth failure, not "server has no tools"), anything else is a 500. `id` is
|
|
56
|
+
* echoed from the request body when available.
|
|
57
|
+
*/
|
|
58
|
+
function filterErrorResponse(error, body) {
|
|
59
|
+
const id = (Array.isArray(body) ? body[0] : body)?.id ?? null;
|
|
60
|
+
if (error instanceof SilkweaveError) return {
|
|
61
|
+
status: error.statusCode,
|
|
62
|
+
body: {
|
|
63
|
+
jsonrpc: "2.0",
|
|
64
|
+
error: {
|
|
65
|
+
code: -32e3,
|
|
66
|
+
message: error.message
|
|
67
|
+
},
|
|
68
|
+
id
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
console.error("filterActions error:", error);
|
|
72
|
+
return {
|
|
73
|
+
status: 500,
|
|
74
|
+
body: {
|
|
75
|
+
jsonrpc: "2.0",
|
|
76
|
+
error: {
|
|
77
|
+
code: -32603,
|
|
78
|
+
message: "Internal server error"
|
|
79
|
+
},
|
|
80
|
+
id
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
38
85
|
//#region src/handlers/registerTools.ts
|
|
39
86
|
/**
|
|
40
87
|
* Build the generic `request` context value (the same key REST/tRPC populate)
|
|
@@ -99,13 +146,50 @@ async function runAction(action, input, context, extra) {
|
|
|
99
146
|
} : void 0);
|
|
100
147
|
return action.run(input, context);
|
|
101
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Result path for a `disposition: 'structured'` action: parse the raw result
|
|
151
|
+
* through the output schema and ship the PARSED data as `structuredContent`.
|
|
152
|
+
* Parsing strips extra fields (so the client-side JSON-Schema validator, which
|
|
153
|
+
* rejects `additionalProperties`, passes by construction) and a genuine
|
|
154
|
+
* mismatch (missing required field, wrong type) degrades to an `isError` tool
|
|
155
|
+
* result - which the SDK exempts from output validation - instead of an opaque
|
|
156
|
+
* protocol error.
|
|
157
|
+
*/
|
|
158
|
+
function structuredResult(action, result) {
|
|
159
|
+
const parsed = action.output.safeParse(result);
|
|
160
|
+
if (!parsed.success) {
|
|
161
|
+
const fields = [...new Set(parsed.error.issues.map((issue) => issue.path.join(".") || "(root)"))].join(", ");
|
|
162
|
+
return errorToolResult(new SilkweaveError(`Output validation failed for '${action.name}' at: ${fields}. The tool returned a shape that does not match its declared output schema - this is a server-side bug, not an input problem; retrying with different arguments will not help.`, "output_validation_error"));
|
|
163
|
+
}
|
|
164
|
+
return structuredToolResult(parsed.data);
|
|
165
|
+
}
|
|
166
|
+
/** MCP-only telemetry fields, computed only when a hook is registered. */
|
|
167
|
+
function resultMeta(result, formatted) {
|
|
168
|
+
return {
|
|
169
|
+
resultBytes: JSON.stringify(result).length,
|
|
170
|
+
sideloaded: (formatted.content ?? []).some((block) => block.type === "resource")
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/** Error identity for telemetry: a SilkweaveError's `code`, else the error's name. */
|
|
174
|
+
function errorMeta(error) {
|
|
175
|
+
if (error instanceof SilkweaveError) return {
|
|
176
|
+
errorCode: error.code,
|
|
177
|
+
errorMessage: error.message
|
|
178
|
+
};
|
|
179
|
+
if (error instanceof Error) return {
|
|
180
|
+
errorCode: error.name,
|
|
181
|
+
errorMessage: error.message
|
|
182
|
+
};
|
|
183
|
+
return { errorCode: "unknown" };
|
|
184
|
+
}
|
|
102
185
|
/** Format via the action's `toolResult` hook, else the resolved disposition. */
|
|
103
186
|
function formatToolResult(action, result, context, disposition) {
|
|
104
187
|
if (action.toolResult) {
|
|
105
188
|
const response = action.toolResult(result, context);
|
|
106
189
|
if (response) return response;
|
|
107
190
|
}
|
|
108
|
-
|
|
191
|
+
if (action.disposition === "structured") return structuredResult(action, result);
|
|
192
|
+
return disposition === "smart" ? smartToolResult(result) : jsonToolResult(result);
|
|
109
193
|
}
|
|
110
194
|
/**
|
|
111
195
|
* Register every action as an MCP tool on `server`. Shared by all MCP transports
|
|
@@ -114,14 +198,20 @@ function formatToolResult(action, result, context, disposition) {
|
|
|
114
198
|
* and - when bearer auth ran for this request - the resolved `auth`. The result
|
|
115
199
|
* is formatted by the action's `toolResult` hook if present, otherwise per the
|
|
116
200
|
* resolved disposition (client `_meta.disposition` > `action.disposition` >
|
|
117
|
-
* `
|
|
201
|
+
* `json`); a `'structured'` action always ships schema-parsed
|
|
202
|
+
* `structuredContent` (its contract cannot be demoted per-call).
|
|
118
203
|
*/
|
|
119
204
|
function registerTools(server, actions, context, options = {}) {
|
|
120
205
|
const stream = options.logStream ?? process.stderr;
|
|
121
206
|
for (const action of actions) server.registerTool(pascalCase(action.name), {
|
|
122
207
|
title: capitalCase(action.name),
|
|
123
208
|
description: action.description,
|
|
124
|
-
inputSchema: action.input
|
|
209
|
+
inputSchema: action.input,
|
|
210
|
+
annotations: {
|
|
211
|
+
readOnlyHint: action.kind === "query",
|
|
212
|
+
...action.annotations
|
|
213
|
+
},
|
|
214
|
+
...action.disposition === "structured" && action.output ? { outputSchema: action.output } : {}
|
|
125
215
|
}, async (input, extra) => {
|
|
126
216
|
const logger = createToolLogger(extra, stream);
|
|
127
217
|
const currentAuth = authStorage.getStore();
|
|
@@ -132,14 +222,35 @@ function registerTools(server, actions, context, options = {}) {
|
|
|
132
222
|
...currentAuth ? { auth: currentAuth } : {}
|
|
133
223
|
});
|
|
134
224
|
const disposition = extra._meta?.disposition ?? action.disposition;
|
|
225
|
+
const base = {
|
|
226
|
+
action: action.name,
|
|
227
|
+
tool: pascalCase(action.name),
|
|
228
|
+
transport: "mcp",
|
|
229
|
+
context: actionContext
|
|
230
|
+
};
|
|
231
|
+
const started = Date.now();
|
|
135
232
|
try {
|
|
136
|
-
|
|
233
|
+
const result = await runAction(action, input, actionContext, extra);
|
|
234
|
+
const formatted = formatToolResult(action, result, actionContext, disposition);
|
|
235
|
+
emitToolCall(options.onToolCall, {
|
|
236
|
+
...base,
|
|
237
|
+
durationMs: Date.now() - started,
|
|
238
|
+
ok: formatted.isError !== true,
|
|
239
|
+
...options.onToolCall ? resultMeta(result, formatted) : {}
|
|
240
|
+
});
|
|
241
|
+
return formatted;
|
|
137
242
|
} catch (error) {
|
|
243
|
+
emitToolCall(options.onToolCall, {
|
|
244
|
+
...base,
|
|
245
|
+
durationMs: Date.now() - started,
|
|
246
|
+
ok: false,
|
|
247
|
+
...errorMeta(error)
|
|
248
|
+
});
|
|
138
249
|
return handleToolError(error);
|
|
139
250
|
}
|
|
140
251
|
});
|
|
141
252
|
}
|
|
142
253
|
//#endregion
|
|
143
|
-
export {
|
|
254
|
+
export { authMiddleware as a, rpcInfo as i, requestFromExtra as n, authStorage as o, filterErrorResponse as r, registerTools as t };
|
|
144
255
|
|
|
145
|
-
//# sourceMappingURL=registerTools-
|
|
256
|
+
//# sourceMappingURL=registerTools-DkaraOyx.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registerTools-DkaraOyx.mjs","names":[],"sources":["../src/handlers/auth.ts","../src/handlers/filter.ts","../src/handlers/registerTools.ts"],"sourcesContent":["import { AuthConfig, AuthInfo, validateToken } from '@silkweave/auth'\nimport { SilkweaveContext } from '@silkweave/core'\nimport { type RequestHandler } from 'express'\nimport { AsyncLocalStorage } from 'node:async_hooks'\n\n/**\n * Per-request bearer-token storage used by tool handlers to read the resolved\n * `AuthInfo` for the currently-handled MCP call.\n */\nexport const authStorage = new AsyncLocalStorage<AuthInfo>()\n\n/**\n * Express middleware that validates the `Authorization: Bearer …` header via\n * the supplied `AuthConfig`. On success the resolved `AuthInfo` is placed in\n * `authStorage` for the duration of the downstream handler - `mcpTransport`'s\n * tool callbacks pick it up to populate the silkweave context's `auth` key.\n *\n * The middleware should NOT be applied to OAuth-discovery / token routes -\n * compose it only on the routes that require an authenticated caller (the MCP\n * transport itself, sideload, etc.).\n */\nexport function authMiddleware(auth: AuthConfig, context: SilkweaveContext): RequestHandler {\n return async (req, res, next) => {\n const result = await validateToken(req.headers.authorization, auth, context.fork({ request: req }))\n if (result.error) {\n for (const [key, value] of Object.entries(result.error.headers)) { res.header(key, value) }\n res.status(result.error.statusCode).json(result.error.body)\n return\n }\n if (result.auth) {\n authStorage.run(result.auth, () => { next() })\n } else {\n next()\n }\n }\n}\n","import { Action, SilkweaveError } from '@silkweave/core'\n\n/**\n * Normalized request stand-in handed to `filterActions` - the same shape for\n * the Express `http()` transport and the Web-Standard `edge()` adapter.\n */\nexport interface FilterRequest {\n /** Inbound HTTP headers (lower-cased keys, as delivered by the host). */\n headers: Record<string, string | string[] | undefined>\n /** Full request URL (or path, as delivered by the host). */\n url: string\n /**\n * JSON-RPC method of the POSTed message (`'initialize'`, `'tools/list'`,\n * `'tools/call'`, `'ping'`, ...). Lets the callback skip expensive\n * permission lookups on `initialize`/`ping`, and doubles as an\n * observability tap (e.g. counting `tools/list`). For a legacy JSON-RPC\n * batch this is the first request's method. Empty string when the body is\n * not a recognizable JSON-RPC message (the transport will reject it anyway).\n */\n method: string\n /** `params.name` of a `tools/call` message; unset for every other method. */\n toolName?: string\n}\n\n/**\n * Per-request tool filter for the stateless MCP transports. Runs before\n * `registerTools()` on every `POST /mcp`; only the returned actions exist for\n * that request (`tools/list` and `tools/call` alike - a client that cached a\n * wider list is still denied). May be async (e.g. a DB lookup of API-key\n * permissions).\n *\n * Error semantics: a thrown `SilkweaveError` propagates as its `statusCode`\n * (e.g. 401 invalid key, 403 insufficient permissions) with a JSON-RPC error\n * body; any other throw maps to HTTP 500. A thrown error NEVER degrades to an\n * empty tool list - return `[]` explicitly if \"no tools\" is the intended\n * answer.\n */\nexport type FilterActions = (actions: Action[], request: FilterRequest) => Action[] | Promise<Action[]>\n\n/**\n * Extract the JSON-RPC `method` (and `toolName` for `tools/call`) from a\n * parsed request body. Tolerates a legacy batch (first request wins) and\n * unrecognizable bodies (empty `method`).\n */\nexport function rpcInfo(body: unknown): { method: string; toolName?: string } {\n const message = (Array.isArray(body) ? body[0] : body) as { method?: unknown; params?: { name?: unknown } } | undefined\n const method = typeof message?.method === 'string' ? message.method : ''\n const toolName = method === 'tools/call' && typeof message?.params?.name === 'string' ? message.params.name : undefined\n return { method, ...(toolName !== undefined ? { toolName } : {}) }\n}\n\n/**\n * Map a `filterActions` throw to an HTTP status + JSON-RPC error body: a\n * `SilkweaveError` keeps its `statusCode` (so an invalid key surfaces as an\n * auth failure, not \"server has no tools\"), anything else is a 500. `id` is\n * echoed from the request body when available.\n */\nexport function filterErrorResponse(error: unknown, body: unknown): { status: number; body: object } {\n const message = (Array.isArray(body) ? body[0] : body) as { id?: unknown } | undefined\n const id = message?.id ?? null\n if (error instanceof SilkweaveError) {\n return {\n status: error.statusCode,\n body: { jsonrpc: '2.0', error: { code: -32_000, message: error.message }, id }\n }\n }\n console.error('filterActions error:', error)\n return {\n status: 500,\n body: { jsonrpc: '2.0', error: { code: -32_603, message: 'Internal server error' }, id }\n }\n}\n","import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js'\nimport type { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types.js'\nimport { Action, ActionRun, createLogger, emitToolCall, isStreamingAction, OnToolCall, runStreamingAction, SilkweaveContext, SilkweaveError, ToolCallEvent } from '@silkweave/core'\nimport { capitalCase, pascalCase } from 'change-case'\nimport { errorToolResult, handleToolError, jsonToolResult, smartToolResult, structuredToolResult } from '../util/result.js'\nimport { authStorage } from './auth.js'\n\ntype LogStream = NonNullable<Parameters<typeof createLogger>[0]>['stream']\ntype ToolExtra = RequestHandlerExtra<ServerRequest, ServerNotification>\n\nexport interface RegisterToolsOptions {\n /**\n * Where the per-call logger writes its human-readable stream. The `stdio`\n * adapter MUST pass `false` (stdout is the MCP protocol channel); the HTTP\n * transports default to `process.stderr`.\n */\n logStream?: LogStream\n /**\n * Telemetry hook invoked once per tool call (fire-and-forget; errors are\n * logged, never propagated). Fires after result formatting, so events carry\n * `resultBytes` (serialized raw-result size) and `sideloaded` (whether\n * `smartToolResult` offloaded to an embedded resource).\n */\n onToolCall?: OnToolCall\n}\n\n/**\n * Build the generic `request` context value (the same key REST/tRPC populate)\n * from the MCP SDK's `extra.requestInfo`, so transport-agnostic consumers - e.g.\n * `@silkweave/nestjs` `@UseGuards` guards reading\n * `switchToHttp().getRequest().headers` - work over MCP. There are no path\n * `params`/`query`/`body` on the raw MCP call, so those start as empty\n * stand-ins; `@silkweave/nestjs` later fills them from the validated tool input\n * per the reflected param sources (path -> `params`, `@Query` -> `query`,\n * `@Body` -> `body`) so guards reading any of them decide as they would over\n * REST. Returns `undefined` when no HTTP request info is available.\n */\nexport function requestFromExtra(requestInfo: { headers?: unknown; url?: { toString(): string } } | undefined) {\n if (!requestInfo) { return undefined }\n return { headers: requestInfo.headers ?? {}, url: requestInfo.url?.toString(), params: {}, query: {}, body: {} }\n}\n\n/** Per-call logger that bridges silkweave logs/progress onto MCP notifications. */\nfunction createToolLogger(extra: ToolExtra, stream: LogStream) {\n return createLogger({\n stream,\n onLog: (level, data) => {\n extra.sendNotification({ method: 'notifications/message', params: { level, data } })\n },\n onProgress: ({ progress, total, message }) => {\n if (!extra._meta?.progressToken) { return }\n extra.sendNotification({\n method: 'notifications/progress',\n params: { progress, total, message, progressToken: extra._meta.progressToken }\n })\n }\n })\n}\n\n/** Run the action, streaming chunks as progress notifications when a token is set. */\nasync function runAction(action: Action, input: object, context: SilkweaveContext, extra: ToolExtra): Promise<object | object[]> {\n const progressToken = extra._meta?.progressToken\n if (isStreamingAction(action)) {\n return runStreamingAction(action, input, context, progressToken\n ? async (chunk, index) => {\n await extra.sendNotification({\n method: 'notifications/progress',\n params: { progressToken, progress: index + 1, message: JSON.stringify(chunk) }\n })\n }\n : undefined)\n }\n return (action.run as ActionRun<object, object>)(input, context)\n}\n\n/**\n * Result path for a `disposition: 'structured'` action: parse the raw result\n * through the output schema and ship the PARSED data as `structuredContent`.\n * Parsing strips extra fields (so the client-side JSON-Schema validator, which\n * rejects `additionalProperties`, passes by construction) and a genuine\n * mismatch (missing required field, wrong type) degrades to an `isError` tool\n * result - which the SDK exempts from output validation - instead of an opaque\n * protocol error.\n */\nfunction structuredResult(action: Action, result: object | object[]) {\n const parsed = action.output!.safeParse(result)\n if (!parsed.success) {\n const fields = [...new Set(parsed.error.issues.map((issue) => issue.path.join('.') || '(root)'))].join(', ')\n return errorToolResult(new SilkweaveError(\n `Output validation failed for '${action.name}' at: ${fields}. The tool returned a shape that does not match its declared output schema - this is a server-side bug, not an input problem; retrying with different arguments will not help.`,\n 'output_validation_error'\n ))\n }\n return structuredToolResult(parsed.data as object)\n}\n\n/** MCP-only telemetry fields, computed only when a hook is registered. */\nfunction resultMeta(result: object | object[], formatted: { content?: { type: string }[] }): Pick<ToolCallEvent, 'resultBytes' | 'sideloaded'> {\n return {\n resultBytes: JSON.stringify(result).length,\n sideloaded: (formatted.content ?? []).some((block) => block.type === 'resource')\n }\n}\n\n/** Error identity for telemetry: a SilkweaveError's `code`, else the error's name. */\nfunction errorMeta(error: unknown): Pick<ToolCallEvent, 'errorCode' | 'errorMessage'> {\n if (error instanceof SilkweaveError) { return { errorCode: error.code, errorMessage: error.message } }\n if (error instanceof Error) { return { errorCode: error.name, errorMessage: error.message } }\n return { errorCode: 'unknown' }\n}\n\n/** Format via the action's `toolResult` hook, else the resolved disposition. */\nfunction formatToolResult(action: Action, result: object | object[], context: SilkweaveContext, disposition: unknown) {\n if (action.toolResult) {\n const response = action.toolResult(result, context)\n if (response) { return response }\n }\n // A structured action's output schema is a contract fixed at tools/list\n // time - a client's `_meta.disposition` cannot demote it.\n if (action.disposition === 'structured') { return structuredResult(action, result) }\n return disposition === 'smart' ? smartToolResult(result) : jsonToolResult(result)\n}\n\n/**\n * Register every action as an MCP tool on `server`. Shared by all MCP transports\n * (`stdio`, `http`, `edge`). Each tool call forks `context` with a per-call\n * `logger`, the SDK `extra`, a synthesized `request` (see `requestFromExtra`),\n * and - when bearer auth ran for this request - the resolved `auth`. The result\n * is formatted by the action's `toolResult` hook if present, otherwise per the\n * resolved disposition (client `_meta.disposition` > `action.disposition` >\n * `json`); a `'structured'` action always ships schema-parsed\n * `structuredContent` (its contract cannot be demoted per-call).\n */\nexport function registerTools(\n server: McpServer,\n actions: Action[],\n context: SilkweaveContext,\n options: RegisterToolsOptions = {}\n) {\n const stream = options.logStream ?? process.stderr\n for (const action of actions) {\n server.registerTool(pascalCase(action.name), {\n title: capitalCase(action.name),\n description: action.description,\n inputSchema: action.input,\n // Derived base (query ⇒ read-only), explicit annotations merged over.\n annotations: { readOnlyHint: action.kind === 'query', ...action.annotations },\n // Only structured actions declare an outputSchema contract - the SDK\n // enforces it server-side and SDK clients enforce it independently, so\n // forwarding is strictly opt-in via disposition.\n ...(action.disposition === 'structured' && action.output ? { outputSchema: action.output } : {})\n }, async (input, extra) => {\n const logger = createToolLogger(extra, stream)\n const currentAuth = authStorage.getStore()\n const actionContext = context.fork({\n logger,\n extra,\n request: requestFromExtra(extra.requestInfo),\n ...(currentAuth ? { auth: currentAuth } : {})\n })\n // Client-sent `_meta.disposition` wins; otherwise fall back to the\n // action's configured default (`json` when neither is set).\n const disposition = extra._meta?.disposition ?? action.disposition\n const base = { action: action.name, tool: pascalCase(action.name), transport: 'mcp' as const, context: actionContext }\n const started = Date.now()\n try {\n const result = await runAction(action, input, actionContext, extra)\n const formatted = formatToolResult(action, result, actionContext, disposition)\n emitToolCall(options.onToolCall, {\n ...base,\n durationMs: Date.now() - started,\n ok: formatted.isError !== true,\n ...(options.onToolCall ? resultMeta(result, formatted) : {})\n })\n return formatted\n } catch (error) {\n emitToolCall(options.onToolCall, { ...base, durationMs: Date.now() - started, ok: false, ...errorMeta(error) })\n return handleToolError(error)\n }\n })\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { Action, OnToolCall, SilkweaveContext, SilkweaveError, createLogger } from "@silkweave/core";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { CallToolResult, EmbeddedResource } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
|
|
5
|
+
//#region src/handlers/filter.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Normalized request stand-in handed to `filterActions` - the same shape for
|
|
8
|
+
* the Express `http()` transport and the Web-Standard `edge()` adapter.
|
|
9
|
+
*/
|
|
10
|
+
interface FilterRequest {
|
|
11
|
+
/** Inbound HTTP headers (lower-cased keys, as delivered by the host). */
|
|
12
|
+
headers: Record<string, string | string[] | undefined>;
|
|
13
|
+
/** Full request URL (or path, as delivered by the host). */
|
|
14
|
+
url: string;
|
|
15
|
+
/**
|
|
16
|
+
* JSON-RPC method of the POSTed message (`'initialize'`, `'tools/list'`,
|
|
17
|
+
* `'tools/call'`, `'ping'`, ...). Lets the callback skip expensive
|
|
18
|
+
* permission lookups on `initialize`/`ping`, and doubles as an
|
|
19
|
+
* observability tap (e.g. counting `tools/list`). For a legacy JSON-RPC
|
|
20
|
+
* batch this is the first request's method. Empty string when the body is
|
|
21
|
+
* not a recognizable JSON-RPC message (the transport will reject it anyway).
|
|
22
|
+
*/
|
|
23
|
+
method: string;
|
|
24
|
+
/** `params.name` of a `tools/call` message; unset for every other method. */
|
|
25
|
+
toolName?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Per-request tool filter for the stateless MCP transports. Runs before
|
|
29
|
+
* `registerTools()` on every `POST /mcp`; only the returned actions exist for
|
|
30
|
+
* that request (`tools/list` and `tools/call` alike - a client that cached a
|
|
31
|
+
* wider list is still denied). May be async (e.g. a DB lookup of API-key
|
|
32
|
+
* permissions).
|
|
33
|
+
*
|
|
34
|
+
* Error semantics: a thrown `SilkweaveError` propagates as its `statusCode`
|
|
35
|
+
* (e.g. 401 invalid key, 403 insufficient permissions) with a JSON-RPC error
|
|
36
|
+
* body; any other throw maps to HTTP 500. A thrown error NEVER degrades to an
|
|
37
|
+
* empty tool list - return `[]` explicitly if "no tools" is the intended
|
|
38
|
+
* answer.
|
|
39
|
+
*/
|
|
40
|
+
type FilterActions = (actions: Action[], request: FilterRequest) => Action[] | Promise<Action[]>;
|
|
41
|
+
/**
|
|
42
|
+
* Extract the JSON-RPC `method` (and `toolName` for `tools/call`) from a
|
|
43
|
+
* parsed request body. Tolerates a legacy batch (first request wins) and
|
|
44
|
+
* unrecognizable bodies (empty `method`).
|
|
45
|
+
*/
|
|
46
|
+
declare function rpcInfo(body: unknown): {
|
|
47
|
+
method: string;
|
|
48
|
+
toolName?: string;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Map a `filterActions` throw to an HTTP status + JSON-RPC error body: a
|
|
52
|
+
* `SilkweaveError` keeps its `statusCode` (so an invalid key surfaces as an
|
|
53
|
+
* auth failure, not "server has no tools"), anything else is a 500. `id` is
|
|
54
|
+
* echoed from the request body when available.
|
|
55
|
+
*/
|
|
56
|
+
declare function filterErrorResponse(error: unknown, body: unknown): {
|
|
57
|
+
status: number;
|
|
58
|
+
body: object;
|
|
59
|
+
};
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/handlers/registerTools.d.ts
|
|
62
|
+
type LogStream = NonNullable<Parameters<typeof createLogger>[0]>['stream'];
|
|
63
|
+
interface RegisterToolsOptions {
|
|
64
|
+
/**
|
|
65
|
+
* Where the per-call logger writes its human-readable stream. The `stdio`
|
|
66
|
+
* adapter MUST pass `false` (stdout is the MCP protocol channel); the HTTP
|
|
67
|
+
* transports default to `process.stderr`.
|
|
68
|
+
*/
|
|
69
|
+
logStream?: LogStream;
|
|
70
|
+
/**
|
|
71
|
+
* Telemetry hook invoked once per tool call (fire-and-forget; errors are
|
|
72
|
+
* logged, never propagated). Fires after result formatting, so events carry
|
|
73
|
+
* `resultBytes` (serialized raw-result size) and `sideloaded` (whether
|
|
74
|
+
* `smartToolResult` offloaded to an embedded resource).
|
|
75
|
+
*/
|
|
76
|
+
onToolCall?: OnToolCall;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Build the generic `request` context value (the same key REST/tRPC populate)
|
|
80
|
+
* from the MCP SDK's `extra.requestInfo`, so transport-agnostic consumers - e.g.
|
|
81
|
+
* `@silkweave/nestjs` `@UseGuards` guards reading
|
|
82
|
+
* `switchToHttp().getRequest().headers` - work over MCP. There are no path
|
|
83
|
+
* `params`/`query`/`body` on the raw MCP call, so those start as empty
|
|
84
|
+
* stand-ins; `@silkweave/nestjs` later fills them from the validated tool input
|
|
85
|
+
* per the reflected param sources (path -> `params`, `@Query` -> `query`,
|
|
86
|
+
* `@Body` -> `body`) so guards reading any of them decide as they would over
|
|
87
|
+
* REST. Returns `undefined` when no HTTP request info is available.
|
|
88
|
+
*/
|
|
89
|
+
declare function requestFromExtra(requestInfo: {
|
|
90
|
+
headers?: unknown;
|
|
91
|
+
url?: {
|
|
92
|
+
toString(): string;
|
|
93
|
+
};
|
|
94
|
+
} | undefined): {
|
|
95
|
+
headers: {};
|
|
96
|
+
url: string | undefined;
|
|
97
|
+
params: {};
|
|
98
|
+
query: {};
|
|
99
|
+
body: {};
|
|
100
|
+
} | undefined;
|
|
101
|
+
/**
|
|
102
|
+
* Register every action as an MCP tool on `server`. Shared by all MCP transports
|
|
103
|
+
* (`stdio`, `http`, `edge`). Each tool call forks `context` with a per-call
|
|
104
|
+
* `logger`, the SDK `extra`, a synthesized `request` (see `requestFromExtra`),
|
|
105
|
+
* and - when bearer auth ran for this request - the resolved `auth`. The result
|
|
106
|
+
* is formatted by the action's `toolResult` hook if present, otherwise per the
|
|
107
|
+
* resolved disposition (client `_meta.disposition` > `action.disposition` >
|
|
108
|
+
* `json`); a `'structured'` action always ships schema-parsed
|
|
109
|
+
* `structuredContent` (its contract cannot be demoted per-call).
|
|
110
|
+
*/
|
|
111
|
+
declare function registerTools(server: McpServer, actions: Action[], context: SilkweaveContext, options?: RegisterToolsOptions): void;
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/util/result.d.ts
|
|
114
|
+
declare function smartToolResult(data: string | object | object[]): CallToolResult;
|
|
115
|
+
/**
|
|
116
|
+
* Result formatter for `disposition: 'structured'` actions: the (already
|
|
117
|
+
* schema-parsed) data ships as `structuredContent` with a compact JSON text
|
|
118
|
+
* mirror, per the spec's backwards-compat recommendation. Callers must pass
|
|
119
|
+
* the OUTPUT-SCHEMA-PARSED data, not the raw result - parsing strips extra
|
|
120
|
+
* fields, which is what keeps client-side JSON-Schema validation
|
|
121
|
+
* (`additionalProperties: false`) passing by construction.
|
|
122
|
+
*/
|
|
123
|
+
declare function structuredToolResult(data: object): CallToolResult;
|
|
124
|
+
declare function jsonToolResult(data: object, isError?: boolean): CallToolResult;
|
|
125
|
+
declare function errorToolResult({
|
|
126
|
+
code,
|
|
127
|
+
name,
|
|
128
|
+
message
|
|
129
|
+
}: SilkweaveError): CallToolResult;
|
|
130
|
+
declare function handleToolError(error: unknown): CallToolResult;
|
|
131
|
+
declare function parseResourceMessage({
|
|
132
|
+
resource
|
|
133
|
+
}: EmbeddedResource): string;
|
|
134
|
+
//#endregion
|
|
135
|
+
export { smartToolResult as a, registerTools as c, FilterRequest as d, filterErrorResponse as f, parseResourceMessage as i, requestFromExtra as l, handleToolError as n, structuredToolResult as o, rpcInfo as p, jsonToolResult as r, RegisterToolsOptions as s, errorToolResult as t, FilterActions as u };
|
|
136
|
+
//# sourceMappingURL=result-BN0lxM6Q.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"result-BN0lxM6Q.d.mts","names":[],"sources":["../src/handlers/filter.ts","../src/handlers/registerTools.ts","../src/util/result.ts"],"mappings":";;;;;;;;;UAMiB,aAAA;EAAa;EAE5B,OAAA,EAAS,MAAA;EAAM;EAEf,GAAA;EAFS;;;;;;AA6BX;;EAlBE,MAAA;EAkBoC;EAhBpC,QAAA;AAAA;;;;;;;;;;;;;;KAgBU,aAAA,IAAiB,OAAA,EAAS,MAAA,IAAU,OAAA,EAAS,aAAA,KAAkB,MAAA,KAAW,OAAA,CAAQ,MAAA;;;;;;iBAO9E,OAAA,CAAQ,IAAA;EAAkB,MAAA;EAAgB,QAAA;AAAA;;;;;;;iBAa1C,mBAAA,CAAoB,KAAA,WAAgB,IAAA;EAAkB,MAAA;EAAgB,IAAA;AAAA;;;KCjDjF,SAAA,GAAY,WAAA,CAAY,UAAA,QAAkB,YAAA;AAAA,UAG9B,oBAAA;;ADLjB;;;;ECWE,SAAA,GAAY,SAAA;EDTH;;;;;;ECgBT,UAAA,GAAa,UAAA;AAAA;;;;;;;;;;;;iBAcC,gBAAA,CAAiB,WAAA;EAAe,OAAA;EAAmB,GAAA;IAAQ,QAAA;EAAA;AAAA;;;;;;;;;;ADmB3E;;;;;;;iBC6EgB,aAAA,CACd,MAAA,EAAQ,SAAA,EACR,OAAA,EAAS,MAAA,IACT,OAAA,EAAS,gBAAA,EACT,OAAA,GAAS,oBAAA;;;iBCtIK,eAAA,CAAgB,IAAA,+BAAmC,cAAA;;;AFEnE;;;;;;iBE2BgB,oBAAA,CAAqB,IAAA,WAAe,cAAA;AAAA,iBAOpC,cAAA,CAAe,IAAA,UAAc,OAAA,aAAkB,cAAA;AAAA,iBAM/C,eAAA,CAAA;EAAkB,IAAA;EAAM,IAAA;EAAM;AAAA,GAAW,cAAA,GAAiB,cAAA;AAAA,iBAU1D,eAAA,CAAgB,KAAA,YAAiB,cAAA;AAAA,iBAejC,oBAAA,CAAA;EAAuB;AAAA,GAAY,gBAAA"}
|
|
@@ -25,6 +25,23 @@ function smartToolResult(data) {
|
|
|
25
25
|
text
|
|
26
26
|
}] };
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Result formatter for `disposition: 'structured'` actions: the (already
|
|
30
|
+
* schema-parsed) data ships as `structuredContent` with a compact JSON text
|
|
31
|
+
* mirror, per the spec's backwards-compat recommendation. Callers must pass
|
|
32
|
+
* the OUTPUT-SCHEMA-PARSED data, not the raw result - parsing strips extra
|
|
33
|
+
* fields, which is what keeps client-side JSON-Schema validation
|
|
34
|
+
* (`additionalProperties: false`) passing by construction.
|
|
35
|
+
*/
|
|
36
|
+
function structuredToolResult(data) {
|
|
37
|
+
return {
|
|
38
|
+
content: [{
|
|
39
|
+
type: "text",
|
|
40
|
+
text: JSON.stringify(data)
|
|
41
|
+
}],
|
|
42
|
+
structuredContent: data
|
|
43
|
+
};
|
|
44
|
+
}
|
|
28
45
|
function jsonToolResult(data, isError = false) {
|
|
29
46
|
const result = { content: [{
|
|
30
47
|
type: "text",
|
|
@@ -74,6 +91,6 @@ function parseResourceMessage({ resource }) {
|
|
|
74
91
|
return "blob" in resource ? Buffer.from(resource.blob, "base64").toString("utf-8") : resource.text;
|
|
75
92
|
}
|
|
76
93
|
//#endregion
|
|
77
|
-
export { smartToolResult as a, parseResourceMessage as i, handleToolError as n, jsonToolResult as r, errorToolResult as t };
|
|
94
|
+
export { smartToolResult as a, parseResourceMessage as i, handleToolError as n, structuredToolResult as o, jsonToolResult as r, errorToolResult as t };
|
|
78
95
|
|
|
79
|
-
//# sourceMappingURL=result-
|
|
96
|
+
//# sourceMappingURL=result-CXgeE8ca.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"result-CXgeE8ca.mjs","names":[],"sources":["../src/util/result.ts"],"sourcesContent":["import { EmbeddedResource, type CallToolResult } from '@modelcontextprotocol/sdk/types.js'\nimport { SilkweaveError } from '@silkweave/core'\nimport { randomUUID } from 'node:crypto'\n\nexport function smartToolResult(data: string | object | object[]): CallToolResult {\n const text = typeof data === 'string' ? data : JSON.stringify(data)\n const mimeType = typeof data === 'string' ? 'text/plain' : 'application/json'\n const ext = typeof data === 'string' ? 'txt' : 'json'\n if (text.length > 4096) {\n const uri = `mcp://toolResult/${randomUUID()}.${ext}`\n const buffer = Buffer.from(text)\n const blob = buffer.toString('base64')\n return {\n content: [\n { type: 'text', text: `Received resource ${uri} with ${buffer.byteLength} bytes` },\n { type: 'resource', resource: { uri, mimeType, blob } }\n ]\n }\n } else {\n return {\n content: [{ type: 'text' as const, text }]\n }\n }\n}\n\n/**\n * Result formatter for `disposition: 'structured'` actions: the (already\n * schema-parsed) data ships as `structuredContent` with a compact JSON text\n * mirror, per the spec's backwards-compat recommendation. Callers must pass\n * the OUTPUT-SCHEMA-PARSED data, not the raw result - parsing strips extra\n * fields, which is what keeps client-side JSON-Schema validation\n * (`additionalProperties: false`) passing by construction.\n */\nexport function structuredToolResult(data: object): CallToolResult {\n return {\n content: [{ type: 'text' as const, text: JSON.stringify(data) }],\n structuredContent: data as Record<string, unknown>\n }\n}\n\nexport function jsonToolResult(data: object, isError = false): CallToolResult {\n const result: CallToolResult = { content: [{ type: 'text' as const, text: JSON.stringify(data) }] }\n if (isError) { result.isError = true }\n return result\n}\n\nexport function errorToolResult({ code, name, message }: SilkweaveError): CallToolResult {\n return {\n isError: true,\n content: [{\n type: 'text',\n text: JSON.stringify({ success: false, code, name, message })\n }]\n }\n}\n\nexport function handleToolError(error: unknown): CallToolResult {\n if (error instanceof SilkweaveError) {\n return jsonToolResult({ success: false, name: error.name, message: error.message, code: error.code }, true)\n } else if (error instanceof Error) {\n // Log the full error (incl. stack) to stderr only - never put the stack trace\n // on the wire, where it would leak server internals to the MCP client.\n console.error(error)\n return jsonToolResult({ success: false, name: error.name, message: error.message }, true)\n } else {\n // Likewise keep the raw thrown value server-side - only a generic message goes out.\n console.error('Unknown tool error:', error)\n return jsonToolResult({ success: false, name: 'Unknown error', message: 'An unknown error occurred' }, true)\n }\n}\n\nexport function parseResourceMessage({ resource }: EmbeddedResource) {\n const text = ('blob' in resource) ? Buffer.from(resource.blob, 'base64').toString('utf-8') : resource.text\n return text\n}\n"],"mappings":";;;AAIA,SAAgB,gBAAgB,MAAkD;CAChF,MAAM,OAAO,OAAO,SAAS,WAAW,OAAO,KAAK,UAAU,KAAK;CACnE,MAAM,WAAW,OAAO,SAAS,WAAW,eAAe;CAC3D,MAAM,MAAM,OAAO,SAAS,WAAW,QAAQ;AAC/C,KAAI,KAAK,SAAS,MAAM;EACtB,MAAM,MAAM,oBAAoB,YAAY,CAAC,GAAG;EAChD,MAAM,SAAS,OAAO,KAAK,KAAK;EAChC,MAAM,OAAO,OAAO,SAAS,SAAS;AACtC,SAAO,EACL,SAAS,CACP;GAAE,MAAM;GAAQ,MAAM,qBAAqB,IAAI,QAAQ,OAAO,WAAW;GAAS,EAClF;GAAE,MAAM;GAAY,UAAU;IAAE;IAAK;IAAU;IAAM;GAAE,CACxD,EACF;OAED,QAAO,EACL,SAAS,CAAC;EAAE,MAAM;EAAiB;EAAM,CAAC,EAC3C;;;;;;;;;;AAYL,SAAgB,qBAAqB,MAA8B;AACjE,QAAO;EACL,SAAS,CAAC;GAAE,MAAM;GAAiB,MAAM,KAAK,UAAU,KAAK;GAAE,CAAC;EAChE,mBAAmB;EACpB;;AAGH,SAAgB,eAAe,MAAc,UAAU,OAAuB;CAC5E,MAAM,SAAyB,EAAE,SAAS,CAAC;EAAE,MAAM;EAAiB,MAAM,KAAK,UAAU,KAAK;EAAE,CAAC,EAAE;AACnG,KAAI,QAAW,QAAO,UAAU;AAChC,QAAO;;AAGT,SAAgB,gBAAgB,EAAE,MAAM,MAAM,WAA2C;AACvF,QAAO;EACL,SAAS;EACT,SAAS,CAAC;GACR,MAAM;GACN,MAAM,KAAK,UAAU;IAAE,SAAS;IAAO;IAAM;IAAM;IAAS,CAAC;GAC9D,CAAC;EACH;;AAGH,SAAgB,gBAAgB,OAAgC;AAC9D,KAAI,iBAAiB,eACnB,QAAO,eAAe;EAAE,SAAS;EAAO,MAAM,MAAM;EAAM,SAAS,MAAM;EAAS,MAAM,MAAM;EAAM,EAAE,KAAK;UAClG,iBAAiB,OAAO;AAGjC,UAAQ,MAAM,MAAM;AACpB,SAAO,eAAe;GAAE,SAAS;GAAO,MAAM,MAAM;GAAM,SAAS,MAAM;GAAS,EAAE,KAAK;QACpF;AAEL,UAAQ,MAAM,uBAAuB,MAAM;AAC3C,SAAO,eAAe;GAAE,SAAS;GAAO,MAAM;GAAiB,SAAS;GAA6B,EAAE,KAAK;;;AAIhH,SAAgB,qBAAqB,EAAE,YAA8B;AAEnE,QADc,UAAU,WAAY,OAAO,KAAK,SAAS,MAAM,SAAS,CAAC,SAAS,QAAQ,GAAG,SAAS"}
|
package/build/tools.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as smartToolResult, c as
|
|
2
|
-
export { RegisterToolsOptions, errorToolResult, handleToolError, jsonToolResult, parseResourceMessage, registerTools, requestFromExtra, smartToolResult };
|
|
1
|
+
import { a as smartToolResult, c as registerTools, d as FilterRequest, f as filterErrorResponse, i as parseResourceMessage, l as requestFromExtra, n as handleToolError, o as structuredToolResult, p as rpcInfo, r as jsonToolResult, s as RegisterToolsOptions, t as errorToolResult, u as FilterActions } from "./result-BN0lxM6Q.mjs";
|
|
2
|
+
export { FilterActions, FilterRequest, RegisterToolsOptions, errorToolResult, filterErrorResponse, handleToolError, jsonToolResult, parseResourceMessage, registerTools, requestFromExtra, rpcInfo, smartToolResult, structuredToolResult };
|
package/build/tools.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { n as requestFromExtra, t as registerTools } from "./registerTools-
|
|
2
|
-
import { a as smartToolResult, i as parseResourceMessage, n as handleToolError, r as jsonToolResult, t as errorToolResult } from "./result-
|
|
3
|
-
export { errorToolResult, handleToolError, jsonToolResult, parseResourceMessage, registerTools, requestFromExtra, smartToolResult };
|
|
1
|
+
import { i as rpcInfo, n as requestFromExtra, r as filterErrorResponse, t as registerTools } from "./registerTools-DkaraOyx.mjs";
|
|
2
|
+
import { a as smartToolResult, i as parseResourceMessage, n as handleToolError, o as structuredToolResult, r as jsonToolResult, t as errorToolResult } from "./result-CXgeE8ca.mjs";
|
|
3
|
+
export { errorToolResult, filterErrorResponse, handleToolError, jsonToolResult, parseResourceMessage, registerTools, requestFromExtra, rpcInfo, smartToolResult, structuredToolResult };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@silkweave/mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Silkweave MCP Package",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://www.silkweave.dev",
|
|
@@ -39,20 +39,15 @@
|
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
40
40
|
"change-case": "^5.4.4",
|
|
41
41
|
"zod": "^3.25.0",
|
|
42
|
-
"@silkweave/
|
|
43
|
-
"@silkweave/
|
|
44
|
-
"@silkweave/auth": "3.0.0"
|
|
42
|
+
"@silkweave/auth": "3.2.0",
|
|
43
|
+
"@silkweave/core": "3.2.0"
|
|
45
44
|
},
|
|
46
45
|
"peerDependencies": {
|
|
47
|
-
"@clack/prompts": "^1.0.1",
|
|
48
46
|
"commander": "^13.1.0",
|
|
49
47
|
"cors": "^2.8.5",
|
|
50
48
|
"express": "^5.0.0"
|
|
51
49
|
},
|
|
52
50
|
"peerDependenciesMeta": {
|
|
53
|
-
"@clack/prompts": {
|
|
54
|
-
"optional": true
|
|
55
|
-
},
|
|
56
51
|
"commander": {
|
|
57
52
|
"optional": true
|
|
58
53
|
},
|
|
@@ -64,7 +59,6 @@
|
|
|
64
59
|
}
|
|
65
60
|
},
|
|
66
61
|
"devDependencies": {
|
|
67
|
-
"@clack/prompts": "^1.0.1",
|
|
68
62
|
"@eslint/js": "^10.0.1",
|
|
69
63
|
"@modelcontextprotocol/inspector": "^0.21.1",
|
|
70
64
|
"@stylistic/eslint-plugin": "^5.10.0",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"registerTools-DlguElKV.mjs","names":[],"sources":["../src/handlers/auth.ts","../src/handlers/registerTools.ts"],"sourcesContent":["import { AuthConfig, AuthInfo, validateToken } from '@silkweave/auth'\nimport { SilkweaveContext } from '@silkweave/core'\nimport { type RequestHandler } from 'express'\nimport { AsyncLocalStorage } from 'node:async_hooks'\n\n/**\n * Per-request bearer-token storage used by tool handlers to read the resolved\n * `AuthInfo` for the currently-handled MCP call.\n */\nexport const authStorage = new AsyncLocalStorage<AuthInfo>()\n\n/**\n * Express middleware that validates the `Authorization: Bearer …` header via\n * the supplied `AuthConfig`. On success the resolved `AuthInfo` is placed in\n * `authStorage` for the duration of the downstream handler - `mcpTransport`'s\n * tool callbacks pick it up to populate the silkweave context's `auth` key.\n *\n * The middleware should NOT be applied to OAuth-discovery / token routes -\n * compose it only on the routes that require an authenticated caller (the MCP\n * transport itself, sideload, etc.).\n */\nexport function authMiddleware(auth: AuthConfig, context: SilkweaveContext): RequestHandler {\n return async (req, res, next) => {\n const result = await validateToken(req.headers.authorization, auth, context.fork({ request: req }))\n if (result.error) {\n for (const [key, value] of Object.entries(result.error.headers)) { res.header(key, value) }\n res.status(result.error.statusCode).json(result.error.body)\n return\n }\n if (result.auth) {\n authStorage.run(result.auth, () => { next() })\n } else {\n next()\n }\n }\n}\n","import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js'\nimport type { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types.js'\nimport { Action, ActionRun, isStreamingAction, runStreamingAction, SilkweaveContext } from '@silkweave/core'\nimport { createLogger } from '@silkweave/logger'\nimport { capitalCase, pascalCase } from 'change-case'\nimport { handleToolError, jsonToolResult, smartToolResult } from '../util/result.js'\nimport { authStorage } from './auth.js'\n\ntype LogStream = NonNullable<Parameters<typeof createLogger>[0]>['stream']\ntype ToolExtra = RequestHandlerExtra<ServerRequest, ServerNotification>\n\nexport interface RegisterToolsOptions {\n /**\n * Where the per-call logger writes its human-readable stream. The `stdio`\n * adapter MUST pass `false` (stdout is the MCP protocol channel); the HTTP\n * transports default to `process.stderr`.\n */\n logStream?: LogStream\n}\n\n/**\n * Build the generic `request` context value (the same key REST/tRPC populate)\n * from the MCP SDK's `extra.requestInfo`, so transport-agnostic consumers - e.g.\n * `@silkweave/nestjs` `@UseGuards` guards reading\n * `switchToHttp().getRequest().headers` - work over MCP. There are no path\n * `params`/`query`/`body` on the raw MCP call, so those start as empty\n * stand-ins; `@silkweave/nestjs` later fills them from the validated tool input\n * per the reflected param sources (path -> `params`, `@Query` -> `query`,\n * `@Body` -> `body`) so guards reading any of them decide as they would over\n * REST. Returns `undefined` when no HTTP request info is available.\n */\nexport function requestFromExtra(requestInfo: { headers?: unknown; url?: { toString(): string } } | undefined) {\n if (!requestInfo) { return undefined }\n return { headers: requestInfo.headers ?? {}, url: requestInfo.url?.toString(), params: {}, query: {}, body: {} }\n}\n\n/** Per-call logger that bridges silkweave logs/progress onto MCP notifications. */\nfunction createToolLogger(extra: ToolExtra, stream: LogStream) {\n return createLogger({\n stream,\n onLog: (level, data) => {\n extra.sendNotification({ method: 'notifications/message', params: { level, data } })\n },\n onProgress: ({ progress, total, message }) => {\n if (!extra._meta?.progressToken) { return }\n extra.sendNotification({\n method: 'notifications/progress',\n params: { progress, total, message, progressToken: extra._meta.progressToken }\n })\n }\n })\n}\n\n/** Run the action, streaming chunks as progress notifications when a token is set. */\nasync function runAction(action: Action, input: object, context: SilkweaveContext, extra: ToolExtra): Promise<object | object[]> {\n const progressToken = extra._meta?.progressToken\n if (isStreamingAction(action)) {\n return runStreamingAction(action, input, context, progressToken\n ? async (chunk, index) => {\n await extra.sendNotification({\n method: 'notifications/progress',\n params: { progressToken, progress: index + 1, message: JSON.stringify(chunk) }\n })\n }\n : undefined)\n }\n return (action.run as ActionRun<object, object>)(input, context)\n}\n\n/** Format via the action's `toolResult` hook, else the resolved disposition. */\nfunction formatToolResult(action: Action, result: object | object[], context: SilkweaveContext, disposition: unknown) {\n if (action.toolResult) {\n const response = action.toolResult(result, context)\n if (response) { return response }\n }\n return disposition === 'json' ? jsonToolResult(result) : smartToolResult(result)\n}\n\n/**\n * Register every action as an MCP tool on `server`. Shared by all MCP transports\n * (`stdio`, `http`, `edge`). Each tool call forks `context` with a per-call\n * `logger`, the SDK `extra`, a synthesized `request` (see `requestFromExtra`),\n * and - when bearer auth ran for this request - the resolved `auth`. The result\n * is formatted by the action's `toolResult` hook if present, otherwise per the\n * resolved disposition (client `_meta.disposition` > `action.disposition` >\n * `smart`).\n */\nexport function registerTools(\n server: McpServer,\n actions: Action[],\n context: SilkweaveContext,\n options: RegisterToolsOptions = {}\n) {\n const stream = options.logStream ?? process.stderr\n for (const action of actions) {\n server.registerTool(pascalCase(action.name), {\n title: capitalCase(action.name),\n description: action.description,\n inputSchema: action.input\n }, async (input, extra) => {\n const logger = createToolLogger(extra, stream)\n const currentAuth = authStorage.getStore()\n const actionContext = context.fork({\n logger,\n extra,\n request: requestFromExtra(extra.requestInfo),\n ...(currentAuth ? { auth: currentAuth } : {})\n })\n // Client-sent `_meta.disposition` wins; otherwise fall back to the\n // action's configured default (`smart` when neither is set).\n const disposition = extra._meta?.disposition ?? action.disposition\n try {\n const result = await runAction(action, input, actionContext, extra)\n return formatToolResult(action, result, actionContext, disposition)\n } catch (error) {\n return handleToolError(error)\n }\n })\n }\n}\n"],"mappings":";;;;;;;;;;;AASA,MAAa,cAAc,IAAI,mBAA6B;;;;;;;;;;;AAY5D,SAAgB,eAAe,MAAkB,SAA2C;AAC1F,QAAO,OAAO,KAAK,KAAK,SAAS;EAC/B,MAAM,SAAS,MAAM,cAAc,IAAI,QAAQ,eAAe,MAAM,QAAQ,KAAK,EAAE,SAAS,KAAK,CAAC,CAAC;AACnG,MAAI,OAAO,OAAO;AAChB,QAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,MAAM,QAAQ,CAAI,KAAI,OAAO,KAAK,MAAM;AACzF,OAAI,OAAO,OAAO,MAAM,WAAW,CAAC,KAAK,OAAO,MAAM,KAAK;AAC3D;;AAEF,MAAI,OAAO,KACT,aAAY,IAAI,OAAO,YAAY;AAAE,SAAM;IAAG;MAE9C,OAAM;;;;;;;;;;;;;;;;ACAZ,SAAgB,iBAAiB,aAA8E;AAC7G,KAAI,CAAC,YAAe;AACpB,QAAO;EAAE,SAAS,YAAY,WAAW,EAAE;EAAE,KAAK,YAAY,KAAK,UAAU;EAAE,QAAQ,EAAE;EAAE,OAAO,EAAE;EAAE,MAAM,EAAE;EAAE;;;AAIlH,SAAS,iBAAiB,OAAkB,QAAmB;AAC7D,QAAO,aAAa;EAClB;EACA,QAAQ,OAAO,SAAS;AACtB,SAAM,iBAAiB;IAAE,QAAQ;IAAyB,QAAQ;KAAE;KAAO;KAAM;IAAE,CAAC;;EAEtF,aAAa,EAAE,UAAU,OAAO,cAAc;AAC5C,OAAI,CAAC,MAAM,OAAO,cAAiB;AACnC,SAAM,iBAAiB;IACrB,QAAQ;IACR,QAAQ;KAAE;KAAU;KAAO;KAAS,eAAe,MAAM,MAAM;KAAe;IAC/E,CAAC;;EAEL,CAAC;;;AAIJ,eAAe,UAAU,QAAgB,OAAe,SAA2B,OAA8C;CAC/H,MAAM,gBAAgB,MAAM,OAAO;AACnC,KAAI,kBAAkB,OAAO,CAC3B,QAAO,mBAAmB,QAAQ,OAAO,SAAS,gBAC9C,OAAO,OAAO,UAAU;AACxB,QAAM,MAAM,iBAAiB;GAC3B,QAAQ;GACR,QAAQ;IAAE;IAAe,UAAU,QAAQ;IAAG,SAAS,KAAK,UAAU,MAAM;IAAE;GAC/E,CAAC;KAEF,KAAA,EAAU;AAEhB,QAAQ,OAAO,IAAkC,OAAO,QAAQ;;;AAIlE,SAAS,iBAAiB,QAAgB,QAA2B,SAA2B,aAAsB;AACpH,KAAI,OAAO,YAAY;EACrB,MAAM,WAAW,OAAO,WAAW,QAAQ,QAAQ;AACnD,MAAI,SAAY,QAAO;;AAEzB,QAAO,gBAAgB,SAAS,eAAe,OAAO,GAAG,gBAAgB,OAAO;;;;;;;;;;;AAYlF,SAAgB,cACd,QACA,SACA,SACA,UAAgC,EAAE,EAClC;CACA,MAAM,SAAS,QAAQ,aAAa,QAAQ;AAC5C,MAAK,MAAM,UAAU,QACnB,QAAO,aAAa,WAAW,OAAO,KAAK,EAAE;EAC3C,OAAO,YAAY,OAAO,KAAK;EAC/B,aAAa,OAAO;EACpB,aAAa,OAAO;EACrB,EAAE,OAAO,OAAO,UAAU;EACzB,MAAM,SAAS,iBAAiB,OAAO,OAAO;EAC9C,MAAM,cAAc,YAAY,UAAU;EAC1C,MAAM,gBAAgB,QAAQ,KAAK;GACjC;GACA;GACA,SAAS,iBAAiB,MAAM,YAAY;GAC5C,GAAI,cAAc,EAAE,MAAM,aAAa,GAAG,EAAE;GAC7C,CAAC;EAGF,MAAM,cAAc,MAAM,OAAO,eAAe,OAAO;AACvD,MAAI;AAEF,UAAO,iBAAiB,QAAQ,MADX,UAAU,QAAQ,OAAO,eAAe,MAAM,EAC3B,eAAe,YAAY;WAC5D,OAAO;AACd,UAAO,gBAAgB,MAAM;;GAE/B"}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { Action, SilkweaveContext, SilkweaveError } from "@silkweave/core";
|
|
2
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
import { createLogger } from "@silkweave/logger";
|
|
4
|
-
import { CallToolResult, EmbeddedResource } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
-
|
|
6
|
-
//#region src/handlers/registerTools.d.ts
|
|
7
|
-
type LogStream = NonNullable<Parameters<typeof createLogger>[0]>['stream'];
|
|
8
|
-
interface RegisterToolsOptions {
|
|
9
|
-
/**
|
|
10
|
-
* Where the per-call logger writes its human-readable stream. The `stdio`
|
|
11
|
-
* adapter MUST pass `false` (stdout is the MCP protocol channel); the HTTP
|
|
12
|
-
* transports default to `process.stderr`.
|
|
13
|
-
*/
|
|
14
|
-
logStream?: LogStream;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Build the generic `request` context value (the same key REST/tRPC populate)
|
|
18
|
-
* from the MCP SDK's `extra.requestInfo`, so transport-agnostic consumers - e.g.
|
|
19
|
-
* `@silkweave/nestjs` `@UseGuards` guards reading
|
|
20
|
-
* `switchToHttp().getRequest().headers` - work over MCP. There are no path
|
|
21
|
-
* `params`/`query`/`body` on the raw MCP call, so those start as empty
|
|
22
|
-
* stand-ins; `@silkweave/nestjs` later fills them from the validated tool input
|
|
23
|
-
* per the reflected param sources (path -> `params`, `@Query` -> `query`,
|
|
24
|
-
* `@Body` -> `body`) so guards reading any of them decide as they would over
|
|
25
|
-
* REST. Returns `undefined` when no HTTP request info is available.
|
|
26
|
-
*/
|
|
27
|
-
declare function requestFromExtra(requestInfo: {
|
|
28
|
-
headers?: unknown;
|
|
29
|
-
url?: {
|
|
30
|
-
toString(): string;
|
|
31
|
-
};
|
|
32
|
-
} | undefined): {
|
|
33
|
-
headers: {};
|
|
34
|
-
url: string | undefined;
|
|
35
|
-
params: {};
|
|
36
|
-
query: {};
|
|
37
|
-
body: {};
|
|
38
|
-
} | undefined;
|
|
39
|
-
/**
|
|
40
|
-
* Register every action as an MCP tool on `server`. Shared by all MCP transports
|
|
41
|
-
* (`stdio`, `http`, `edge`). Each tool call forks `context` with a per-call
|
|
42
|
-
* `logger`, the SDK `extra`, a synthesized `request` (see `requestFromExtra`),
|
|
43
|
-
* and - when bearer auth ran for this request - the resolved `auth`. The result
|
|
44
|
-
* is formatted by the action's `toolResult` hook if present, otherwise per the
|
|
45
|
-
* resolved disposition (client `_meta.disposition` > `action.disposition` >
|
|
46
|
-
* `smart`).
|
|
47
|
-
*/
|
|
48
|
-
declare function registerTools(server: McpServer, actions: Action[], context: SilkweaveContext, options?: RegisterToolsOptions): void;
|
|
49
|
-
//#endregion
|
|
50
|
-
//#region src/util/result.d.ts
|
|
51
|
-
declare function smartToolResult(data: string | object | object[]): CallToolResult;
|
|
52
|
-
declare function jsonToolResult(data: object, isError?: boolean): CallToolResult;
|
|
53
|
-
declare function errorToolResult({
|
|
54
|
-
code,
|
|
55
|
-
name,
|
|
56
|
-
message
|
|
57
|
-
}: SilkweaveError): CallToolResult;
|
|
58
|
-
declare function handleToolError(error: unknown): CallToolResult;
|
|
59
|
-
declare function parseResourceMessage({
|
|
60
|
-
resource
|
|
61
|
-
}: EmbeddedResource): string;
|
|
62
|
-
//#endregion
|
|
63
|
-
export { smartToolResult as a, requestFromExtra as c, parseResourceMessage as i, handleToolError as n, RegisterToolsOptions as o, jsonToolResult as r, registerTools as s, errorToolResult as t };
|
|
64
|
-
//# sourceMappingURL=result-BVpEX7jU.d.mts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"result-BVpEX7jU.d.mts","names":[],"sources":["../src/handlers/registerTools.ts","../src/util/result.ts"],"mappings":";;;;;;KASK,SAAA,GAAY,WAAA,CAAY,UAAA,QAAkB,YAAA;AAAA,UAG9B,oBAAA;;AAR+B;;;;EAc9C,SAAA,GAAY,SAAA;AAAA;;;;;;;;AANd;;;;iBAoBgB,gBAAA,CAAiB,WAAA;EAAe,OAAA;EAAmB,GAAA;IAAQ,QAAA;EAAA;AAAA;;;;;;;;;;;AAwD3E;;;;;iBAAgB,aAAA,CACd,MAAA,EAAQ,SAAA,EACR,OAAA,EAAS,MAAA,IACT,OAAA,EAAS,gBAAA,EACT,OAAA,GAAS,oBAAA;;;iBCxFK,eAAA,CAAgB,IAAA,+BAAmC,cAAA;AAAA,iBAqBnD,cAAA,CAAe,IAAA,UAAc,OAAA,aAAkB,cAAA;AAAA,iBAM/C,eAAA,CAAA;EAAkB,IAAA;EAAM,IAAA;EAAM;AAAA,GAAW,cAAA,GAAiB,cAAA;AAAA,iBAU1D,eAAA,CAAgB,KAAA,YAAiB,cAAA;AAAA,iBAejC,oBAAA,CAAA;EAAuB;AAAA,GAAY,gBAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"result-B_9Eo1ey.mjs","names":[],"sources":["../src/util/result.ts"],"sourcesContent":["import { EmbeddedResource, type CallToolResult } from '@modelcontextprotocol/sdk/types.js'\nimport { SilkweaveError } from '@silkweave/core'\nimport { randomUUID } from 'node:crypto'\n\nexport function smartToolResult(data: string | object | object[]): CallToolResult {\n const text = typeof data === 'string' ? data : JSON.stringify(data)\n const mimeType = typeof data === 'string' ? 'text/plain' : 'application/json'\n const ext = typeof data === 'string' ? 'txt' : 'json'\n if (text.length > 4096) {\n const uri = `mcp://toolResult/${randomUUID()}.${ext}`\n const buffer = Buffer.from(text)\n const blob = buffer.toString('base64')\n return {\n content: [\n { type: 'text', text: `Received resource ${uri} with ${buffer.byteLength} bytes` },\n { type: 'resource', resource: { uri, mimeType, blob } }\n ]\n }\n } else {\n return {\n content: [{ type: 'text' as const, text }]\n }\n }\n}\n\nexport function jsonToolResult(data: object, isError = false): CallToolResult {\n const result: CallToolResult = { content: [{ type: 'text' as const, text: JSON.stringify(data) }] }\n if (isError) { result.isError = true }\n return result\n}\n\nexport function errorToolResult({ code, name, message }: SilkweaveError): CallToolResult {\n return {\n isError: true,\n content: [{\n type: 'text',\n text: JSON.stringify({ success: false, code, name, message })\n }]\n }\n}\n\nexport function handleToolError(error: unknown): CallToolResult {\n if (error instanceof SilkweaveError) {\n return jsonToolResult({ success: false, name: error.name, message: error.message, code: error.code }, true)\n } else if (error instanceof Error) {\n // Log the full error (incl. stack) to stderr only - never put the stack trace\n // on the wire, where it would leak server internals to the MCP client.\n console.error(error)\n return jsonToolResult({ success: false, name: error.name, message: error.message }, true)\n } else {\n // Likewise keep the raw thrown value server-side - only a generic message goes out.\n console.error('Unknown tool error:', error)\n return jsonToolResult({ success: false, name: 'Unknown error', message: 'An unknown error occurred' }, true)\n }\n}\n\nexport function parseResourceMessage({ resource }: EmbeddedResource) {\n const text = ('blob' in resource) ? Buffer.from(resource.blob, 'base64').toString('utf-8') : resource.text\n return text\n}\n"],"mappings":";;;AAIA,SAAgB,gBAAgB,MAAkD;CAChF,MAAM,OAAO,OAAO,SAAS,WAAW,OAAO,KAAK,UAAU,KAAK;CACnE,MAAM,WAAW,OAAO,SAAS,WAAW,eAAe;CAC3D,MAAM,MAAM,OAAO,SAAS,WAAW,QAAQ;AAC/C,KAAI,KAAK,SAAS,MAAM;EACtB,MAAM,MAAM,oBAAoB,YAAY,CAAC,GAAG;EAChD,MAAM,SAAS,OAAO,KAAK,KAAK;EAChC,MAAM,OAAO,OAAO,SAAS,SAAS;AACtC,SAAO,EACL,SAAS,CACP;GAAE,MAAM;GAAQ,MAAM,qBAAqB,IAAI,QAAQ,OAAO,WAAW;GAAS,EAClF;GAAE,MAAM;GAAY,UAAU;IAAE;IAAK;IAAU;IAAM;GAAE,CACxD,EACF;OAED,QAAO,EACL,SAAS,CAAC;EAAE,MAAM;EAAiB;EAAM,CAAC,EAC3C;;AAIL,SAAgB,eAAe,MAAc,UAAU,OAAuB;CAC5E,MAAM,SAAyB,EAAE,SAAS,CAAC;EAAE,MAAM;EAAiB,MAAM,KAAK,UAAU,KAAK;EAAE,CAAC,EAAE;AACnG,KAAI,QAAW,QAAO,UAAU;AAChC,QAAO;;AAGT,SAAgB,gBAAgB,EAAE,MAAM,MAAM,WAA2C;AACvF,QAAO;EACL,SAAS;EACT,SAAS,CAAC;GACR,MAAM;GACN,MAAM,KAAK,UAAU;IAAE,SAAS;IAAO;IAAM;IAAM;IAAS,CAAC;GAC9D,CAAC;EACH;;AAGH,SAAgB,gBAAgB,OAAgC;AAC9D,KAAI,iBAAiB,eACnB,QAAO,eAAe;EAAE,SAAS;EAAO,MAAM,MAAM;EAAM,SAAS,MAAM;EAAS,MAAM,MAAM;EAAM,EAAE,KAAK;UAClG,iBAAiB,OAAO;AAGjC,UAAQ,MAAM,MAAM;AACpB,SAAO,eAAe;GAAE,SAAS;GAAO,MAAM,MAAM;GAAM,SAAS,MAAM;GAAS,EAAE,KAAK;QACpF;AAEL,UAAQ,MAAM,uBAAuB,MAAM;AAC3C,SAAO,eAAe;GAAE,SAAS;GAAO,MAAM;GAAiB,SAAS;GAA6B,EAAE,KAAK;;;AAIhH,SAAgB,qBAAqB,EAAE,YAA8B;AAEnE,QADc,UAAU,WAAY,OAAO,KAAK,SAAS,MAAM,SAAS,CAAC,SAAS,QAAQ,GAAG,SAAS"}
|