@daloyjs/core 1.0.0-beta.6 → 1.0.0-beta.7
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 +2 -2
- package/dist/adapters/bun.d.ts +13 -1
- package/dist/adapters/bun.js +8 -1
- package/dist/adapters/cloudflare.d.ts +7 -1
- package/dist/adapters/cloudflare.js +6 -1
- package/dist/adapters/deno.d.ts +11 -1
- package/dist/adapters/deno.js +8 -1
- package/dist/adapters/fastly.d.ts +12 -2
- package/dist/adapters/fastly.js +12 -2
- package/dist/adapters/lambda.d.ts +37 -1
- package/dist/adapters/lambda.js +6 -1
- package/dist/adapters/node.d.ts +12 -1
- package/dist/adapters/node.js +7 -1
- package/dist/adapters/vercel.d.ts +13 -1
- package/dist/adapters/vercel.js +12 -1
- package/dist/app.d.ts +67 -17
- package/dist/app.js +97 -31
- package/dist/banner.d.ts +6 -0
- package/dist/banner.js +6 -0
- package/dist/cli.d.ts +35 -0
- package/dist/cli.js +23 -1
- package/dist/combine.d.ts +8 -0
- package/dist/combine.js +8 -0
- package/dist/compression.d.ts +3 -0
- package/dist/compression.js +3 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +4 -0
- package/dist/conn-info.d.ts +35 -2
- package/dist/conn-info.js +35 -2
- package/dist/contract.d.ts +2 -0
- package/dist/contract.js +2 -0
- package/dist/cookie.d.ts +12 -0
- package/dist/cookie.js +12 -0
- package/dist/dependency.d.ts +4 -0
- package/dist/dependency.js +3 -0
- package/dist/discriminator.d.ts +13 -0
- package/dist/discriminator.js +23 -1
- package/dist/docs.d.ts +78 -0
- package/dist/docs.js +21 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +14 -0
- package/dist/etag.d.ts +2 -0
- package/dist/etag.js +2 -0
- package/dist/fetch-guard.d.ts +7 -0
- package/dist/fetch-guard.js +7 -0
- package/dist/fetch-resilience.d.ts +4 -0
- package/dist/fetch-resilience.js +4 -0
- package/dist/http-signatures.d.ts +32 -0
- package/dist/http-signatures.js +30 -0
- package/dist/index.d.ts +1 -1
- package/dist/ip-restriction.d.ts +40 -3
- package/dist/ip-restriction.js +35 -3
- package/dist/jwk.d.ts +12 -1
- package/dist/jwk.js +6 -0
- package/dist/jwt.d.ts +14 -0
- package/dist/jwt.js +10 -0
- package/dist/load-shedding.d.ts +3 -0
- package/dist/load-shedding.js +3 -0
- package/dist/logger.d.ts +13 -0
- package/dist/logger.js +3 -0
- package/dist/mcp.d.ts +152 -10
- package/dist/mcp.js +223 -19
- package/dist/middleware.d.ts +68 -0
- package/dist/middleware.js +17 -0
- package/dist/mtls.d.ts +19 -2
- package/dist/mtls.js +12 -2
- package/dist/multipart.d.ts +42 -5
- package/dist/multipart.js +41 -5
- package/dist/openapi.d.ts +15 -9
- package/dist/openapi.js +6 -9
- package/dist/rate-limit-redis.d.ts +21 -2
- package/dist/rate-limit-redis.js +17 -2
- package/dist/safe-redirect.d.ts +6 -0
- package/dist/safe-redirect.js +6 -0
- package/dist/sbom.cdx.json +9 -9
- package/dist/sbom.spdx.json +5 -5
- package/dist/scheduler.d.ts +4 -0
- package/dist/schema.d.ts +25 -0
- package/dist/security-schemes.d.ts +50 -0
- package/dist/security-schemes.js +6 -0
- package/dist/security.d.ts +33 -0
- package/dist/security.js +28 -10
- package/dist/session.d.ts +34 -5
- package/dist/session.js +31 -5
- package/dist/streaming.d.ts +19 -0
- package/dist/streaming.js +16 -0
- package/dist/subdomains.d.ts +4 -0
- package/dist/subdomains.js +4 -0
- package/dist/time-claims.d.ts +22 -2
- package/dist/time-claims.js +6 -0
- package/dist/tracing.d.ts +12 -0
- package/dist/tracing.js +6 -0
- package/dist/types.d.ts +70 -1
- package/dist/waf.js +21 -1
- package/dist/webhook-delivery.d.ts +6 -0
- package/dist/webhook-delivery.js +5 -0
- package/dist/websocket.d.ts +137 -4
- package/dist/websocket.js +105 -4
- package/package.json +1 -1
package/dist/mcp.d.ts
CHANGED
|
@@ -52,6 +52,22 @@ export type McpJsonSchema = McpJsonObject;
|
|
|
52
52
|
* @since 1.0.0
|
|
53
53
|
*/
|
|
54
54
|
export type McpJsonRpcId = string | number | null;
|
|
55
|
+
/**
|
|
56
|
+
* Icon metadata clients may render next to a server, tool, resource, or
|
|
57
|
+
* prompt (MCP 2025-11-25, SEP-973).
|
|
58
|
+
*
|
|
59
|
+
* @since 1.0.0
|
|
60
|
+
*/
|
|
61
|
+
export interface McpIcon {
|
|
62
|
+
/** Icon URL. Prefer `https:` or `data:` URIs that clients can fetch safely. */
|
|
63
|
+
src: string;
|
|
64
|
+
/** Optional icon media type, e.g. `"image/png"`. */
|
|
65
|
+
mimeType?: string;
|
|
66
|
+
/** Optional pixel sizes the icon is available in, e.g. `["48x48"]`. */
|
|
67
|
+
sizes?: string[];
|
|
68
|
+
/** Optional theme the icon is designed for. */
|
|
69
|
+
theme?: "light" | "dark";
|
|
70
|
+
}
|
|
55
71
|
/**
|
|
56
72
|
* Identity block returned from the MCP `initialize` handshake.
|
|
57
73
|
*
|
|
@@ -64,6 +80,12 @@ export interface McpServerInfo {
|
|
|
64
80
|
title?: string;
|
|
65
81
|
/** Server version surfaced to clients for debugging and compatibility. */
|
|
66
82
|
version: string;
|
|
83
|
+
/** Optional human-readable server description (MCP 2025-11-25). */
|
|
84
|
+
description?: string;
|
|
85
|
+
/** Optional homepage URL for this server (MCP 2025-11-25). */
|
|
86
|
+
websiteUrl?: string;
|
|
87
|
+
/** Optional icons clients may display for this server (MCP 2025-11-25). */
|
|
88
|
+
icons?: McpIcon[];
|
|
67
89
|
}
|
|
68
90
|
/**
|
|
69
91
|
* Per-request context passed to tool, resource, and prompt handlers.
|
|
@@ -74,9 +96,10 @@ export interface McpRequestContext {
|
|
|
74
96
|
/** The original HTTP request received by the DaloyJS route. */
|
|
75
97
|
request: Request;
|
|
76
98
|
/**
|
|
77
|
-
* Protocol version selected for this call.
|
|
78
|
-
*
|
|
79
|
-
*
|
|
99
|
+
* Protocol version selected for this call. `initialize` negotiates it from
|
|
100
|
+
* `params.protocolVersion`; other calls take the `MCP-Protocol-Version`
|
|
101
|
+
* header, falling back to `2025-03-26` (the spec's assumption for
|
|
102
|
+
* headerless requests) when supported, otherwise the preferred version.
|
|
80
103
|
*/
|
|
81
104
|
protocolVersion: string;
|
|
82
105
|
/** JSON-RPC id for request/response correlation. */
|
|
@@ -90,7 +113,9 @@ export interface McpRequestContext {
|
|
|
90
113
|
* @since 1.0.0
|
|
91
114
|
*/
|
|
92
115
|
export interface McpTextContent {
|
|
116
|
+
/** Discriminator literal identifying this block as text. */
|
|
93
117
|
type: "text";
|
|
118
|
+
/** The plain-text payload of the block. */
|
|
94
119
|
text: string;
|
|
95
120
|
}
|
|
96
121
|
/**
|
|
@@ -102,8 +127,11 @@ export interface McpTextContent {
|
|
|
102
127
|
* @since 1.0.0
|
|
103
128
|
*/
|
|
104
129
|
export interface McpImageContent {
|
|
130
|
+
/** Discriminator literal identifying this block as an image. */
|
|
105
131
|
type: "image";
|
|
132
|
+
/** Base64-encoded image bytes. */
|
|
106
133
|
data: string;
|
|
134
|
+
/** Image media type, e.g. `"image/png"`. */
|
|
107
135
|
mimeType: string;
|
|
108
136
|
}
|
|
109
137
|
/**
|
|
@@ -112,7 +140,9 @@ export interface McpImageContent {
|
|
|
112
140
|
* @since 1.0.0
|
|
113
141
|
*/
|
|
114
142
|
export interface McpEmbeddedResourceContent {
|
|
143
|
+
/** Discriminator literal identifying this block as an embedded resource. */
|
|
115
144
|
type: "resource";
|
|
145
|
+
/** The embedded resource contents (uri plus text or base64 blob). */
|
|
116
146
|
resource: McpResourceContents;
|
|
117
147
|
}
|
|
118
148
|
/**
|
|
@@ -131,13 +161,36 @@ export type McpContent = McpTextContent | McpImageContent | McpEmbeddedResourceC
|
|
|
131
161
|
* @since 1.0.0
|
|
132
162
|
*/
|
|
133
163
|
export interface McpToolResult {
|
|
134
|
-
/**
|
|
135
|
-
|
|
164
|
+
/**
|
|
165
|
+
* Human or model-readable content blocks returned to the MCP client. When
|
|
166
|
+
* omitted, {@link createMcpHandler} backfills a text block serializing
|
|
167
|
+
* `structuredContent` so pre-2025-06-18 clients still receive output.
|
|
168
|
+
*/
|
|
169
|
+
content?: McpContent[];
|
|
136
170
|
/** Optional structured payload for clients that can consume typed output. */
|
|
137
171
|
structuredContent?: McpJsonObject;
|
|
138
172
|
/** Set to `true` for domain/tool errors the model may recover from. */
|
|
139
173
|
isError?: boolean;
|
|
140
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Behavioral hints a tool can advertise to MCP clients. Hints are untrusted
|
|
177
|
+
* metadata for UX decisions (confirmation prompts, badges); clients must not
|
|
178
|
+
* rely on them for security decisions.
|
|
179
|
+
*
|
|
180
|
+
* @since 1.0.0
|
|
181
|
+
*/
|
|
182
|
+
export interface McpToolAnnotations {
|
|
183
|
+
/** Human-readable title for the tool. */
|
|
184
|
+
title?: string;
|
|
185
|
+
/** Hint that the tool does not modify its environment. */
|
|
186
|
+
readOnlyHint?: boolean;
|
|
187
|
+
/** Hint that the tool may perform destructive updates. */
|
|
188
|
+
destructiveHint?: boolean;
|
|
189
|
+
/** Hint that repeated calls with the same arguments have no extra effect. */
|
|
190
|
+
idempotentHint?: boolean;
|
|
191
|
+
/** Hint that the tool interacts with external entities. */
|
|
192
|
+
openWorldHint?: boolean;
|
|
193
|
+
}
|
|
141
194
|
/**
|
|
142
195
|
* Handler for a single MCP tool.
|
|
143
196
|
*
|
|
@@ -172,6 +225,16 @@ export interface McpTool<TArgs extends Record<string, unknown> = Record<string,
|
|
|
172
225
|
description: string;
|
|
173
226
|
/** JSON Schema for `params.arguments`. */
|
|
174
227
|
inputSchema: McpJsonSchema;
|
|
228
|
+
/**
|
|
229
|
+
* Optional JSON Schema describing `structuredContent` in tool results
|
|
230
|
+
* (MCP 2025-06-18). When set, handlers should return `structuredContent`
|
|
231
|
+
* matching it.
|
|
232
|
+
*/
|
|
233
|
+
outputSchema?: McpJsonSchema;
|
|
234
|
+
/** Optional behavioral hints for clients. */
|
|
235
|
+
annotations?: McpToolAnnotations;
|
|
236
|
+
/** Optional icons clients may display for this tool (MCP 2025-11-25). */
|
|
237
|
+
icons?: McpIcon[];
|
|
175
238
|
/** Execute the tool with untrusted JSON arguments. */
|
|
176
239
|
handler: McpToolHandler<TArgs>;
|
|
177
240
|
}
|
|
@@ -191,6 +254,8 @@ export interface McpResource {
|
|
|
191
254
|
description?: string;
|
|
192
255
|
/** MIME type returned by `resources/read`, such as `"application/json"`. */
|
|
193
256
|
mimeType?: string;
|
|
257
|
+
/** Optional icons clients may display for this resource (MCP 2025-11-25). */
|
|
258
|
+
icons?: McpIcon[];
|
|
194
259
|
}
|
|
195
260
|
/**
|
|
196
261
|
* Resource payload returned from `resources/read`.
|
|
@@ -228,6 +293,51 @@ export interface McpResourceDefinition extends McpResource {
|
|
|
228
293
|
*/
|
|
229
294
|
read: (ctx: McpRequestContext) => McpResourceContents | McpResourceContents[] | Promise<McpResourceContents | McpResourceContents[]>;
|
|
230
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* Resource template metadata returned from `resources/templates/list`.
|
|
298
|
+
*
|
|
299
|
+
* @since 1.0.0
|
|
300
|
+
*/
|
|
301
|
+
export interface McpResourceTemplate {
|
|
302
|
+
/**
|
|
303
|
+
* URI template for this resource family, e.g. `"daloy://records/{id}"`.
|
|
304
|
+
* DaloyJS supports simple `{name}` variables (RFC 6570 level 1); each
|
|
305
|
+
* variable matches one URI segment (no `/`).
|
|
306
|
+
*/
|
|
307
|
+
uriTemplate: string;
|
|
308
|
+
/** Stable template name. */
|
|
309
|
+
name: string;
|
|
310
|
+
/** Optional human-readable title. */
|
|
311
|
+
title?: string;
|
|
312
|
+
/** Optional description shown by clients. */
|
|
313
|
+
description?: string;
|
|
314
|
+
/** MIME type of resources produced by this template. */
|
|
315
|
+
mimeType?: string;
|
|
316
|
+
/** Optional icons clients may display for this template (MCP 2025-11-25). */
|
|
317
|
+
icons?: McpIcon[];
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Definition of a parameterized MCP resource template.
|
|
321
|
+
*
|
|
322
|
+
* Templates answer `resources/read` for URIs that match `uriTemplate` but are
|
|
323
|
+
* not listed as concrete resources. Template variables arrive as raw URI
|
|
324
|
+
* segment strings; validate them before touching databases or files.
|
|
325
|
+
*
|
|
326
|
+
* @since 1.0.0
|
|
327
|
+
*/
|
|
328
|
+
export interface McpResourceTemplateDefinition extends McpResourceTemplate {
|
|
329
|
+
/**
|
|
330
|
+
* Read a resource instantiated from this template for `resources/read`.
|
|
331
|
+
*
|
|
332
|
+
* @param uri - The full resource URI requested by the client.
|
|
333
|
+
* @param variables - Template variable values extracted from `uri`.
|
|
334
|
+
* @param ctx - Request metadata and the original HTTP request.
|
|
335
|
+
* @returns One or more content entries for this resource.
|
|
336
|
+
* @throws {McpToolError} for caller-correctable failures such as an unknown
|
|
337
|
+
* record id; these become JSON-RPC invalid-params errors.
|
|
338
|
+
*/
|
|
339
|
+
read: (uri: string, variables: Record<string, string>, ctx: McpRequestContext) => McpResourceContents | McpResourceContents[] | Promise<McpResourceContents | McpResourceContents[]>;
|
|
340
|
+
}
|
|
231
341
|
/**
|
|
232
342
|
* Argument metadata for an MCP prompt.
|
|
233
343
|
*
|
|
@@ -253,8 +363,14 @@ export interface McpPrompt {
|
|
|
253
363
|
title?: string;
|
|
254
364
|
/** Optional prompt description. */
|
|
255
365
|
description?: string;
|
|
256
|
-
/**
|
|
366
|
+
/**
|
|
367
|
+
* Prompt arguments clients may supply to `prompts/get`. Arguments marked
|
|
368
|
+
* `required: true` are enforced by {@link createMcpHandler}: a `prompts/get`
|
|
369
|
+
* call missing one fails with a JSON-RPC invalid-params error.
|
|
370
|
+
*/
|
|
257
371
|
arguments?: McpPromptArgument[];
|
|
372
|
+
/** Optional icons clients may display for this prompt (MCP 2025-11-25). */
|
|
373
|
+
icons?: McpIcon[];
|
|
258
374
|
}
|
|
259
375
|
/**
|
|
260
376
|
* Message returned from `prompts/get`.
|
|
@@ -326,8 +442,25 @@ export interface McpHandlerOptions {
|
|
|
326
442
|
tools?: readonly McpTool[];
|
|
327
443
|
/** Readable resources exposed through `resources/list` and `resources/read`. */
|
|
328
444
|
resources?: readonly McpResourceDefinition[];
|
|
445
|
+
/**
|
|
446
|
+
* Parameterized resource templates exposed through
|
|
447
|
+
* `resources/templates/list` and matched by `resources/read` when a URI is
|
|
448
|
+
* not a listed concrete resource.
|
|
449
|
+
*/
|
|
450
|
+
resourceTemplates?: readonly McpResourceTemplateDefinition[];
|
|
329
451
|
/** Reusable prompts exposed through `prompts/list` and `prompts/get`. */
|
|
330
452
|
prompts?: readonly McpPromptDefinition[];
|
|
453
|
+
/**
|
|
454
|
+
* Extra `Origin` header values allowed on MCP requests, e.g.
|
|
455
|
+
* `"https://app.example.com"` (or the literal `"null"` for opaque origins).
|
|
456
|
+
*
|
|
457
|
+
* The MCP Streamable HTTP spec requires servers to validate `Origin` to
|
|
458
|
+
* prevent DNS rebinding attacks. DaloyJS always allows requests without an
|
|
459
|
+
* `Origin` header (non-browser MCP clients), same-origin requests, and
|
|
460
|
+
* loopback origins (`localhost`, `*.localhost`, `127.0.0.1`, `[::1]`); every
|
|
461
|
+
* other origin is rejected with `403` unless listed here.
|
|
462
|
+
*/
|
|
463
|
+
allowedOrigins?: readonly string[];
|
|
331
464
|
/** Accepted MCP protocol versions. Defaults to {@link MCP_PROTOCOL_VERSIONS}. */
|
|
332
465
|
protocolVersions?: readonly string[];
|
|
333
466
|
/**
|
|
@@ -364,10 +497,16 @@ export type McpHandler = (request: Request) => Promise<Response>;
|
|
|
364
497
|
*
|
|
365
498
|
* The handler implements the server side of MCP over one HTTP endpoint:
|
|
366
499
|
* `initialize`, `ping`, `tools/list`, `tools/call`, `resources/list`,
|
|
367
|
-
* `resources/read`, `prompts/list`, and
|
|
368
|
-
* requests over `POST`, acknowledges
|
|
369
|
-
* `MCP-Protocol-Version` header,
|
|
370
|
-
*
|
|
500
|
+
* `resources/templates/list`, `resources/read`, `prompts/list`, and
|
|
501
|
+
* `prompts/get`. It accepts JSON-RPC requests over `POST`, acknowledges
|
|
502
|
+
* notifications with `202`, validates the `MCP-Protocol-Version` header,
|
|
503
|
+
* bounds request bodies, enforces required prompt arguments, and returns
|
|
504
|
+
* JSON-RPC errors for malformed input.
|
|
505
|
+
*
|
|
506
|
+
* Security: per the Streamable HTTP spec's DNS-rebinding guidance, every
|
|
507
|
+
* request bearing an `Origin` header is validated. Same-origin and loopback
|
|
508
|
+
* origins pass; anything else is rejected with `403` unless listed in
|
|
509
|
+
* {@link McpHandlerOptions.allowedOrigins}.
|
|
371
510
|
*
|
|
372
511
|
* It intentionally does not spawn stdio servers, manage OAuth metadata, keep
|
|
373
512
|
* durable sessions, or open server-initiated SSE streams. Use DaloyJS
|
|
@@ -378,6 +517,9 @@ export type McpHandler = (request: Request) => Promise<Response>;
|
|
|
378
517
|
* @param options - Server identity, capabilities, limits, and response headers.
|
|
379
518
|
* @returns A Fetch-compatible request handler suitable for {@link mcpRoutes}
|
|
380
519
|
* or for direct use in any web-standard runtime.
|
|
520
|
+
* @throws {TypeError} at construction for invalid serverInfo, protocol
|
|
521
|
+
* versions, body limits, duplicate names/URIs, malformed `allowedOrigins`
|
|
522
|
+
* entries, or unsupported URI template expressions.
|
|
381
523
|
*
|
|
382
524
|
* @example
|
|
383
525
|
* ```ts
|
package/dist/mcp.js
CHANGED
|
@@ -25,17 +25,51 @@ export const MCP_PROTOCOL_VERSIONS = Object.freeze([
|
|
|
25
25
|
* @since 1.0.0
|
|
26
26
|
*/
|
|
27
27
|
export const MCP_DEFAULT_MAX_BODY_BYTES = 1 << 18;
|
|
28
|
+
/**
|
|
29
|
+
* Protocol revision assumed when an HTTP request carries no
|
|
30
|
+
* `MCP-Protocol-Version` header, as required by the Streamable HTTP spec for
|
|
31
|
+
* backwards compatibility with pre-2025-06-18 clients.
|
|
32
|
+
*/
|
|
33
|
+
const LEGACY_ASSUMED_PROTOCOL_VERSION = "2025-03-26";
|
|
28
34
|
const PARSE_ERROR = -32700;
|
|
29
35
|
const INVALID_REQUEST = -32600;
|
|
30
36
|
const METHOD_NOT_FOUND = -32601;
|
|
31
37
|
const INVALID_PARAMS = -32602;
|
|
32
38
|
const INTERNAL_ERROR = -32603;
|
|
39
|
+
/**
|
|
40
|
+
* JSON Schema for the JSON-RPC 2.0 envelope every MCP response uses. Exposed
|
|
41
|
+
* through `toJSONSchema()` so the generated OpenAPI document describes the
|
|
42
|
+
* `/mcp` route honestly instead of leaving it an undocumented blind spot.
|
|
43
|
+
*/
|
|
44
|
+
const MCP_JSONRPC_ENVELOPE_JSON_SCHEMA = {
|
|
45
|
+
type: "object",
|
|
46
|
+
description: "JSON-RPC 2.0 envelope produced by the MCP Streamable HTTP endpoint.",
|
|
47
|
+
properties: {
|
|
48
|
+
jsonrpc: { type: "string", const: "2.0" },
|
|
49
|
+
id: { oneOf: [{ type: "string" }, { type: "number" }, { type: "null" }] },
|
|
50
|
+
result: { description: "Method result. Present on success; shape varies by MCP method." },
|
|
51
|
+
error: {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: {
|
|
54
|
+
code: { type: "integer" },
|
|
55
|
+
message: { type: "string" },
|
|
56
|
+
data: {},
|
|
57
|
+
},
|
|
58
|
+
required: ["code", "message"],
|
|
59
|
+
additionalProperties: false,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
required: ["jsonrpc"],
|
|
63
|
+
};
|
|
33
64
|
const MCP_JSON_RESPONSE_SCHEMA = {
|
|
34
65
|
"~standard": {
|
|
35
66
|
version: 1,
|
|
36
67
|
vendor: "daloyjs",
|
|
68
|
+
// Pass-through: the MCP handler fully controls the envelope it builds, so
|
|
69
|
+
// re-validating (or field-stripping) it here would only burn cycles.
|
|
37
70
|
validate: (value) => ({ value }),
|
|
38
71
|
},
|
|
72
|
+
toJSONSchema: () => MCP_JSONRPC_ENVELOPE_JSON_SCHEMA,
|
|
39
73
|
};
|
|
40
74
|
/**
|
|
41
75
|
* Caller-correctable MCP tool/resource/prompt error.
|
|
@@ -103,21 +137,105 @@ function publicPrompt(prompt) {
|
|
|
103
137
|
const { get: _get, ...rest } = prompt;
|
|
104
138
|
return rest;
|
|
105
139
|
}
|
|
140
|
+
function publicResourceTemplate(template) {
|
|
141
|
+
const { read: _read, ...rest } = template;
|
|
142
|
+
return rest;
|
|
143
|
+
}
|
|
106
144
|
function normalizeToolResult(value) {
|
|
107
|
-
|
|
145
|
+
if (typeof value === "string")
|
|
146
|
+
return { content: [{ type: "text", text: value }] };
|
|
147
|
+
if (value.content && value.content.length > 0)
|
|
148
|
+
return value;
|
|
149
|
+
// Backwards compatibility: clients that predate `structuredContent` only
|
|
150
|
+
// read `content`, so mirror the structured payload into a text block.
|
|
151
|
+
const content = value.structuredContent !== undefined
|
|
152
|
+
? [{ type: "text", text: JSON.stringify(value.structuredContent) }]
|
|
153
|
+
: [];
|
|
154
|
+
return { ...value, content };
|
|
108
155
|
}
|
|
109
156
|
function selectedProtocolVersion(requested, supported, preferred) {
|
|
110
157
|
return supported.has(requested) ? requested : preferred;
|
|
111
158
|
}
|
|
159
|
+
function escapeRegExp(literal) {
|
|
160
|
+
return literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Compile a simple RFC 6570 level-1 URI template into a matcher. Each
|
|
164
|
+
* `{name}` variable matches exactly one URI segment (`[^/]+`). Operators such
|
|
165
|
+
* as `{+path}` or `{?query}` are rejected so the handler never advertises a
|
|
166
|
+
* template it cannot match.
|
|
167
|
+
*/
|
|
168
|
+
function compileUriTemplate(template) {
|
|
169
|
+
const { uriTemplate } = template;
|
|
170
|
+
const variables = [];
|
|
171
|
+
let pattern = "";
|
|
172
|
+
let index = 0;
|
|
173
|
+
while (index < uriTemplate.length) {
|
|
174
|
+
const open = uriTemplate.indexOf("{", index);
|
|
175
|
+
if (open === -1) {
|
|
176
|
+
pattern += escapeRegExp(uriTemplate.slice(index));
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
pattern += escapeRegExp(uriTemplate.slice(index, open));
|
|
180
|
+
const close = uriTemplate.indexOf("}", open);
|
|
181
|
+
if (close === -1) {
|
|
182
|
+
throw new TypeError(`MCP resource template "${uriTemplate}" has an unterminated "{".`);
|
|
183
|
+
}
|
|
184
|
+
const name = uriTemplate.slice(open + 1, close);
|
|
185
|
+
if (!/^[A-Za-z0-9_]+$/.test(name)) {
|
|
186
|
+
throw new TypeError(`MCP resource template "${uriTemplate}" uses an unsupported expression "{${name}}"; ` +
|
|
187
|
+
"only simple {name} variables are supported.");
|
|
188
|
+
}
|
|
189
|
+
variables.push(name);
|
|
190
|
+
pattern += "([^/]+)";
|
|
191
|
+
index = close + 1;
|
|
192
|
+
}
|
|
193
|
+
return { template, regex: new RegExp(`^${pattern}$`), variables };
|
|
194
|
+
}
|
|
195
|
+
const LOOPBACK_HOSTNAMES = new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
196
|
+
/**
|
|
197
|
+
* Streamable HTTP DNS-rebinding defense: decide whether a browser `Origin`
|
|
198
|
+
* may talk to this MCP endpoint. Same-origin and loopback origins are always
|
|
199
|
+
* allowed; anything else must be explicitly allowlisted.
|
|
200
|
+
*/
|
|
201
|
+
function isAllowedOrigin(origin, request, allowlist) {
|
|
202
|
+
const normalized = origin.toLowerCase();
|
|
203
|
+
if (allowlist.has(normalized))
|
|
204
|
+
return true;
|
|
205
|
+
if (normalized === "null")
|
|
206
|
+
return false;
|
|
207
|
+
let parsed;
|
|
208
|
+
try {
|
|
209
|
+
parsed = new URL(normalized);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
const hostname = parsed.hostname;
|
|
215
|
+
if (LOOPBACK_HOSTNAMES.has(hostname) || hostname.endsWith(".localhost"))
|
|
216
|
+
return true;
|
|
217
|
+
try {
|
|
218
|
+
return parsed.host === new URL(request.url).host;
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
112
224
|
/**
|
|
113
225
|
* Create a dependency-free MCP Streamable HTTP endpoint handler.
|
|
114
226
|
*
|
|
115
227
|
* The handler implements the server side of MCP over one HTTP endpoint:
|
|
116
228
|
* `initialize`, `ping`, `tools/list`, `tools/call`, `resources/list`,
|
|
117
|
-
* `resources/read`, `prompts/list`, and
|
|
118
|
-
* requests over `POST`, acknowledges
|
|
119
|
-
* `MCP-Protocol-Version` header,
|
|
120
|
-
*
|
|
229
|
+
* `resources/templates/list`, `resources/read`, `prompts/list`, and
|
|
230
|
+
* `prompts/get`. It accepts JSON-RPC requests over `POST`, acknowledges
|
|
231
|
+
* notifications with `202`, validates the `MCP-Protocol-Version` header,
|
|
232
|
+
* bounds request bodies, enforces required prompt arguments, and returns
|
|
233
|
+
* JSON-RPC errors for malformed input.
|
|
234
|
+
*
|
|
235
|
+
* Security: per the Streamable HTTP spec's DNS-rebinding guidance, every
|
|
236
|
+
* request bearing an `Origin` header is validated. Same-origin and loopback
|
|
237
|
+
* origins pass; anything else is rejected with `403` unless listed in
|
|
238
|
+
* {@link McpHandlerOptions.allowedOrigins}.
|
|
121
239
|
*
|
|
122
240
|
* It intentionally does not spawn stdio servers, manage OAuth metadata, keep
|
|
123
241
|
* durable sessions, or open server-initiated SSE streams. Use DaloyJS
|
|
@@ -128,6 +246,9 @@ function selectedProtocolVersion(requested, supported, preferred) {
|
|
|
128
246
|
* @param options - Server identity, capabilities, limits, and response headers.
|
|
129
247
|
* @returns A Fetch-compatible request handler suitable for {@link mcpRoutes}
|
|
130
248
|
* or for direct use in any web-standard runtime.
|
|
249
|
+
* @throws {TypeError} at construction for invalid serverInfo, protocol
|
|
250
|
+
* versions, body limits, duplicate names/URIs, malformed `allowedOrigins`
|
|
251
|
+
* entries, or unsupported URI template expressions.
|
|
131
252
|
*
|
|
132
253
|
* @example
|
|
133
254
|
* ```ts
|
|
@@ -173,6 +294,7 @@ export function createMcpHandler(options) {
|
|
|
173
294
|
}
|
|
174
295
|
const tools = options.tools ?? [];
|
|
175
296
|
const resources = options.resources ?? [];
|
|
297
|
+
const resourceTemplates = options.resourceTemplates ?? [];
|
|
176
298
|
const prompts = options.prompts ?? [];
|
|
177
299
|
const toolMap = new Map(tools.map((tool) => [tool.name, tool]));
|
|
178
300
|
const resourceMap = new Map(resources.map((resource) => [resource.uri, resource]));
|
|
@@ -183,24 +305,68 @@ export function createMcpHandler(options) {
|
|
|
183
305
|
throw new TypeError("MCP resource URIs must be unique.");
|
|
184
306
|
if (promptMap.size !== prompts.length)
|
|
185
307
|
throw new TypeError("MCP prompt names must be unique.");
|
|
308
|
+
if (new Set(resourceTemplates.map((template) => template.uriTemplate)).size !==
|
|
309
|
+
resourceTemplates.length) {
|
|
310
|
+
throw new TypeError("MCP resource template URIs must be unique.");
|
|
311
|
+
}
|
|
312
|
+
const compiledTemplates = resourceTemplates.map(compileUriTemplate);
|
|
313
|
+
const allowedOrigins = new Set();
|
|
314
|
+
for (const entry of options.allowedOrigins ?? []) {
|
|
315
|
+
const normalized = entry.toLowerCase();
|
|
316
|
+
if (normalized === "null") {
|
|
317
|
+
allowedOrigins.add(normalized);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
let parsed;
|
|
321
|
+
try {
|
|
322
|
+
parsed = new URL(normalized);
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
parsed = undefined;
|
|
326
|
+
}
|
|
327
|
+
if (!parsed || parsed.origin !== normalized) {
|
|
328
|
+
throw new TypeError(`MCP allowedOrigins entry "${entry}" must be a bare origin such as "https://app.example.com".`);
|
|
329
|
+
}
|
|
330
|
+
allowedOrigins.add(normalized);
|
|
331
|
+
}
|
|
186
332
|
const exposeInternalErrors = options.exposeInternalErrors ??
|
|
187
333
|
(typeof process === "object" && process.env?.NODE_ENV !== "production");
|
|
188
334
|
const headers = options.headers;
|
|
335
|
+
const legacyAssumed = supported.has(LEGACY_ASSUMED_PROTOCOL_VERSION)
|
|
336
|
+
? LEGACY_ASSUMED_PROTOCOL_VERSION
|
|
337
|
+
: preferred;
|
|
189
338
|
async function handleRpcRequest(message, request) {
|
|
190
339
|
const id = (message.id ?? null);
|
|
191
340
|
const method = message.method;
|
|
192
341
|
const params = asRecord(message.params);
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
342
|
+
const headerVersion = request.headers.get("mcp-protocol-version");
|
|
343
|
+
// Per the Streamable HTTP spec, a request without the header is assumed
|
|
344
|
+
// to speak 2025-03-26; `initialize` negotiates via params instead.
|
|
345
|
+
const protocolVersion = method === "initialize"
|
|
346
|
+
? selectedProtocolVersion(typeof params.protocolVersion === "string"
|
|
347
|
+
? params.protocolVersion
|
|
348
|
+
: (headerVersion ?? ""), supported, preferred)
|
|
349
|
+
: headerVersion !== null
|
|
350
|
+
? selectedProtocolVersion(headerVersion, supported, preferred)
|
|
351
|
+
: legacyAssumed;
|
|
196
352
|
const ctx = { request, protocolVersion, id, method };
|
|
353
|
+
const cursor = params.cursor;
|
|
354
|
+
if (cursor !== undefined &&
|
|
355
|
+
(method === "tools/list" ||
|
|
356
|
+
method === "resources/list" ||
|
|
357
|
+
method === "resources/templates/list" ||
|
|
358
|
+
method === "prompts/list")) {
|
|
359
|
+
// This handler returns complete lists and never issues cursors, so any
|
|
360
|
+
// client-supplied cursor is unknown by definition.
|
|
361
|
+
return rpcError(id, INVALID_PARAMS, "Unknown pagination cursor.", undefined, 200, headers);
|
|
362
|
+
}
|
|
197
363
|
switch (method) {
|
|
198
364
|
case "initialize":
|
|
199
365
|
return rpcResult(id, {
|
|
200
366
|
protocolVersion,
|
|
201
367
|
capabilities: {
|
|
202
368
|
...(tools.length > 0 ? { tools: {} } : {}),
|
|
203
|
-
...(resources.length > 0 ? { resources: {} } : {}),
|
|
369
|
+
...(resources.length > 0 || resourceTemplates.length > 0 ? { resources: {} } : {}),
|
|
204
370
|
...(prompts.length > 0 ? { prompts: {} } : {}),
|
|
205
371
|
},
|
|
206
372
|
serverInfo: options.serverInfo,
|
|
@@ -229,23 +395,46 @@ export function createMcpHandler(options) {
|
|
|
229
395
|
}
|
|
230
396
|
case "resources/list":
|
|
231
397
|
return rpcResult(id, { resources: resources.map(publicResource) }, headers);
|
|
398
|
+
case "resources/templates/list":
|
|
399
|
+
return rpcResult(id, { resourceTemplates: resourceTemplates.map(publicResourceTemplate) }, headers);
|
|
232
400
|
case "resources/read": {
|
|
233
401
|
const uri = typeof params.uri === "string" ? params.uri : "";
|
|
234
|
-
const
|
|
235
|
-
if (!resource) {
|
|
236
|
-
return rpcError(id, INVALID_PARAMS, `Unknown resource: ${uri || "<missing>"}`, undefined, 200, headers);
|
|
237
|
-
}
|
|
238
|
-
try {
|
|
239
|
-
const read = await resource.read(ctx);
|
|
240
|
-
return rpcResult(id, { contents: Array.isArray(read) ? read : [read] }, headers);
|
|
241
|
-
}
|
|
242
|
-
catch (error) {
|
|
402
|
+
const readError = (error) => {
|
|
243
403
|
const message = error instanceof McpToolError ? error.message : "Resource read failed.";
|
|
244
404
|
const data = error instanceof McpToolError
|
|
245
405
|
? undefined
|
|
246
406
|
: safeInternalErrorData(error, exposeInternalErrors);
|
|
247
407
|
return rpcError(id, error instanceof McpToolError ? INVALID_PARAMS : INTERNAL_ERROR, message, data, 200, headers);
|
|
408
|
+
};
|
|
409
|
+
const resource = resourceMap.get(uri);
|
|
410
|
+
if (resource) {
|
|
411
|
+
try {
|
|
412
|
+
const read = await resource.read(ctx);
|
|
413
|
+
return rpcResult(id, { contents: Array.isArray(read) ? read : [read] }, headers);
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
return readError(error);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (uri) {
|
|
420
|
+
for (const compiled of compiledTemplates) {
|
|
421
|
+
const match = compiled.regex.exec(uri);
|
|
422
|
+
if (!match)
|
|
423
|
+
continue;
|
|
424
|
+
const variables = {};
|
|
425
|
+
compiled.variables.forEach((name, position) => {
|
|
426
|
+
variables[name] = match[position + 1] ?? "";
|
|
427
|
+
});
|
|
428
|
+
try {
|
|
429
|
+
const read = await compiled.template.read(uri, variables, ctx);
|
|
430
|
+
return rpcResult(id, { contents: Array.isArray(read) ? read : [read] }, headers);
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
return readError(error);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
248
436
|
}
|
|
437
|
+
return rpcError(id, INVALID_PARAMS, `Unknown resource: ${uri || "<missing>"}`, undefined, 200, headers);
|
|
249
438
|
}
|
|
250
439
|
case "prompts/list":
|
|
251
440
|
return rpcResult(id, { prompts: prompts.map(publicPrompt) }, headers);
|
|
@@ -255,8 +444,15 @@ export function createMcpHandler(options) {
|
|
|
255
444
|
if (!prompt) {
|
|
256
445
|
return rpcError(id, INVALID_PARAMS, `Unknown prompt: ${name || "<missing>"}`, undefined, 200, headers);
|
|
257
446
|
}
|
|
447
|
+
const promptArgs = asRecord(params.arguments);
|
|
448
|
+
const missing = (prompt.arguments ?? [])
|
|
449
|
+
.filter((argument) => argument.required && promptArgs[argument.name] === undefined)
|
|
450
|
+
.map((argument) => argument.name);
|
|
451
|
+
if (missing.length > 0) {
|
|
452
|
+
return rpcError(id, INVALID_PARAMS, `Missing required prompt arguments: ${missing.join(", ")}`, undefined, 200, headers);
|
|
453
|
+
}
|
|
258
454
|
try {
|
|
259
|
-
return rpcResult(id, await prompt.get(
|
|
455
|
+
return rpcResult(id, await prompt.get(promptArgs, ctx), headers);
|
|
260
456
|
}
|
|
261
457
|
catch (error) {
|
|
262
458
|
const message = error instanceof McpToolError ? error.message : "Prompt rendering failed.";
|
|
@@ -271,6 +467,12 @@ export function createMcpHandler(options) {
|
|
|
271
467
|
}
|
|
272
468
|
}
|
|
273
469
|
return async function handleMcpRequest(request) {
|
|
470
|
+
// Streamable HTTP requires Origin validation on every request to defeat
|
|
471
|
+
// DNS rebinding; invalid browser origins are refused with 403.
|
|
472
|
+
const origin = request.headers.get("origin");
|
|
473
|
+
if (origin !== null && !isAllowedOrigin(origin, request, allowedOrigins)) {
|
|
474
|
+
return rpcError(null, INVALID_REQUEST, "Origin is not allowed for this MCP endpoint.", undefined, 403, headers);
|
|
475
|
+
}
|
|
274
476
|
if (request.method === "OPTIONS") {
|
|
275
477
|
return new Response(null, {
|
|
276
478
|
status: 204,
|
|
@@ -284,6 +486,7 @@ export function createMcpHandler(options) {
|
|
|
284
486
|
capabilities: {
|
|
285
487
|
tools: tools.map((tool) => tool.name),
|
|
286
488
|
resources: resources.map((resource) => resource.uri),
|
|
489
|
+
resourceTemplates: resourceTemplates.map((template) => template.uriTemplate),
|
|
287
490
|
prompts: prompts.map((prompt) => prompt.name),
|
|
288
491
|
},
|
|
289
492
|
hint: "Send JSON-RPC 2.0 over HTTP POST to this endpoint.",
|
|
@@ -387,6 +590,7 @@ export function mcpRoutes(path, handler) {
|
|
|
387
590
|
202: { description: "MCP notification accepted", body: MCP_JSON_RESPONSE_SCHEMA },
|
|
388
591
|
204: { description: "CORS preflight accepted" },
|
|
389
592
|
400: { description: "Invalid MCP request" },
|
|
593
|
+
403: { description: "Origin not allowed" },
|
|
390
594
|
405: { description: "Unsupported MCP transport method" },
|
|
391
595
|
413: { description: "MCP request body too large" },
|
|
392
596
|
};
|