@daloyjs/core 1.0.0-beta.5 → 1.0.0-beta.6
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 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/mcp.d.ts +432 -0
- package/dist/mcp.js +419 -0
- package/dist/sbom.cdx.json +9 -9
- package/dist/sbom.spdx.json +5 -5
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -509,7 +509,7 @@ The core only ever sees `Request → Response`. Adapters live at the edge.
|
|
|
509
509
|
|
|
510
510
|
## Status
|
|
511
511
|
|
|
512
|
-
DaloyJS is now in the **`1.0.0` beta** (`1.0.0-beta.
|
|
512
|
+
DaloyJS is now in the **`1.0.0` beta** (`1.0.0-beta.6`). The public API is feature-complete and stable for the 1.0 line; from `1.0.0` onward, breaking changes follow SemVer and deprecations get at least one minor cycle. Small adjustments are still possible before the `1.0.0` GA if beta feedback surfaces something. The framework is already in use for production trials.
|
|
513
513
|
|
|
514
514
|
**Release quality bar.** Every release ships with **≥90% line + function coverage and ≥90% branch coverage**, strict TypeScript, OpenSSF Scorecard, CodeQL + Opengrep dual SAST, zizmor workflow linting, and npm provenance. Coverage was relaxed from a former 100% gate so complex security work isn't blocked chasing throwaway tests for unreachable defensive branches or tsx source-map phantoms; see [AGENTS.md](AGENTS.md) for the policy.
|
|
515
515
|
|
|
@@ -520,6 +520,7 @@ DaloyJS is now in the **`1.0.0` beta** (`1.0.0-beta.5`). The public API is featu
|
|
|
520
520
|
- Zero-config OpenAPI `info` autofill from `package.json` (Node / Bun) or `deno.json` / `deno.jsonc` (Deno); explicit `openapi.info` values always win.
|
|
521
521
|
- RFC 7231 + RFC 5789 HTTP-method allowlist enforced inside `app.route()` (WebDAV, `TRACE`, `CONNECT` rejected at the framework boundary).
|
|
522
522
|
- AI-friendly route metadata via optional `meta: { examples, extensions, summary, description, tags }`; examples are validated against your schemas at build time, surfaced as OpenAPI `examples` + `x-daloy-*` extensions, and dumped as `routes.json` / `routes.yaml` via `daloy inspect --ai`.
|
|
523
|
+
- Dependency-free MCP Streamable HTTP server helpers at `@daloyjs/core/mcp`: `createMcpHandler()` exposes tools, resources, and prompts over JSON-RPC 2.0, while `mcpRoutes("/mcp", handler)` mounts the POST / GET / OPTIONS Daloy routes for a dedicated MCP service with the same auth, rate-limit, body-limit, and timeout middleware as any other app.
|
|
523
524
|
- API lifecycle and breaking-change detection: mark routes `deprecated` or give them a `sunset` date to emit RFC 8594 `Deprecation` / `Sunset` headers and an `x-sunset` OpenAPI extension, then gate CI with `diffOpenAPI()` / the `daloy diff` command, which fail on a breaking change versus the last published spec.
|
|
524
525
|
- In-process test client (`app.request()`), contract-test runner (gated in CI via `daloy inspect --check` and shipped as a default test in every `create-daloy` template), in-process typed client, and Hey API codegen via `pnpm gen`.
|
|
525
526
|
|
package/dist/index.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export type { StandardSchemaV1 } from "./schema.js";
|
|
|
19
19
|
export { validate, isStandardSchema } from "./schema.js";
|
|
20
20
|
export { diffOpenAPI, hasBreakingChanges } from "./openapi-diff.js";
|
|
21
21
|
export type { ChangeSeverity, OpenAPIChange, OpenAPIDiffResult } from "./openapi-diff.js";
|
|
22
|
+
export { MCP_DEFAULT_MAX_BODY_BYTES, MCP_PROTOCOL_VERSION, MCP_PROTOCOL_VERSIONS, McpToolError, createMcpHandler, mcpRoutes, } from "./mcp.js";
|
|
23
|
+
export type { McpContent, McpEmbeddedResourceContent, McpHandler, McpHandlerOptions, McpImageContent, McpJsonObject, McpJsonRpcId, McpJsonSchema, McpJsonValue, McpPrompt, McpPromptArgument, McpPromptDefinition, McpPromptMessage, McpPromptResult, McpRequestContext, McpResource, McpResourceContents, McpResourceDefinition, McpServerInfo, McpTextContent, McpTool, McpToolHandler, McpToolResult, } from "./mcp.js";
|
|
22
24
|
export { readBodyLimited, safeJsonParse, isForbiddenObjectKey, sanitizeHeaderName, sanitizeHeaderValue, timingSafeEqual, randomId, assertNoDuplicateSingletonHeaders, assertNoReservedInternalHeaders, assertHeaderCountWithinLimit, DEFAULT_MAX_HEADER_COUNT, RESERVED_INBOUND_HEADER_PREFIXES, SMUGGLING_SINGLETON_HEADERS, verifyWebhookSignature, signWebhookPayload, WEBHOOK_DEFAULT_TOLERANCE_SECONDS, assertStrongSecret, MIN_PROD_SECRET_BYTES, WEAK_SECRET_STRINGS, sanitizeFilename, assertSafeRelativePath, hasMongoOperatorKeys, assertNoMongoOperators, } from "./security.js";
|
|
23
25
|
export type { WebhookHmacAlgorithm } from "./security.js";
|
|
24
26
|
export { requestId, secureHeaders, SECURE_HEADERS_MARKER, cors, CORS_HOOK_MARKER, CORS_ORIGIN_ALLOW_MARKER, CORS_WILDCARD_ORIGIN_MARKER, rateLimit, loginThrottle, timing, bearerAuth, basicAuth, csrf, CSRF_HOOK_MARKER, fetchMetadata, requireScopes, REQUIRE_SCOPES_AGGREGATE_KEY, REQUIRE_SCOPES_HOOK_MARKER, _resetSharedRateLimitStoresForTests, } from "./middleware.js";
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ export { defineDependency, DEPENDENCY_MARKER } from "./dependency.js";
|
|
|
11
11
|
export { HttpError, BadRequestError, ValidationError, NotFoundError, ConflictError, UnauthorizedError, ForbiddenError, MethodNotAllowedError, PayloadTooLargeError, RequestHeaderFieldsTooLargeError, UnsupportedMediaTypeError, TooManyRequestsError, RequestTimeoutError, InternalError, MessageLeakError, httpError, SAFE_CUSTOM_ERROR_RESPONSE_HEADERS, checkCustomErrorResponseHeaders, } from "./errors.js";
|
|
12
12
|
export { validate, isStandardSchema } from "./schema.js";
|
|
13
13
|
export { diffOpenAPI, hasBreakingChanges } from "./openapi-diff.js";
|
|
14
|
+
export { MCP_DEFAULT_MAX_BODY_BYTES, MCP_PROTOCOL_VERSION, MCP_PROTOCOL_VERSIONS, McpToolError, createMcpHandler, mcpRoutes, } from "./mcp.js";
|
|
14
15
|
export { readBodyLimited, safeJsonParse, isForbiddenObjectKey, sanitizeHeaderName, sanitizeHeaderValue, timingSafeEqual, randomId, assertNoDuplicateSingletonHeaders, assertNoReservedInternalHeaders, assertHeaderCountWithinLimit, DEFAULT_MAX_HEADER_COUNT, RESERVED_INBOUND_HEADER_PREFIXES, SMUGGLING_SINGLETON_HEADERS, verifyWebhookSignature, signWebhookPayload, WEBHOOK_DEFAULT_TOLERANCE_SECONDS, assertStrongSecret, MIN_PROD_SECRET_BYTES, WEAK_SECRET_STRINGS, sanitizeFilename, assertSafeRelativePath, hasMongoOperatorKeys, assertNoMongoOperators, } from "./security.js";
|
|
15
16
|
export { requestId, secureHeaders, SECURE_HEADERS_MARKER, cors, CORS_HOOK_MARKER, CORS_ORIGIN_ALLOW_MARKER, CORS_WILDCARD_ORIGIN_MARKER, rateLimit, loginThrottle, timing, bearerAuth, basicAuth, csrf, CSRF_HOOK_MARKER, fetchMetadata, requireScopes, REQUIRE_SCOPES_AGGREGATE_KEY, REQUIRE_SCOPES_HOOK_MARKER, _resetSharedRateLimitStoresForTests, } from "./middleware.js";
|
|
16
17
|
export { etag } from "./etag.js";
|
package/dist/mcp.d.ts
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import type { PathString, RouteDefinition } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Latest MCP protocol version DaloyJS negotiates by default.
|
|
4
|
+
*
|
|
5
|
+
* @see https://modelcontextprotocol.io/specification/2025-11-25
|
|
6
|
+
* @since 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
export declare const MCP_PROTOCOL_VERSION = "2025-11-25";
|
|
9
|
+
/**
|
|
10
|
+
* Protocol revisions accepted by {@link createMcpHandler} unless the caller
|
|
11
|
+
* provides an explicit `protocolVersions` list.
|
|
12
|
+
*
|
|
13
|
+
* @since 1.0.0
|
|
14
|
+
*/
|
|
15
|
+
export declare const MCP_PROTOCOL_VERSIONS: readonly string[];
|
|
16
|
+
/**
|
|
17
|
+
* Default maximum accepted JSON-RPC request body for a DaloyJS MCP endpoint.
|
|
18
|
+
* The cap is intentionally small because MCP calls should carry parameters,
|
|
19
|
+
* not bulk uploads. Raise it per endpoint when a real tool needs larger input.
|
|
20
|
+
*
|
|
21
|
+
* @since 1.0.0
|
|
22
|
+
*/
|
|
23
|
+
export declare const MCP_DEFAULT_MAX_BODY_BYTES: number;
|
|
24
|
+
/**
|
|
25
|
+
* JSON value accepted in MCP schemas, structured results, and metadata.
|
|
26
|
+
*
|
|
27
|
+
* @since 1.0.0
|
|
28
|
+
*/
|
|
29
|
+
export type McpJsonValue = null | boolean | number | string | McpJsonValue[] | {
|
|
30
|
+
[key: string]: McpJsonValue;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* JSON object used for MCP tool arguments and structured payloads.
|
|
34
|
+
*
|
|
35
|
+
* @since 1.0.0
|
|
36
|
+
*/
|
|
37
|
+
export type McpJsonObject = {
|
|
38
|
+
[key: string]: McpJsonValue;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* JSON Schema fragment advertised to MCP clients for a tool or prompt
|
|
42
|
+
* argument object. DaloyJS does not bundle a schema validator here, so the
|
|
43
|
+
* schema is documentation and client-side guidance. Validate sensitive inputs
|
|
44
|
+
* inside your handler before touching databases, files, or remote services.
|
|
45
|
+
*
|
|
46
|
+
* @since 1.0.0
|
|
47
|
+
*/
|
|
48
|
+
export type McpJsonSchema = McpJsonObject;
|
|
49
|
+
/**
|
|
50
|
+
* JSON-RPC id type accepted by MCP requests.
|
|
51
|
+
*
|
|
52
|
+
* @since 1.0.0
|
|
53
|
+
*/
|
|
54
|
+
export type McpJsonRpcId = string | number | null;
|
|
55
|
+
/**
|
|
56
|
+
* Identity block returned from the MCP `initialize` handshake.
|
|
57
|
+
*
|
|
58
|
+
* @since 1.0.0
|
|
59
|
+
*/
|
|
60
|
+
export interface McpServerInfo {
|
|
61
|
+
/** Stable machine-readable server name, for example `"acme-inventory-mcp"`. */
|
|
62
|
+
name: string;
|
|
63
|
+
/** Optional human-readable display title for MCP clients. */
|
|
64
|
+
title?: string;
|
|
65
|
+
/** Server version surfaced to clients for debugging and compatibility. */
|
|
66
|
+
version: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Per-request context passed to tool, resource, and prompt handlers.
|
|
70
|
+
*
|
|
71
|
+
* @since 1.0.0
|
|
72
|
+
*/
|
|
73
|
+
export interface McpRequestContext {
|
|
74
|
+
/** The original HTTP request received by the DaloyJS route. */
|
|
75
|
+
request: Request;
|
|
76
|
+
/**
|
|
77
|
+
* Protocol version selected for this call. Before `initialize`, this is the
|
|
78
|
+
* version from the `MCP-Protocol-Version` header when present, otherwise the
|
|
79
|
+
* handler's preferred protocol version.
|
|
80
|
+
*/
|
|
81
|
+
protocolVersion: string;
|
|
82
|
+
/** JSON-RPC id for request/response correlation. */
|
|
83
|
+
id: McpJsonRpcId;
|
|
84
|
+
/** Raw MCP method name, such as `"tools/call"` or `"resources/read"`. */
|
|
85
|
+
method: string;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Text content block returned from an MCP tool, resource, or prompt.
|
|
89
|
+
*
|
|
90
|
+
* @since 1.0.0
|
|
91
|
+
*/
|
|
92
|
+
export interface McpTextContent {
|
|
93
|
+
type: "text";
|
|
94
|
+
text: string;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Image content block returned from an MCP tool.
|
|
98
|
+
*
|
|
99
|
+
* `data` is base64-encoded image bytes. Keep images small; for large assets,
|
|
100
|
+
* return a resource link or URL-bearing text instead.
|
|
101
|
+
*
|
|
102
|
+
* @since 1.0.0
|
|
103
|
+
*/
|
|
104
|
+
export interface McpImageContent {
|
|
105
|
+
type: "image";
|
|
106
|
+
data: string;
|
|
107
|
+
mimeType: string;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Embedded resource content block returned from an MCP tool.
|
|
111
|
+
*
|
|
112
|
+
* @since 1.0.0
|
|
113
|
+
*/
|
|
114
|
+
export interface McpEmbeddedResourceContent {
|
|
115
|
+
type: "resource";
|
|
116
|
+
resource: McpResourceContents;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Content block supported by the dependency-free MCP helper.
|
|
120
|
+
*
|
|
121
|
+
* @since 1.0.0
|
|
122
|
+
*/
|
|
123
|
+
export type McpContent = McpTextContent | McpImageContent | McpEmbeddedResourceContent;
|
|
124
|
+
/**
|
|
125
|
+
* Result returned by an MCP tool handler.
|
|
126
|
+
*
|
|
127
|
+
* `isError` marks caller-correctable tool failures, such as invalid input or a
|
|
128
|
+
* domain error. Unexpected thrown errors become JSON-RPC internal errors and
|
|
129
|
+
* are redacted in production.
|
|
130
|
+
*
|
|
131
|
+
* @since 1.0.0
|
|
132
|
+
*/
|
|
133
|
+
export interface McpToolResult {
|
|
134
|
+
/** Human or model-readable content blocks returned to the MCP client. */
|
|
135
|
+
content: McpContent[];
|
|
136
|
+
/** Optional structured payload for clients that can consume typed output. */
|
|
137
|
+
structuredContent?: McpJsonObject;
|
|
138
|
+
/** Set to `true` for domain/tool errors the model may recover from. */
|
|
139
|
+
isError?: boolean;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Handler for a single MCP tool.
|
|
143
|
+
*
|
|
144
|
+
* @typeParam TArgs - Type expected in `params.arguments` for this tool.
|
|
145
|
+
* @param args - Tool arguments supplied by the MCP client. They are typed for
|
|
146
|
+
* developer experience but are still untrusted JSON at runtime.
|
|
147
|
+
* @param ctx - Request metadata and the original HTTP request.
|
|
148
|
+
* @returns Text shorthand or a full {@link McpToolResult}.
|
|
149
|
+
* @throws {McpToolError} for caller-correctable failures that should be
|
|
150
|
+
* returned as an MCP tool error result.
|
|
151
|
+
*
|
|
152
|
+
* @since 1.0.0
|
|
153
|
+
*/
|
|
154
|
+
export type McpToolHandler<TArgs extends Record<string, unknown> = Record<string, unknown>> = (args: TArgs, ctx: McpRequestContext) => string | McpToolResult | Promise<string | McpToolResult>;
|
|
155
|
+
/**
|
|
156
|
+
* Definition of a callable MCP tool.
|
|
157
|
+
*
|
|
158
|
+
* Tools are model-controlled in MCP: clients may let the language model decide
|
|
159
|
+
* when to call them. Treat every tool as a public API operation and enforce
|
|
160
|
+
* authentication, authorization, rate limits, and validation before side
|
|
161
|
+
* effects.
|
|
162
|
+
*
|
|
163
|
+
* @typeParam TArgs - Type expected by this tool's handler.
|
|
164
|
+
* @since 1.0.0
|
|
165
|
+
*/
|
|
166
|
+
export interface McpTool<TArgs extends Record<string, unknown> = Record<string, unknown>> {
|
|
167
|
+
/** Unique tool name within this MCP server. Prefer namespaced verbs. */
|
|
168
|
+
name: string;
|
|
169
|
+
/** Optional human-readable title displayed by clients. */
|
|
170
|
+
title?: string;
|
|
171
|
+
/** Clear description of when the model should use this tool. */
|
|
172
|
+
description: string;
|
|
173
|
+
/** JSON Schema for `params.arguments`. */
|
|
174
|
+
inputSchema: McpJsonSchema;
|
|
175
|
+
/** Execute the tool with untrusted JSON arguments. */
|
|
176
|
+
handler: McpToolHandler<TArgs>;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Resource metadata returned from `resources/list`.
|
|
180
|
+
*
|
|
181
|
+
* @since 1.0.0
|
|
182
|
+
*/
|
|
183
|
+
export interface McpResource {
|
|
184
|
+
/** Unique resource URI, for example `"daloy://schema/inventory"`. */
|
|
185
|
+
uri: string;
|
|
186
|
+
/** Stable resource name. */
|
|
187
|
+
name: string;
|
|
188
|
+
/** Optional human-readable title. */
|
|
189
|
+
title?: string;
|
|
190
|
+
/** Optional description shown by clients. */
|
|
191
|
+
description?: string;
|
|
192
|
+
/** MIME type returned by `resources/read`, such as `"application/json"`. */
|
|
193
|
+
mimeType?: string;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Resource payload returned from `resources/read`.
|
|
197
|
+
*
|
|
198
|
+
* Use either `text` for UTF-8 content or `blob` for base64-encoded binary
|
|
199
|
+
* content. Set `mimeType` so clients know how to present the resource.
|
|
200
|
+
*
|
|
201
|
+
* @since 1.0.0
|
|
202
|
+
*/
|
|
203
|
+
export interface McpResourceContents {
|
|
204
|
+
/** URI of the resource being returned. */
|
|
205
|
+
uri: string;
|
|
206
|
+
/** MIME type of the returned content. */
|
|
207
|
+
mimeType?: string;
|
|
208
|
+
/** UTF-8 text content. */
|
|
209
|
+
text?: string;
|
|
210
|
+
/** Base64-encoded binary content. */
|
|
211
|
+
blob?: string;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Definition of a readable MCP resource.
|
|
215
|
+
*
|
|
216
|
+
* Resources are application-controlled context. They are a good fit for
|
|
217
|
+
* schemas, read-only records, catalogs, runbooks, and other context a client
|
|
218
|
+
* can choose to include before a tool call.
|
|
219
|
+
*
|
|
220
|
+
* @since 1.0.0
|
|
221
|
+
*/
|
|
222
|
+
export interface McpResourceDefinition extends McpResource {
|
|
223
|
+
/**
|
|
224
|
+
* Read the resource contents for `resources/read`.
|
|
225
|
+
*
|
|
226
|
+
* @param ctx - Request metadata and the original HTTP request.
|
|
227
|
+
* @returns One or more content entries for this resource.
|
|
228
|
+
*/
|
|
229
|
+
read: (ctx: McpRequestContext) => McpResourceContents | McpResourceContents[] | Promise<McpResourceContents | McpResourceContents[]>;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Argument metadata for an MCP prompt.
|
|
233
|
+
*
|
|
234
|
+
* @since 1.0.0
|
|
235
|
+
*/
|
|
236
|
+
export interface McpPromptArgument {
|
|
237
|
+
/** Argument name accepted by the prompt. */
|
|
238
|
+
name: string;
|
|
239
|
+
/** Optional description displayed by clients. */
|
|
240
|
+
description?: string;
|
|
241
|
+
/** Whether the argument is required. */
|
|
242
|
+
required?: boolean;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Prompt metadata returned from `prompts/list`.
|
|
246
|
+
*
|
|
247
|
+
* @since 1.0.0
|
|
248
|
+
*/
|
|
249
|
+
export interface McpPrompt {
|
|
250
|
+
/** Unique prompt name within this MCP server. */
|
|
251
|
+
name: string;
|
|
252
|
+
/** Optional human-readable title. */
|
|
253
|
+
title?: string;
|
|
254
|
+
/** Optional prompt description. */
|
|
255
|
+
description?: string;
|
|
256
|
+
/** Prompt arguments clients may supply to `prompts/get`. */
|
|
257
|
+
arguments?: McpPromptArgument[];
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Message returned from `prompts/get`.
|
|
261
|
+
*
|
|
262
|
+
* @since 1.0.0
|
|
263
|
+
*/
|
|
264
|
+
export interface McpPromptMessage {
|
|
265
|
+
/** Role that should receive the prompt content. */
|
|
266
|
+
role: "user" | "assistant";
|
|
267
|
+
/** Prompt content block. */
|
|
268
|
+
content: McpTextContent | McpImageContent | McpEmbeddedResourceContent;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Result returned by an MCP prompt handler.
|
|
272
|
+
*
|
|
273
|
+
* @since 1.0.0
|
|
274
|
+
*/
|
|
275
|
+
export interface McpPromptResult {
|
|
276
|
+
/** Optional description of the rendered prompt. */
|
|
277
|
+
description?: string;
|
|
278
|
+
/** Messages the client can inject into the model conversation. */
|
|
279
|
+
messages: McpPromptMessage[];
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Definition of a reusable MCP prompt.
|
|
283
|
+
*
|
|
284
|
+
* @since 1.0.0
|
|
285
|
+
*/
|
|
286
|
+
export interface McpPromptDefinition extends McpPrompt {
|
|
287
|
+
/**
|
|
288
|
+
* Render the prompt for `prompts/get`.
|
|
289
|
+
*
|
|
290
|
+
* @param args - Prompt arguments supplied by the MCP client.
|
|
291
|
+
* @param ctx - Request metadata and the original HTTP request.
|
|
292
|
+
* @returns Prompt messages.
|
|
293
|
+
*/
|
|
294
|
+
get: (args: Record<string, unknown>, ctx: McpRequestContext) => McpPromptResult | Promise<McpPromptResult>;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Caller-correctable MCP tool/resource/prompt error.
|
|
298
|
+
*
|
|
299
|
+
* Throw this when the model supplied bad arguments, referenced a missing
|
|
300
|
+
* domain object, or otherwise made a recoverable call. Tool errors become
|
|
301
|
+
* `{ isError: true }` tool results; resource and prompt errors become
|
|
302
|
+
* JSON-RPC invalid-params errors. Unexpected errors are treated as internal
|
|
303
|
+
* server failures and are redacted in production.
|
|
304
|
+
*
|
|
305
|
+
* @since 1.0.0
|
|
306
|
+
*/
|
|
307
|
+
export declare class McpToolError extends Error {
|
|
308
|
+
/**
|
|
309
|
+
* Create a recoverable MCP handler error.
|
|
310
|
+
*
|
|
311
|
+
* @param message - Safe, caller-visible explanation.
|
|
312
|
+
*/
|
|
313
|
+
constructor(message: string);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Options for {@link createMcpHandler}.
|
|
317
|
+
*
|
|
318
|
+
* @since 1.0.0
|
|
319
|
+
*/
|
|
320
|
+
export interface McpHandlerOptions {
|
|
321
|
+
/** Server identity returned from the `initialize` handshake. */
|
|
322
|
+
serverInfo: McpServerInfo;
|
|
323
|
+
/** Optional guidance returned from `initialize`. */
|
|
324
|
+
instructions?: string;
|
|
325
|
+
/** Callable tools exposed through `tools/list` and `tools/call`. */
|
|
326
|
+
tools?: readonly McpTool[];
|
|
327
|
+
/** Readable resources exposed through `resources/list` and `resources/read`. */
|
|
328
|
+
resources?: readonly McpResourceDefinition[];
|
|
329
|
+
/** Reusable prompts exposed through `prompts/list` and `prompts/get`. */
|
|
330
|
+
prompts?: readonly McpPromptDefinition[];
|
|
331
|
+
/** Accepted MCP protocol versions. Defaults to {@link MCP_PROTOCOL_VERSIONS}. */
|
|
332
|
+
protocolVersions?: readonly string[];
|
|
333
|
+
/**
|
|
334
|
+
* Protocol version returned when the client asks for an unsupported version.
|
|
335
|
+
* Defaults to {@link MCP_PROTOCOL_VERSION}.
|
|
336
|
+
*/
|
|
337
|
+
preferredProtocolVersion?: string;
|
|
338
|
+
/** Maximum accepted JSON-RPC body size in bytes. Defaults to 256 KiB. */
|
|
339
|
+
maxBodyBytes?: number;
|
|
340
|
+
/**
|
|
341
|
+
* Extra headers added to every JSON response. Use this for endpoint-local
|
|
342
|
+
* cache, CORS, or deployment metadata. Authentication should usually live in
|
|
343
|
+
* DaloyJS middleware before the MCP route.
|
|
344
|
+
*/
|
|
345
|
+
headers?: Record<string, string>;
|
|
346
|
+
/**
|
|
347
|
+
* Include development error details in JSON-RPC internal errors. Defaults to
|
|
348
|
+
* `process.env.NODE_ENV !== "production"` when `process` exists.
|
|
349
|
+
*/
|
|
350
|
+
exposeInternalErrors?: boolean;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Fetch-compatible handler returned by {@link createMcpHandler}.
|
|
354
|
+
*
|
|
355
|
+
* @param request - Incoming HTTP request for the MCP endpoint.
|
|
356
|
+
* @returns A standard `Response` containing a JSON-RPC response, `202` for
|
|
357
|
+
* accepted notifications, or `405` for unsupported HTTP methods.
|
|
358
|
+
*
|
|
359
|
+
* @since 1.0.0
|
|
360
|
+
*/
|
|
361
|
+
export type McpHandler = (request: Request) => Promise<Response>;
|
|
362
|
+
/**
|
|
363
|
+
* Create a dependency-free MCP Streamable HTTP endpoint handler.
|
|
364
|
+
*
|
|
365
|
+
* The handler implements the server side of MCP over one HTTP endpoint:
|
|
366
|
+
* `initialize`, `ping`, `tools/list`, `tools/call`, `resources/list`,
|
|
367
|
+
* `resources/read`, `prompts/list`, and `prompts/get`. It accepts JSON-RPC
|
|
368
|
+
* requests over `POST`, acknowledges notifications with `202`, validates the
|
|
369
|
+
* `MCP-Protocol-Version` header, bounds request bodies, and returns JSON-RPC
|
|
370
|
+
* errors for malformed input.
|
|
371
|
+
*
|
|
372
|
+
* It intentionally does not spawn stdio servers, manage OAuth metadata, keep
|
|
373
|
+
* durable sessions, or open server-initiated SSE streams. Use DaloyJS
|
|
374
|
+
* middleware for authentication and authorization, and run this on a dedicated
|
|
375
|
+
* Daloy app when your MCP server has a different trust boundary than your REST
|
|
376
|
+
* API.
|
|
377
|
+
*
|
|
378
|
+
* @param options - Server identity, capabilities, limits, and response headers.
|
|
379
|
+
* @returns A Fetch-compatible request handler suitable for {@link mcpRoutes}
|
|
380
|
+
* or for direct use in any web-standard runtime.
|
|
381
|
+
*
|
|
382
|
+
* @example
|
|
383
|
+
* ```ts
|
|
384
|
+
* const mcp = createMcpHandler({
|
|
385
|
+
* serverInfo: { name: "inventory-mcp", version: "1.0.0" },
|
|
386
|
+
* tools: [
|
|
387
|
+
* {
|
|
388
|
+
* name: "inventory_lookup",
|
|
389
|
+
* description: "Look up inventory by SKU.",
|
|
390
|
+
* inputSchema: {
|
|
391
|
+
* type: "object",
|
|
392
|
+
* properties: { sku: { type: "string" } },
|
|
393
|
+
* required: ["sku"],
|
|
394
|
+
* additionalProperties: false,
|
|
395
|
+
* },
|
|
396
|
+
* handler: async ({ sku }) => `SKU ${sku} has 42 units.`,
|
|
397
|
+
* },
|
|
398
|
+
* ],
|
|
399
|
+
* });
|
|
400
|
+
* ```
|
|
401
|
+
*
|
|
402
|
+
* @since 1.0.0
|
|
403
|
+
*/
|
|
404
|
+
export declare function createMcpHandler(options: McpHandlerOptions): McpHandler;
|
|
405
|
+
/**
|
|
406
|
+
* Build the Daloy route definitions for a Streamable HTTP MCP endpoint.
|
|
407
|
+
*
|
|
408
|
+
* Register each returned route on the Daloy app that should host MCP. A
|
|
409
|
+
* separate app is often the cleanest production shape: the REST API can keep
|
|
410
|
+
* its public contract and auth policy, while the MCP server can use its own
|
|
411
|
+
* bearer token, rate limit, network allowlist, and tool set.
|
|
412
|
+
*
|
|
413
|
+
* @param path - Public MCP endpoint path, usually `"/mcp"`.
|
|
414
|
+
* @param handler - Handler returned by {@link createMcpHandler}.
|
|
415
|
+
* @returns Route definitions for `POST`, `GET`, and `OPTIONS` on the same
|
|
416
|
+
* path. `POST` is the actual MCP transport; `GET` gives a human-readable
|
|
417
|
+
* 405 hint because this helper does not open server-initiated SSE streams;
|
|
418
|
+
* `OPTIONS` supports preflight when CORS middleware is installed.
|
|
419
|
+
*
|
|
420
|
+
* @example
|
|
421
|
+
* ```ts
|
|
422
|
+
* const app = new App();
|
|
423
|
+
* const mcp = createMcpHandler({ serverInfo, tools });
|
|
424
|
+
*
|
|
425
|
+
* for (const route of mcpRoutes("/mcp", mcp)) {
|
|
426
|
+
* app.route(route);
|
|
427
|
+
* }
|
|
428
|
+
* ```
|
|
429
|
+
*
|
|
430
|
+
* @since 1.0.0
|
|
431
|
+
*/
|
|
432
|
+
export declare function mcpRoutes(path: PathString, handler: McpHandler): RouteDefinition<PathString, "GET" | "POST" | "OPTIONS">[];
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Latest MCP protocol version DaloyJS negotiates by default.
|
|
3
|
+
*
|
|
4
|
+
* @see https://modelcontextprotocol.io/specification/2025-11-25
|
|
5
|
+
* @since 1.0.0
|
|
6
|
+
*/
|
|
7
|
+
export const MCP_PROTOCOL_VERSION = "2025-11-25";
|
|
8
|
+
/**
|
|
9
|
+
* Protocol revisions accepted by {@link createMcpHandler} unless the caller
|
|
10
|
+
* provides an explicit `protocolVersions` list.
|
|
11
|
+
*
|
|
12
|
+
* @since 1.0.0
|
|
13
|
+
*/
|
|
14
|
+
export const MCP_PROTOCOL_VERSIONS = Object.freeze([
|
|
15
|
+
"2024-11-05",
|
|
16
|
+
"2025-03-26",
|
|
17
|
+
"2025-06-18",
|
|
18
|
+
"2025-11-25",
|
|
19
|
+
]);
|
|
20
|
+
/**
|
|
21
|
+
* Default maximum accepted JSON-RPC request body for a DaloyJS MCP endpoint.
|
|
22
|
+
* The cap is intentionally small because MCP calls should carry parameters,
|
|
23
|
+
* not bulk uploads. Raise it per endpoint when a real tool needs larger input.
|
|
24
|
+
*
|
|
25
|
+
* @since 1.0.0
|
|
26
|
+
*/
|
|
27
|
+
export const MCP_DEFAULT_MAX_BODY_BYTES = 1 << 18;
|
|
28
|
+
const PARSE_ERROR = -32700;
|
|
29
|
+
const INVALID_REQUEST = -32600;
|
|
30
|
+
const METHOD_NOT_FOUND = -32601;
|
|
31
|
+
const INVALID_PARAMS = -32602;
|
|
32
|
+
const INTERNAL_ERROR = -32603;
|
|
33
|
+
const MCP_JSON_RESPONSE_SCHEMA = {
|
|
34
|
+
"~standard": {
|
|
35
|
+
version: 1,
|
|
36
|
+
vendor: "daloyjs",
|
|
37
|
+
validate: (value) => ({ value }),
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Caller-correctable MCP tool/resource/prompt error.
|
|
42
|
+
*
|
|
43
|
+
* Throw this when the model supplied bad arguments, referenced a missing
|
|
44
|
+
* domain object, or otherwise made a recoverable call. Tool errors become
|
|
45
|
+
* `{ isError: true }` tool results; resource and prompt errors become
|
|
46
|
+
* JSON-RPC invalid-params errors. Unexpected errors are treated as internal
|
|
47
|
+
* server failures and are redacted in production.
|
|
48
|
+
*
|
|
49
|
+
* @since 1.0.0
|
|
50
|
+
*/
|
|
51
|
+
export class McpToolError extends Error {
|
|
52
|
+
/**
|
|
53
|
+
* Create a recoverable MCP handler error.
|
|
54
|
+
*
|
|
55
|
+
* @param message - Safe, caller-visible explanation.
|
|
56
|
+
*/
|
|
57
|
+
constructor(message) {
|
|
58
|
+
super(message);
|
|
59
|
+
this.name = "McpToolError";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function isJsonRpcId(value) {
|
|
63
|
+
return value === null || typeof value === "string" || typeof value === "number";
|
|
64
|
+
}
|
|
65
|
+
function jsonResponse(body, status, extraHeaders) {
|
|
66
|
+
return new Response(JSON.stringify(body), {
|
|
67
|
+
status,
|
|
68
|
+
headers: {
|
|
69
|
+
"content-type": "application/json; charset=utf-8",
|
|
70
|
+
"cache-control": "no-store",
|
|
71
|
+
...(extraHeaders ?? {}),
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function rpcResult(id, result, extraHeaders) {
|
|
76
|
+
return jsonResponse({ jsonrpc: "2.0", id, result }, 200, extraHeaders);
|
|
77
|
+
}
|
|
78
|
+
function rpcError(id, code, message, data, status, extraHeaders) {
|
|
79
|
+
const error = { code, message };
|
|
80
|
+
if (data !== undefined)
|
|
81
|
+
error.data = data;
|
|
82
|
+
return jsonResponse({ jsonrpc: "2.0", id, error }, status, extraHeaders);
|
|
83
|
+
}
|
|
84
|
+
function safeInternalErrorData(error, expose) {
|
|
85
|
+
if (!expose)
|
|
86
|
+
return undefined;
|
|
87
|
+
return { detail: error instanceof Error ? error.message : String(error) };
|
|
88
|
+
}
|
|
89
|
+
function asRecord(value) {
|
|
90
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
91
|
+
? value
|
|
92
|
+
: {};
|
|
93
|
+
}
|
|
94
|
+
function publicTool(tool) {
|
|
95
|
+
const { handler: _handler, ...rest } = tool;
|
|
96
|
+
return rest;
|
|
97
|
+
}
|
|
98
|
+
function publicResource(resource) {
|
|
99
|
+
const { read: _read, ...rest } = resource;
|
|
100
|
+
return rest;
|
|
101
|
+
}
|
|
102
|
+
function publicPrompt(prompt) {
|
|
103
|
+
const { get: _get, ...rest } = prompt;
|
|
104
|
+
return rest;
|
|
105
|
+
}
|
|
106
|
+
function normalizeToolResult(value) {
|
|
107
|
+
return typeof value === "string" ? { content: [{ type: "text", text: value }] } : value;
|
|
108
|
+
}
|
|
109
|
+
function selectedProtocolVersion(requested, supported, preferred) {
|
|
110
|
+
return supported.has(requested) ? requested : preferred;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Create a dependency-free MCP Streamable HTTP endpoint handler.
|
|
114
|
+
*
|
|
115
|
+
* The handler implements the server side of MCP over one HTTP endpoint:
|
|
116
|
+
* `initialize`, `ping`, `tools/list`, `tools/call`, `resources/list`,
|
|
117
|
+
* `resources/read`, `prompts/list`, and `prompts/get`. It accepts JSON-RPC
|
|
118
|
+
* requests over `POST`, acknowledges notifications with `202`, validates the
|
|
119
|
+
* `MCP-Protocol-Version` header, bounds request bodies, and returns JSON-RPC
|
|
120
|
+
* errors for malformed input.
|
|
121
|
+
*
|
|
122
|
+
* It intentionally does not spawn stdio servers, manage OAuth metadata, keep
|
|
123
|
+
* durable sessions, or open server-initiated SSE streams. Use DaloyJS
|
|
124
|
+
* middleware for authentication and authorization, and run this on a dedicated
|
|
125
|
+
* Daloy app when your MCP server has a different trust boundary than your REST
|
|
126
|
+
* API.
|
|
127
|
+
*
|
|
128
|
+
* @param options - Server identity, capabilities, limits, and response headers.
|
|
129
|
+
* @returns A Fetch-compatible request handler suitable for {@link mcpRoutes}
|
|
130
|
+
* or for direct use in any web-standard runtime.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```ts
|
|
134
|
+
* const mcp = createMcpHandler({
|
|
135
|
+
* serverInfo: { name: "inventory-mcp", version: "1.0.0" },
|
|
136
|
+
* tools: [
|
|
137
|
+
* {
|
|
138
|
+
* name: "inventory_lookup",
|
|
139
|
+
* description: "Look up inventory by SKU.",
|
|
140
|
+
* inputSchema: {
|
|
141
|
+
* type: "object",
|
|
142
|
+
* properties: { sku: { type: "string" } },
|
|
143
|
+
* required: ["sku"],
|
|
144
|
+
* additionalProperties: false,
|
|
145
|
+
* },
|
|
146
|
+
* handler: async ({ sku }) => `SKU ${sku} has 42 units.`,
|
|
147
|
+
* },
|
|
148
|
+
* ],
|
|
149
|
+
* });
|
|
150
|
+
* ```
|
|
151
|
+
*
|
|
152
|
+
* @since 1.0.0
|
|
153
|
+
*/
|
|
154
|
+
export function createMcpHandler(options) {
|
|
155
|
+
if (options.serverInfo.name.trim().length === 0) {
|
|
156
|
+
throw new TypeError("MCP serverInfo.name is required.");
|
|
157
|
+
}
|
|
158
|
+
if (options.serverInfo.version.trim().length === 0) {
|
|
159
|
+
throw new TypeError("MCP serverInfo.version is required.");
|
|
160
|
+
}
|
|
161
|
+
const protocolVersions = options.protocolVersions ?? MCP_PROTOCOL_VERSIONS;
|
|
162
|
+
if (protocolVersions.length === 0) {
|
|
163
|
+
throw new TypeError("MCP protocolVersions must contain at least one version.");
|
|
164
|
+
}
|
|
165
|
+
const preferred = options.preferredProtocolVersion ?? MCP_PROTOCOL_VERSION;
|
|
166
|
+
const supported = new Set(protocolVersions);
|
|
167
|
+
if (!supported.has(preferred)) {
|
|
168
|
+
throw new TypeError("MCP preferredProtocolVersion must be listed in protocolVersions.");
|
|
169
|
+
}
|
|
170
|
+
const maxBodyBytes = options.maxBodyBytes ?? MCP_DEFAULT_MAX_BODY_BYTES;
|
|
171
|
+
if (!Number.isSafeInteger(maxBodyBytes) || maxBodyBytes < 1) {
|
|
172
|
+
throw new TypeError("MCP maxBodyBytes must be a positive safe integer.");
|
|
173
|
+
}
|
|
174
|
+
const tools = options.tools ?? [];
|
|
175
|
+
const resources = options.resources ?? [];
|
|
176
|
+
const prompts = options.prompts ?? [];
|
|
177
|
+
const toolMap = new Map(tools.map((tool) => [tool.name, tool]));
|
|
178
|
+
const resourceMap = new Map(resources.map((resource) => [resource.uri, resource]));
|
|
179
|
+
const promptMap = new Map(prompts.map((prompt) => [prompt.name, prompt]));
|
|
180
|
+
if (toolMap.size !== tools.length)
|
|
181
|
+
throw new TypeError("MCP tool names must be unique.");
|
|
182
|
+
if (resourceMap.size !== resources.length)
|
|
183
|
+
throw new TypeError("MCP resource URIs must be unique.");
|
|
184
|
+
if (promptMap.size !== prompts.length)
|
|
185
|
+
throw new TypeError("MCP prompt names must be unique.");
|
|
186
|
+
const exposeInternalErrors = options.exposeInternalErrors ??
|
|
187
|
+
(typeof process === "object" && process.env?.NODE_ENV !== "production");
|
|
188
|
+
const headers = options.headers;
|
|
189
|
+
async function handleRpcRequest(message, request) {
|
|
190
|
+
const id = (message.id ?? null);
|
|
191
|
+
const method = message.method;
|
|
192
|
+
const params = asRecord(message.params);
|
|
193
|
+
const protocolVersion = selectedProtocolVersion(typeof params.protocolVersion === "string"
|
|
194
|
+
? params.protocolVersion
|
|
195
|
+
: (request.headers.get("mcp-protocol-version") ?? ""), supported, preferred);
|
|
196
|
+
const ctx = { request, protocolVersion, id, method };
|
|
197
|
+
switch (method) {
|
|
198
|
+
case "initialize":
|
|
199
|
+
return rpcResult(id, {
|
|
200
|
+
protocolVersion,
|
|
201
|
+
capabilities: {
|
|
202
|
+
...(tools.length > 0 ? { tools: {} } : {}),
|
|
203
|
+
...(resources.length > 0 ? { resources: {} } : {}),
|
|
204
|
+
...(prompts.length > 0 ? { prompts: {} } : {}),
|
|
205
|
+
},
|
|
206
|
+
serverInfo: options.serverInfo,
|
|
207
|
+
...(options.instructions ? { instructions: options.instructions } : {}),
|
|
208
|
+
}, headers);
|
|
209
|
+
case "ping":
|
|
210
|
+
return rpcResult(id, {}, headers);
|
|
211
|
+
case "tools/list":
|
|
212
|
+
return rpcResult(id, { tools: tools.map(publicTool) }, headers);
|
|
213
|
+
case "tools/call": {
|
|
214
|
+
const name = typeof params.name === "string" ? params.name : "";
|
|
215
|
+
const tool = toolMap.get(name);
|
|
216
|
+
if (!tool) {
|
|
217
|
+
return rpcError(id, INVALID_PARAMS, `Unknown tool: ${name || "<missing>"}`, undefined, 200, headers);
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
const result = await tool.handler(asRecord(params.arguments), ctx);
|
|
221
|
+
return rpcResult(id, normalizeToolResult(result), headers);
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
if (error instanceof McpToolError) {
|
|
225
|
+
return rpcResult(id, { content: [{ type: "text", text: error.message }], isError: true }, headers);
|
|
226
|
+
}
|
|
227
|
+
return rpcError(id, INTERNAL_ERROR, "Tool execution failed.", safeInternalErrorData(error, exposeInternalErrors), 200, headers);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
case "resources/list":
|
|
231
|
+
return rpcResult(id, { resources: resources.map(publicResource) }, headers);
|
|
232
|
+
case "resources/read": {
|
|
233
|
+
const uri = typeof params.uri === "string" ? params.uri : "";
|
|
234
|
+
const resource = resourceMap.get(uri);
|
|
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) {
|
|
243
|
+
const message = error instanceof McpToolError ? error.message : "Resource read failed.";
|
|
244
|
+
const data = error instanceof McpToolError
|
|
245
|
+
? undefined
|
|
246
|
+
: safeInternalErrorData(error, exposeInternalErrors);
|
|
247
|
+
return rpcError(id, error instanceof McpToolError ? INVALID_PARAMS : INTERNAL_ERROR, message, data, 200, headers);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
case "prompts/list":
|
|
251
|
+
return rpcResult(id, { prompts: prompts.map(publicPrompt) }, headers);
|
|
252
|
+
case "prompts/get": {
|
|
253
|
+
const name = typeof params.name === "string" ? params.name : "";
|
|
254
|
+
const prompt = promptMap.get(name);
|
|
255
|
+
if (!prompt) {
|
|
256
|
+
return rpcError(id, INVALID_PARAMS, `Unknown prompt: ${name || "<missing>"}`, undefined, 200, headers);
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
return rpcResult(id, await prompt.get(asRecord(params.arguments), ctx), headers);
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
const message = error instanceof McpToolError ? error.message : "Prompt rendering failed.";
|
|
263
|
+
const data = error instanceof McpToolError
|
|
264
|
+
? undefined
|
|
265
|
+
: safeInternalErrorData(error, exposeInternalErrors);
|
|
266
|
+
return rpcError(id, error instanceof McpToolError ? INVALID_PARAMS : INTERNAL_ERROR, message, data, 200, headers);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
default:
|
|
270
|
+
return rpcError(id, METHOD_NOT_FOUND, `Method not found: ${method}`, undefined, 200, headers);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return async function handleMcpRequest(request) {
|
|
274
|
+
if (request.method === "OPTIONS") {
|
|
275
|
+
return new Response(null, {
|
|
276
|
+
status: 204,
|
|
277
|
+
headers: { allow: "GET, POST, OPTIONS", ...(headers ?? {}) },
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
if (request.method === "GET") {
|
|
281
|
+
return jsonResponse({
|
|
282
|
+
transport: "streamable-http",
|
|
283
|
+
protocolVersions,
|
|
284
|
+
capabilities: {
|
|
285
|
+
tools: tools.map((tool) => tool.name),
|
|
286
|
+
resources: resources.map((resource) => resource.uri),
|
|
287
|
+
prompts: prompts.map((prompt) => prompt.name),
|
|
288
|
+
},
|
|
289
|
+
hint: "Send JSON-RPC 2.0 over HTTP POST to this endpoint.",
|
|
290
|
+
}, 405, { allow: "POST, OPTIONS", ...(headers ?? {}) });
|
|
291
|
+
}
|
|
292
|
+
if (request.method !== "POST") {
|
|
293
|
+
return jsonResponse({ error: "MCP Streamable HTTP endpoints accept POST requests." }, 405, {
|
|
294
|
+
allow: "POST, OPTIONS",
|
|
295
|
+
...(headers ?? {}),
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
const contentType = request.headers.get("content-type") ?? "";
|
|
299
|
+
if (!contentType.toLowerCase().includes("application/json")) {
|
|
300
|
+
return rpcError(null, INVALID_REQUEST, "MCP POST requests must use application/json.", undefined, 415, headers);
|
|
301
|
+
}
|
|
302
|
+
const protocolHeader = request.headers.get("mcp-protocol-version");
|
|
303
|
+
if (protocolHeader && !supported.has(protocolHeader)) {
|
|
304
|
+
return rpcError(null, INVALID_REQUEST, `Unsupported MCP-Protocol-Version: ${protocolHeader}`, { supported: protocolVersions }, 400, headers);
|
|
305
|
+
}
|
|
306
|
+
const declaredLength = Number(request.headers.get("content-length") ?? "");
|
|
307
|
+
if (Number.isFinite(declaredLength) && declaredLength > maxBodyBytes) {
|
|
308
|
+
return rpcError(null, INVALID_REQUEST, "Request body too large.", undefined, 413, headers);
|
|
309
|
+
}
|
|
310
|
+
const body = await request.arrayBuffer();
|
|
311
|
+
if (body.byteLength > maxBodyBytes) {
|
|
312
|
+
return rpcError(null, INVALID_REQUEST, "Request body too large.", undefined, 413, headers);
|
|
313
|
+
}
|
|
314
|
+
let raw;
|
|
315
|
+
try {
|
|
316
|
+
raw = new TextDecoder("utf-8", { fatal: true }).decode(body);
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return rpcError(null, PARSE_ERROR, "Request body must be valid UTF-8.", undefined, 400, headers);
|
|
320
|
+
}
|
|
321
|
+
let message;
|
|
322
|
+
try {
|
|
323
|
+
message = JSON.parse(raw);
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return rpcError(null, PARSE_ERROR, "Invalid JSON in request body.", undefined, 400, headers);
|
|
327
|
+
}
|
|
328
|
+
if (Array.isArray(message)) {
|
|
329
|
+
return rpcError(null, INVALID_REQUEST, "JSON-RPC batch requests are not supported.", undefined, 400, headers);
|
|
330
|
+
}
|
|
331
|
+
if (!message || typeof message !== "object" || message.jsonrpc !== "2.0") {
|
|
332
|
+
return rpcError(null, INVALID_REQUEST, "Request must be a JSON-RPC 2.0 message.", undefined, 400, headers);
|
|
333
|
+
}
|
|
334
|
+
if (message.id !== undefined && !isJsonRpcId(message.id)) {
|
|
335
|
+
return rpcError(null, INVALID_REQUEST, "JSON-RPC id must be a string, number, or null.", undefined, 400, headers);
|
|
336
|
+
}
|
|
337
|
+
if (message.method === undefined) {
|
|
338
|
+
if (!("result" in message) && !("error" in message)) {
|
|
339
|
+
return rpcError(null, INVALID_REQUEST, "JSON-RPC message is missing `method`, `result`, or `error`.", undefined, 400, headers);
|
|
340
|
+
}
|
|
341
|
+
return new Response(null, { status: 202, headers });
|
|
342
|
+
}
|
|
343
|
+
if (typeof message.method !== "string") {
|
|
344
|
+
return rpcError(null, INVALID_REQUEST, "JSON-RPC method must be a string.", undefined, 400, headers);
|
|
345
|
+
}
|
|
346
|
+
if (message.id === undefined) {
|
|
347
|
+
return new Response(null, { status: 202, headers });
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
return await handleRpcRequest(message, request);
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
return rpcError(message.id, INTERNAL_ERROR, "Internal server error.", safeInternalErrorData(error, exposeInternalErrors), 200, headers);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Build the Daloy route definitions for a Streamable HTTP MCP endpoint.
|
|
359
|
+
*
|
|
360
|
+
* Register each returned route on the Daloy app that should host MCP. A
|
|
361
|
+
* separate app is often the cleanest production shape: the REST API can keep
|
|
362
|
+
* its public contract and auth policy, while the MCP server can use its own
|
|
363
|
+
* bearer token, rate limit, network allowlist, and tool set.
|
|
364
|
+
*
|
|
365
|
+
* @param path - Public MCP endpoint path, usually `"/mcp"`.
|
|
366
|
+
* @param handler - Handler returned by {@link createMcpHandler}.
|
|
367
|
+
* @returns Route definitions for `POST`, `GET`, and `OPTIONS` on the same
|
|
368
|
+
* path. `POST` is the actual MCP transport; `GET` gives a human-readable
|
|
369
|
+
* 405 hint because this helper does not open server-initiated SSE streams;
|
|
370
|
+
* `OPTIONS` supports preflight when CORS middleware is installed.
|
|
371
|
+
*
|
|
372
|
+
* @example
|
|
373
|
+
* ```ts
|
|
374
|
+
* const app = new App();
|
|
375
|
+
* const mcp = createMcpHandler({ serverInfo, tools });
|
|
376
|
+
*
|
|
377
|
+
* for (const route of mcpRoutes("/mcp", mcp)) {
|
|
378
|
+
* app.route(route);
|
|
379
|
+
* }
|
|
380
|
+
* ```
|
|
381
|
+
*
|
|
382
|
+
* @since 1.0.0
|
|
383
|
+
*/
|
|
384
|
+
export function mcpRoutes(path, handler) {
|
|
385
|
+
const responses = {
|
|
386
|
+
200: { description: "MCP JSON-RPC response", body: MCP_JSON_RESPONSE_SCHEMA },
|
|
387
|
+
202: { description: "MCP notification accepted", body: MCP_JSON_RESPONSE_SCHEMA },
|
|
388
|
+
204: { description: "CORS preflight accepted" },
|
|
389
|
+
400: { description: "Invalid MCP request" },
|
|
390
|
+
405: { description: "Unsupported MCP transport method" },
|
|
391
|
+
413: { description: "MCP request body too large" },
|
|
392
|
+
};
|
|
393
|
+
return [
|
|
394
|
+
{
|
|
395
|
+
method: "POST",
|
|
396
|
+
path,
|
|
397
|
+
operationId: "mcpPost",
|
|
398
|
+
summary: "MCP Streamable HTTP endpoint",
|
|
399
|
+
responses,
|
|
400
|
+
handler: ({ request }) => handler(request),
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
method: "GET",
|
|
404
|
+
path,
|
|
405
|
+
operationId: "mcpGet",
|
|
406
|
+
summary: "MCP Streamable HTTP discovery hint",
|
|
407
|
+
responses,
|
|
408
|
+
handler: ({ request }) => handler(request),
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
method: "OPTIONS",
|
|
412
|
+
path,
|
|
413
|
+
operationId: "mcpOptions",
|
|
414
|
+
summary: "MCP Streamable HTTP preflight",
|
|
415
|
+
responses,
|
|
416
|
+
handler: ({ request }) => handler(request),
|
|
417
|
+
},
|
|
418
|
+
];
|
|
419
|
+
}
|
package/dist/sbom.cdx.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bomFormat": "CycloneDX",
|
|
3
3
|
"specVersion": "1.5",
|
|
4
|
-
"serialNumber": "urn:uuid:
|
|
4
|
+
"serialNumber": "urn:uuid:a88e3a69-b278-5771-a67e-40fb774e004e",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
|
-
"timestamp": "2026-07-
|
|
7
|
+
"timestamp": "2026-07-01T22:47:12.881Z",
|
|
8
8
|
"tools": [
|
|
9
9
|
{
|
|
10
10
|
"vendor": "DaloyJS",
|
|
11
11
|
"name": "daloy-generate-sbom",
|
|
12
|
-
"version": "1.0.0-beta.
|
|
12
|
+
"version": "1.0.0-beta.6"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"authors": [
|
|
@@ -19,11 +19,11 @@
|
|
|
19
19
|
],
|
|
20
20
|
"component": {
|
|
21
21
|
"type": "library",
|
|
22
|
-
"bom-ref": "pkg:npm/@daloyjs/core@1.0.0-beta.
|
|
22
|
+
"bom-ref": "pkg:npm/@daloyjs/core@1.0.0-beta.6",
|
|
23
23
|
"name": "@daloyjs/core",
|
|
24
|
-
"version": "1.0.0-beta.
|
|
24
|
+
"version": "1.0.0-beta.6",
|
|
25
25
|
"description": "DaloyJS is a runtime-portable, contract-first TypeScript web framework with built-in OpenAPI (Hey API), typed client generation, large-scale maintainability, and security-first defaults. Hono-grade portability, Elysia-grade DX, FastAPI-grade docs, Fastify-grade ops — distributed via pnpm.",
|
|
26
|
-
"purl": "pkg:npm/@daloyjs/core@1.0.0-beta.
|
|
26
|
+
"purl": "pkg:npm/@daloyjs/core@1.0.0-beta.6",
|
|
27
27
|
"licenses": [
|
|
28
28
|
{
|
|
29
29
|
"license": {
|
|
@@ -46,9 +46,9 @@
|
|
|
46
46
|
}
|
|
47
47
|
],
|
|
48
48
|
"swid": {
|
|
49
|
-
"tagId": "swidtag--daloyjs-core-1.0.0-beta.
|
|
49
|
+
"tagId": "swidtag--daloyjs-core-1.0.0-beta.6",
|
|
50
50
|
"name": "@daloyjs/core",
|
|
51
|
-
"version": "1.0.0-beta.
|
|
51
|
+
"version": "1.0.0-beta.6",
|
|
52
52
|
"tagVersion": 0,
|
|
53
53
|
"patch": false
|
|
54
54
|
}
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"components": [],
|
|
58
58
|
"dependencies": [
|
|
59
59
|
{
|
|
60
|
-
"ref": "pkg:npm/@daloyjs/core@1.0.0-beta.
|
|
60
|
+
"ref": "pkg:npm/@daloyjs/core@1.0.0-beta.6",
|
|
61
61
|
"dependsOn": []
|
|
62
62
|
}
|
|
63
63
|
]
|
package/dist/sbom.spdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"spdxVersion": "SPDX-2.3",
|
|
3
3
|
"dataLicense": "CC0-1.0",
|
|
4
4
|
"SPDXID": "SPDXRef-DOCUMENT",
|
|
5
|
-
"name": "@daloyjs/core-1.0.0-beta.
|
|
6
|
-
"documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-1.0.0-beta.
|
|
5
|
+
"name": "@daloyjs/core-1.0.0-beta.6",
|
|
6
|
+
"documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-1.0.0-beta.6-a88e3a69-b278-5771-a67e-40fb774e004e",
|
|
7
7
|
"creationInfo": {
|
|
8
|
-
"created": "2026-07-
|
|
8
|
+
"created": "2026-07-01T22:47:12.881Z",
|
|
9
9
|
"creators": [
|
|
10
10
|
"Tool: daloy-generate-sbom",
|
|
11
11
|
"Organization: DaloyJS"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
{
|
|
17
17
|
"SPDXID": "SPDXRef-Package--daloyjs-core",
|
|
18
18
|
"name": "@daloyjs/core",
|
|
19
|
-
"versionInfo": "1.0.0-beta.
|
|
19
|
+
"versionInfo": "1.0.0-beta.6",
|
|
20
20
|
"downloadLocation": "https://github.com/daloyjs/daloy",
|
|
21
21
|
"filesAnalyzed": false,
|
|
22
22
|
"licenseConcluded": "MIT",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
{
|
|
28
28
|
"referenceCategory": "PACKAGE-MANAGER",
|
|
29
29
|
"referenceType": "purl",
|
|
30
|
-
"referenceLocator": "pkg:npm/@daloyjs/core@1.0.0-beta.
|
|
30
|
+
"referenceLocator": "pkg:npm/@daloyjs/core@1.0.0-beta.6"
|
|
31
31
|
}
|
|
32
32
|
]
|
|
33
33
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@daloyjs/core",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.6",
|
|
4
4
|
"description": "DaloyJS is a runtime-portable, contract-first TypeScript web framework with built-in OpenAPI (Hey API), typed client generation, large-scale maintainability, and security-first defaults. Hono-grade portability, Elysia-grade DX, FastAPI-grade docs, Fastify-grade ops — distributed via pnpm.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"framework",
|
|
21
21
|
"http",
|
|
22
22
|
"openapi",
|
|
23
|
+
"mcp",
|
|
23
24
|
"hey-api",
|
|
24
25
|
"contract-first",
|
|
25
26
|
"typescript",
|
|
@@ -85,6 +86,10 @@
|
|
|
85
86
|
"types": "./dist/openapi-diff.d.ts",
|
|
86
87
|
"import": "./dist/openapi-diff.js"
|
|
87
88
|
},
|
|
89
|
+
"./mcp": {
|
|
90
|
+
"types": "./dist/mcp.d.ts",
|
|
91
|
+
"import": "./dist/mcp.js"
|
|
92
|
+
},
|
|
88
93
|
"./asyncapi": {
|
|
89
94
|
"types": "./dist/asyncapi.d.ts",
|
|
90
95
|
"import": "./dist/asyncapi.js"
|