@daloyjs/core 1.0.0-beta.6 → 1.0.0-rc.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 +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 +223 -12
- package/dist/adapters/vercel.d.ts +13 -1
- package/dist/adapters/vercel.js +12 -1
- package/dist/app.d.ts +78 -17
- package/dist/app.js +444 -86
- 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.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
|
};
|
package/dist/middleware.d.ts
CHANGED
|
@@ -78,16 +78,38 @@ export interface CspDirectivesOptions {
|
|
|
78
78
|
}
|
|
79
79
|
/** Options for {@link secureHeaders}. Every field can be disabled with `false`. */
|
|
80
80
|
export interface SecureHeadersOptions {
|
|
81
|
+
/**
|
|
82
|
+
* `Content-Security-Policy` value: a raw header string, a
|
|
83
|
+
* {@link CspDirectivesOptions} object (enables nonces / Trusted Types),
|
|
84
|
+
* or `false` to omit. Defaults to
|
|
85
|
+
* `"default-src 'self'; frame-ancestors 'none'"`.
|
|
86
|
+
*/
|
|
81
87
|
contentSecurityPolicy?: string | false | CspDirectivesOptions;
|
|
88
|
+
/**
|
|
89
|
+
* `Strict-Transport-Security` settings (`maxAgeSeconds` in seconds).
|
|
90
|
+
* Defaults to `{ maxAgeSeconds: 31536000, includeSubDomains: true }`.
|
|
91
|
+
*/
|
|
82
92
|
hsts?: {
|
|
83
93
|
maxAgeSeconds: number;
|
|
84
94
|
includeSubDomains?: boolean;
|
|
85
95
|
preload?: boolean;
|
|
86
96
|
} | false;
|
|
97
|
+
/**
|
|
98
|
+
* `X-Frame-Options` value. Defaults to `"DENY"`. Disabling it with
|
|
99
|
+
* `false` while the CSP also lacks `frame-ancestors` throws at
|
|
100
|
+
* construction time (clickjacking guard).
|
|
101
|
+
*/
|
|
87
102
|
frameOptions?: "DENY" | "SAMEORIGIN" | false;
|
|
103
|
+
/** `Referrer-Policy` value. Defaults to `"no-referrer"`. */
|
|
88
104
|
referrerPolicy?: string | false;
|
|
105
|
+
/**
|
|
106
|
+
* `Permissions-Policy` value. Defaults to
|
|
107
|
+
* `"camera=(), microphone=(), geolocation=(), clipboard-write=()"`.
|
|
108
|
+
*/
|
|
89
109
|
permissionsPolicy?: string | false;
|
|
110
|
+
/** `Cross-Origin-Opener-Policy` value. Defaults to `"same-origin"`. */
|
|
90
111
|
crossOriginOpenerPolicy?: string | false;
|
|
112
|
+
/** `Cross-Origin-Resource-Policy` value. Defaults to `"same-origin"`. */
|
|
91
113
|
crossOriginResourcePolicy?: string | false;
|
|
92
114
|
/**
|
|
93
115
|
* `Cross-Origin-Embedder-Policy` (COEP). Off by default because
|
|
@@ -101,7 +123,12 @@ export interface SecureHeadersOptions {
|
|
|
101
123
|
* @since 0.37.0
|
|
102
124
|
*/
|
|
103
125
|
crossOriginEmbedderPolicy?: "require-corp" | "credentialless" | "unsafe-none" | false;
|
|
126
|
+
/** Emit `X-Content-Type-Options: nosniff`. Defaults to `true`. */
|
|
104
127
|
noSniff?: boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Emit `X-XSS-Protection: 0` (disables the legacy XSS auditor, per
|
|
130
|
+
* modern guidance). Defaults to `false` (header omitted).
|
|
131
|
+
*/
|
|
105
132
|
xssProtection?: boolean;
|
|
106
133
|
/**
|
|
107
134
|
* Reporting API endpoint declarations rendered as the
|
|
@@ -165,6 +192,11 @@ export declare const SECURE_HEADERS_MARKER: unique symbol;
|
|
|
165
192
|
* {@link SecureHeadersOptions} for the full list). Pass `false` for any
|
|
166
193
|
* field to disable that header; handler-set headers always win.
|
|
167
194
|
*
|
|
195
|
+
* @param opts - Per-header overrides. Pass `false` to disable a header.
|
|
196
|
+
* @returns A {@link Hooks} bundle ready for `app.use(...)`, stamped with
|
|
197
|
+
* {@link SECURE_HEADERS_MARKER}.
|
|
198
|
+
* @throws {Error} When `frameOptions: false` is combined with a CSP that has
|
|
199
|
+
* no `frame-ancestors` directive (would disable every clickjacking defense).
|
|
168
200
|
* @since 0.1.0
|
|
169
201
|
*/
|
|
170
202
|
export declare function secureHeaders(opts?: SecureHeadersOptions): Hooks;
|
|
@@ -286,11 +318,30 @@ export declare const CSRF_HOOK_MARKER: unique symbol;
|
|
|
286
318
|
export type CorsOriginAllow = (origin: string) => boolean;
|
|
287
319
|
/** Options for {@link cors}. */
|
|
288
320
|
export interface CorsOptions {
|
|
321
|
+
/**
|
|
322
|
+
* Allowed cross-origin caller(s): a single origin string, an array of
|
|
323
|
+
* origins, or a predicate. `"*"` is rejected with `credentials: true`
|
|
324
|
+
* and refused at boot on production `secureDefaults` apps.
|
|
325
|
+
*/
|
|
289
326
|
origin: string | string[] | ((origin: string) => boolean);
|
|
327
|
+
/**
|
|
328
|
+
* Verbs advertised on preflight (`Access-Control-Allow-Methods`).
|
|
329
|
+
* Defaults to `["GET", "HEAD", "POST"]`. `"*"` is rejected.
|
|
330
|
+
*/
|
|
290
331
|
methods?: string[];
|
|
332
|
+
/**
|
|
333
|
+
* Request headers advertised on preflight (`Access-Control-Allow-Headers`).
|
|
334
|
+
* Defaults to `["content-type", "authorization"]`.
|
|
335
|
+
*/
|
|
291
336
|
allowedHeaders?: string[];
|
|
337
|
+
/** Response headers exposed to browser JS (`Access-Control-Expose-Headers`). None by default. */
|
|
292
338
|
exposedHeaders?: string[];
|
|
339
|
+
/**
|
|
340
|
+
* Emit `Access-Control-Allow-Credentials: true`. Defaults to `false`.
|
|
341
|
+
* Cannot be combined with a wildcard `origin`.
|
|
342
|
+
*/
|
|
293
343
|
credentials?: boolean;
|
|
344
|
+
/** Preflight cache lifetime in seconds (`Access-Control-Max-Age`). Defaults to `600`. */
|
|
294
345
|
maxAgeSeconds?: number;
|
|
295
346
|
}
|
|
296
347
|
/**
|
|
@@ -327,6 +378,11 @@ export declare function cors(opts: CorsOptions): Hooks;
|
|
|
327
378
|
* (`@daloyjs/core/rate-limit-redis`).
|
|
328
379
|
*/
|
|
329
380
|
export interface RateLimitStore {
|
|
381
|
+
/**
|
|
382
|
+
* Record one request for `key` in the window of `windowMs` milliseconds
|
|
383
|
+
* and return the updated `count` plus the window's absolute reset time
|
|
384
|
+
* (`resetMs`, epoch milliseconds).
|
|
385
|
+
*/
|
|
330
386
|
hit(key: string, windowMs: number): Promise<{
|
|
331
387
|
count: number;
|
|
332
388
|
resetMs: number;
|
|
@@ -437,6 +493,9 @@ export interface LoginThrottleOptions {
|
|
|
437
493
|
* same `groupId`) across related routes so an attacker cannot bypass the limit
|
|
438
494
|
* by rotating between password, OTP, and reset endpoints.
|
|
439
495
|
*
|
|
496
|
+
* @param opts - Throttle tuning (see {@link LoginThrottleOptions}); every field has a safe default.
|
|
497
|
+
* @returns A {@link Hooks} bundle ready for `app.use(...)` or per-route `hooks`.
|
|
498
|
+
* @throws {Error} When `windowMs`/`max` are not positive integers or the delay options are negative.
|
|
440
499
|
* @since 0.23.0
|
|
441
500
|
*/
|
|
442
501
|
export declare function loginThrottle(opts?: LoginThrottleOptions): Hooks;
|
|
@@ -627,6 +686,11 @@ export interface CsrfOptions {
|
|
|
627
686
|
* allowedOrigins: ["https://app.example.com"],
|
|
628
687
|
* }));
|
|
629
688
|
* ```
|
|
689
|
+
*
|
|
690
|
+
* @param opts - Strategy, cookie/header names, and cookie attributes (see {@link CsrfOptions}).
|
|
691
|
+
* @returns A {@link Hooks} bundle ready for `app.use(...)`, stamped with {@link CSRF_HOOK_MARKER}.
|
|
692
|
+
* @throws {Error} When `strategy` is unknown or the cookie name/attributes are invalid
|
|
693
|
+
* (e.g. `__Host-` prefix without `secure: true`, `path: "/"`, and no `domain`).
|
|
630
694
|
*/
|
|
631
695
|
export declare function csrf(opts?: CsrfOptions): Hooks;
|
|
632
696
|
/** Options for {@link basicAuth}. */
|
|
@@ -723,6 +787,10 @@ export declare const REQUIRE_SCOPES_HOOK_MARKER: unique symbol;
|
|
|
723
787
|
* });
|
|
724
788
|
* ```
|
|
725
789
|
*
|
|
790
|
+
* @param scopes - Required scope strings; must be non-empty and free of RFC 6749 illegal characters.
|
|
791
|
+
* @returns A {@link Hooks} bundle ready for `app.use(...)` or per-route `hooks`,
|
|
792
|
+
* stamped with {@link REQUIRE_SCOPES_HOOK_MARKER}.
|
|
793
|
+
* @throws {Error} When `scopes` is empty or contains a non-string, empty, or illegal-character scope.
|
|
726
794
|
* @since 0.21.0
|
|
727
795
|
*/
|
|
728
796
|
export declare function requireScopes(scopes: readonly string[]): Hooks;
|
package/dist/middleware.js
CHANGED
|
@@ -148,6 +148,11 @@ export const SECURE_HEADERS_MARKER = Symbol.for("daloyjs.middleware.secureHeader
|
|
|
148
148
|
* {@link SecureHeadersOptions} for the full list). Pass `false` for any
|
|
149
149
|
* field to disable that header; handler-set headers always win.
|
|
150
150
|
*
|
|
151
|
+
* @param opts - Per-header overrides. Pass `false` to disable a header.
|
|
152
|
+
* @returns A {@link Hooks} bundle ready for `app.use(...)`, stamped with
|
|
153
|
+
* {@link SECURE_HEADERS_MARKER}.
|
|
154
|
+
* @throws {Error} When `frameOptions: false` is combined with a CSP that has
|
|
155
|
+
* no `frame-ancestors` directive (would disable every clickjacking defense).
|
|
151
156
|
* @since 0.1.0
|
|
152
157
|
*/
|
|
153
158
|
export function secureHeaders(opts = {}) {
|
|
@@ -706,6 +711,9 @@ function wait(ms) {
|
|
|
706
711
|
* same `groupId`) across related routes so an attacker cannot bypass the limit
|
|
707
712
|
* by rotating between password, OTP, and reset endpoints.
|
|
708
713
|
*
|
|
714
|
+
* @param opts - Throttle tuning (see {@link LoginThrottleOptions}); every field has a safe default.
|
|
715
|
+
* @returns A {@link Hooks} bundle ready for `app.use(...)` or per-route `hooks`.
|
|
716
|
+
* @throws {Error} When `windowMs`/`max` are not positive integers or the delay options are negative.
|
|
709
717
|
* @since 0.23.0
|
|
710
718
|
*/
|
|
711
719
|
export function loginThrottle(opts = {}) {
|
|
@@ -912,6 +920,11 @@ function csrfCookieAttributes(opts) {
|
|
|
912
920
|
* allowedOrigins: ["https://app.example.com"],
|
|
913
921
|
* }));
|
|
914
922
|
* ```
|
|
923
|
+
*
|
|
924
|
+
* @param opts - Strategy, cookie/header names, and cookie attributes (see {@link CsrfOptions}).
|
|
925
|
+
* @returns A {@link Hooks} bundle ready for `app.use(...)`, stamped with {@link CSRF_HOOK_MARKER}.
|
|
926
|
+
* @throws {Error} When `strategy` is unknown or the cookie name/attributes are invalid
|
|
927
|
+
* (e.g. `__Host-` prefix without `secure: true`, `path: "/"`, and no `domain`).
|
|
915
928
|
*/
|
|
916
929
|
export function csrf(opts = {}) {
|
|
917
930
|
const strategy = opts.strategy ?? "double-submit";
|
|
@@ -1191,6 +1204,10 @@ function readUserScopes(user) {
|
|
|
1191
1204
|
* });
|
|
1192
1205
|
* ```
|
|
1193
1206
|
*
|
|
1207
|
+
* @param scopes - Required scope strings; must be non-empty and free of RFC 6749 illegal characters.
|
|
1208
|
+
* @returns A {@link Hooks} bundle ready for `app.use(...)` or per-route `hooks`,
|
|
1209
|
+
* stamped with {@link REQUIRE_SCOPES_HOOK_MARKER}.
|
|
1210
|
+
* @throws {Error} When `scopes` is empty or contains a non-string, empty, or illegal-character scope.
|
|
1194
1211
|
* @since 0.21.0
|
|
1195
1212
|
*/
|
|
1196
1213
|
export function requireScopes(scopes) {
|
package/dist/mtls.d.ts
CHANGED
|
@@ -84,6 +84,9 @@ export type ClientCertificateSource = ClientCertificate | undefined | (() => Cli
|
|
|
84
84
|
* Pass a thunk to defer the read until {@link getClientCertificate} is first
|
|
85
85
|
* called; the resolved value is cached back onto the request.
|
|
86
86
|
*
|
|
87
|
+
* @param request The incoming request to stash the certificate on.
|
|
88
|
+
* @param source The certificate, a lazy thunk producing one, or `undefined`
|
|
89
|
+
* when the connection presented no client certificate.
|
|
87
90
|
* @since 0.37.0
|
|
88
91
|
*/
|
|
89
92
|
export declare function setClientCertificate(request: Request, source: ClientCertificateSource): void;
|
|
@@ -93,6 +96,8 @@ export declare function setClientCertificate(request: Request, source: ClientCer
|
|
|
93
96
|
* adapter does not expose TLS peer info). If a lazy thunk was stashed, it is
|
|
94
97
|
* resolved once and the result cached.
|
|
95
98
|
*
|
|
99
|
+
* @param request The request previously seen by {@link setClientCertificate}.
|
|
100
|
+
* @returns The attached certificate, or `undefined` when none was presented.
|
|
96
101
|
* @since 0.37.0
|
|
97
102
|
*/
|
|
98
103
|
export declare function getClientCertificate(request: Request): ClientCertificate | undefined;
|
|
@@ -104,12 +109,19 @@ export declare function getClientCertificate(request: Request): ClientCertificat
|
|
|
104
109
|
* @since 0.37.0
|
|
105
110
|
*/
|
|
106
111
|
export interface PeerCertificateLike {
|
|
112
|
+
/** Subject RDN attributes keyed by short name (e.g. `CN`, `O`). */
|
|
107
113
|
subject?: Record<string, string | string[]> | null;
|
|
114
|
+
/** Issuer RDN attributes keyed by short name. */
|
|
108
115
|
issuer?: Record<string, string | string[]> | null;
|
|
116
|
+
/** Validity start as a Node-formatted date string. */
|
|
109
117
|
valid_from?: string;
|
|
118
|
+
/** Validity end as a Node-formatted date string. */
|
|
110
119
|
valid_to?: string;
|
|
120
|
+
/** SHA-256 fingerprint, colon-delimited hex as Node emits it. */
|
|
111
121
|
fingerprint256?: string;
|
|
122
|
+
/** Certificate serial number as a hex string. */
|
|
112
123
|
serialNumber?: string;
|
|
124
|
+
/** Subject Alternative Names rendered as `DNS:a, IP Address:1.2.3.4, ...`. */
|
|
113
125
|
subjectaltname?: string;
|
|
114
126
|
}
|
|
115
127
|
/**
|
|
@@ -120,6 +132,9 @@ export interface PeerCertificateLike {
|
|
|
120
132
|
* @param raw - The structured peer-certificate object from the TLS socket.
|
|
121
133
|
* @param verified - Whether the socket reported `authorized === true` (the
|
|
122
134
|
* chain was verified against the configured CA).
|
|
135
|
+
* @returns The normalized certificate, or `undefined` when `raw` is missing
|
|
136
|
+
* or carries no identifying fields (no subject, issuer, fingerprint, or
|
|
137
|
+
* serial).
|
|
123
138
|
* @since 0.37.0
|
|
124
139
|
*/
|
|
125
140
|
export declare function normalizePeerCertificate(raw: PeerCertificateLike | null | undefined, verified: boolean): ClientCertificate | undefined;
|
|
@@ -131,8 +146,10 @@ export declare function normalizePeerCertificate(raw: PeerCertificateLike | null
|
|
|
131
146
|
* and is the one returned. Because Envoy only emits XFCC for connections it
|
|
132
147
|
* mutually authenticated, the result is marked `verified: true`.
|
|
133
148
|
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
149
|
+
* @param headerValue Raw XFCC header value; `null`/`undefined` are tolerated.
|
|
150
|
+
* @returns The certificate parsed from the first XFCC element, or `undefined`
|
|
151
|
+
* for an empty or unparseable header. Only trust this header behind a proxy
|
|
152
|
+
* you control that strips client-supplied XFCC.
|
|
136
153
|
* @since 0.37.0
|
|
137
154
|
*/
|
|
138
155
|
export declare function parseForwardedClientCert(headerValue: string | null | undefined): ClientCertificate | undefined;
|
package/dist/mtls.js
CHANGED
|
@@ -36,6 +36,9 @@ const CLIENT_CERT_SYMBOL = Symbol.for("daloyjs.clientCertificate");
|
|
|
36
36
|
* Pass a thunk to defer the read until {@link getClientCertificate} is first
|
|
37
37
|
* called; the resolved value is cached back onto the request.
|
|
38
38
|
*
|
|
39
|
+
* @param request The incoming request to stash the certificate on.
|
|
40
|
+
* @param source The certificate, a lazy thunk producing one, or `undefined`
|
|
41
|
+
* when the connection presented no client certificate.
|
|
39
42
|
* @since 0.37.0
|
|
40
43
|
*/
|
|
41
44
|
export function setClientCertificate(request, source) {
|
|
@@ -47,6 +50,8 @@ export function setClientCertificate(request, source) {
|
|
|
47
50
|
* adapter does not expose TLS peer info). If a lazy thunk was stashed, it is
|
|
48
51
|
* resolved once and the result cached.
|
|
49
52
|
*
|
|
53
|
+
* @param request The request previously seen by {@link setClientCertificate}.
|
|
54
|
+
* @returns The attached certificate, or `undefined` when none was presented.
|
|
50
55
|
* @since 0.37.0
|
|
51
56
|
*/
|
|
52
57
|
export function getClientCertificate(request) {
|
|
@@ -67,6 +72,9 @@ export function getClientCertificate(request) {
|
|
|
67
72
|
* @param raw - The structured peer-certificate object from the TLS socket.
|
|
68
73
|
* @param verified - Whether the socket reported `authorized === true` (the
|
|
69
74
|
* chain was verified against the configured CA).
|
|
75
|
+
* @returns The normalized certificate, or `undefined` when `raw` is missing
|
|
76
|
+
* or carries no identifying fields (no subject, issuer, fingerprint, or
|
|
77
|
+
* serial).
|
|
70
78
|
* @since 0.37.0
|
|
71
79
|
*/
|
|
72
80
|
export function normalizePeerCertificate(raw, verified) {
|
|
@@ -205,8 +213,10 @@ function cnFromDN(dn) {
|
|
|
205
213
|
* and is the one returned. Because Envoy only emits XFCC for connections it
|
|
206
214
|
* mutually authenticated, the result is marked `verified: true`.
|
|
207
215
|
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
216
|
+
* @param headerValue Raw XFCC header value; `null`/`undefined` are tolerated.
|
|
217
|
+
* @returns The certificate parsed from the first XFCC element, or `undefined`
|
|
218
|
+
* for an empty or unparseable header. Only trust this header behind a proxy
|
|
219
|
+
* you control that strips client-supplied XFCC.
|
|
210
220
|
* @since 0.37.0
|
|
211
221
|
*/
|
|
212
222
|
export function parseForwardedClientCert(headerValue) {
|