From 83061593b172b00a5cf6d2c7b9cac7afd9e256e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Fri, 12 Jun 2026 17:14:03 +0200 Subject: [PATCH] experiment: native MCP server API (Deno.McpServer) Adds an unstable Deno.McpServer API behind --unstable-mcp that makes it easy to write Model Context Protocol servers in Deno without any dependencies: const server = new Deno.McpServer({ name: "demo", version: "1.0.0" }); server.tool("add", { inputSchema }, ({ a, b }) => a + b); server.resource("file:///greeting.txt", () => "hello"); server.prompt("review", ({ code }) => "Review this: " + code); await server.serve(); // stdio transport // or: Deno.serve(server.fetch) // streamable HTTP transport Implemented as a new lazy loaded JS-only extension (ext/mcp) that handles the MCP JSON-RPC lifecycle (initialize, ping, tools, resources, prompts), reports tool errors in-band per spec, and supports both the stdio transport (newline delimited JSON-RPC) and a stateless streamable HTTP transport. Verified against the official MCP Inspector client (initialize handshake, tools/list, tools/call) and with raw JSON-RPC over both transports. --- .github/workflows/ci.generated.yml | 6 +- Cargo.lock | 8 + Cargo.toml | 2 + cli/tsc/dts/lib.deno.unstable.d.ts | 175 +++++++++ ext/mcp/01_mcp.ts | 576 +++++++++++++++++++++++++++++ ext/mcp/Cargo.toml | 17 + ext/mcp/README.md | 12 + ext/mcp/clippy.toml | 35 ++ ext/mcp/lib.rs | 5 + runtime/Cargo.toml | 1 + runtime/features/data.rs | 7 + runtime/features/gen.js | 23 +- runtime/features/gen.rs | 38 +- runtime/js/90_deno_ns.js | 11 + runtime/lib.rs | 1 + runtime/snapshot.rs | 1 + runtime/snapshot_info.rs | 1 + runtime/web_worker.rs | 1 + runtime/worker.rs | 2 + 19 files changed, 893 insertions(+), 29 deletions(-) create mode 100644 ext/mcp/01_mcp.ts create mode 100644 ext/mcp/Cargo.toml create mode 100644 ext/mcp/README.md create mode 100644 ext/mcp/clippy.toml create mode 100644 ext/mcp/lib.rs diff --git a/.github/workflows/ci.generated.yml b/.github/workflows/ci.generated.yml index 3412e61c386299..5047a59656a53c 100644 --- a/.github/workflows/ci.generated.yml +++ b/.github/workflows/ci.generated.yml @@ -1614,7 +1614,7 @@ jobs: if: '!startsWith(github.ref, ''refs/tags/'')' env: CARGO_PROFILE_DEV_DEBUG: 0 - run: cargo test --locked --lib -p deno -p denort -p node_shim -p bsdiff_helper -p deno_lib -p deno_snapshots -p deno_bundle_runtime -p deno_cache -p deno_canvas -p deno_cron -p deno_crypto -p deno_fetch -p deno_ffi -p deno_fs -p deno_http -p deno_image -p deno_io -p deno_kv -p deno_napi -p napi_sym -p deno_net -p deno_node -p deno_node_crypto -p deno_node_sqlite -p denort_helper -p deno_signals -p deno_telemetry -p deno_url -p deno_web -p deno_webgpu -p deno_webidl -p deno_websocket -p deno_webstorage -p deno_cache_dir -p deno_config -p deno_crypto_provider -p deno_dotenv -p eszip -p deno_http_h1 -p deno_inspector_server -p deno_lockfile -p deno_maybe_sync -p napi_sys -p node_resolver -p deno_npm -p deno_npm_cache -p deno_npm_installer -p deno_npmrc -p deno_package_json -p deno_resolver -p deno_typescript_go_client_rust -p deno_runtime -p deno_features -p deno_permissions -p deno_subprocess_windows + run: cargo test --locked --lib -p deno -p denort -p node_shim -p bsdiff_helper -p deno_lib -p deno_snapshots -p deno_bundle_runtime -p deno_cache -p deno_canvas -p deno_cron -p deno_crypto -p deno_fetch -p deno_ffi -p deno_fs -p deno_http -p deno_image -p deno_io -p deno_kv -p deno_mcp -p deno_napi -p napi_sym -p deno_net -p deno_node -p deno_node_crypto -p deno_node_sqlite -p denort_helper -p deno_signals -p deno_telemetry -p deno_url -p deno_web -p deno_webgpu -p deno_webidl -p deno_websocket -p deno_webstorage -p deno_cache_dir -p deno_config -p deno_crypto_provider -p deno_dotenv -p eszip -p deno_http_h1 -p deno_inspector_server -p deno_lockfile -p deno_maybe_sync -p napi_sys -p node_resolver -p deno_npm -p deno_npm_cache -p deno_npm_installer -p deno_npmrc -p deno_package_json -p deno_resolver -p deno_typescript_go_client_rust -p deno_runtime -p deno_features -p deno_permissions -p deno_subprocess_windows - name: Cache cargo home uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 if: '!startsWith(github.ref, ''refs/tags/'') && github.ref == ''refs/heads/main''' @@ -2572,7 +2572,7 @@ jobs: if: '!startsWith(github.ref, ''refs/tags/'')' env: CARGO_PROFILE_DEV_DEBUG: 0 - run: cargo test --locked --lib -p deno -p denort -p node_shim -p bsdiff_helper -p deno_lib -p deno_snapshots -p deno_bundle_runtime -p deno_cache -p deno_canvas -p deno_cron -p deno_crypto -p deno_fetch -p deno_ffi -p deno_fs -p deno_http -p deno_image -p deno_io -p deno_kv -p deno_napi -p napi_sym -p deno_net -p deno_node -p deno_node_crypto -p deno_node_sqlite -p denort_helper -p deno_signals -p deno_telemetry -p deno_url -p deno_web -p deno_webgpu -p deno_webidl -p deno_websocket -p deno_webstorage -p deno_cache_dir -p deno_config -p deno_crypto_provider -p deno_dotenv -p eszip -p deno_http_h1 -p deno_inspector_server -p deno_lockfile -p deno_maybe_sync -p napi_sys -p node_resolver -p deno_npm -p deno_npm_cache -p deno_npm_installer -p deno_npmrc -p deno_package_json -p deno_resolver -p deno_typescript_go_client_rust -p deno_runtime -p deno_features -p deno_permissions -p deno_subprocess_windows + run: cargo test --locked --lib -p deno -p denort -p node_shim -p bsdiff_helper -p deno_lib -p deno_snapshots -p deno_bundle_runtime -p deno_cache -p deno_canvas -p deno_cron -p deno_crypto -p deno_fetch -p deno_ffi -p deno_fs -p deno_http -p deno_image -p deno_io -p deno_kv -p deno_mcp -p deno_napi -p napi_sym -p deno_net -p deno_node -p deno_node_crypto -p deno_node_sqlite -p denort_helper -p deno_signals -p deno_telemetry -p deno_url -p deno_web -p deno_webgpu -p deno_webidl -p deno_websocket -p deno_webstorage -p deno_cache_dir -p deno_config -p deno_crypto_provider -p deno_dotenv -p eszip -p deno_http_h1 -p deno_inspector_server -p deno_lockfile -p deno_maybe_sync -p napi_sys -p node_resolver -p deno_npm -p deno_npm_cache -p deno_npm_installer -p deno_npmrc -p deno_package_json -p deno_resolver -p deno_typescript_go_client_rust -p deno_runtime -p deno_features -p deno_permissions -p deno_subprocess_windows - name: Cache cargo home uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 if: '!startsWith(github.ref, ''refs/tags/'') && github.ref == ''refs/heads/main''' @@ -5807,7 +5807,7 @@ jobs: if: '!startsWith(github.ref, ''refs/tags/'')' env: CARGO_PROFILE_DEV_DEBUG: 0 - run: cargo test --locked --lib -p deno -p denort -p node_shim -p bsdiff_helper -p deno_lib -p deno_snapshots -p deno_bundle_runtime -p deno_cache -p deno_canvas -p deno_cron -p deno_crypto -p deno_fetch -p deno_ffi -p deno_fs -p deno_http -p deno_image -p deno_io -p deno_kv -p deno_napi -p napi_sym -p deno_net -p deno_node -p deno_node_crypto -p deno_node_sqlite -p denort_helper -p deno_signals -p deno_telemetry -p deno_url -p deno_web -p deno_webgpu -p deno_webidl -p deno_websocket -p deno_webstorage -p deno_cache_dir -p deno_config -p deno_crypto_provider -p deno_dotenv -p eszip -p deno_http_h1 -p deno_inspector_server -p deno_lockfile -p deno_maybe_sync -p napi_sys -p node_resolver -p deno_npm -p deno_npm_cache -p deno_npm_installer -p deno_npmrc -p deno_package_json -p deno_resolver -p deno_typescript_go_client_rust -p deno_runtime -p deno_features -p deno_permissions -p deno_subprocess_windows + run: cargo test --locked --lib -p deno -p denort -p node_shim -p bsdiff_helper -p deno_lib -p deno_snapshots -p deno_bundle_runtime -p deno_cache -p deno_canvas -p deno_cron -p deno_crypto -p deno_fetch -p deno_ffi -p deno_fs -p deno_http -p deno_image -p deno_io -p deno_kv -p deno_mcp -p deno_napi -p napi_sym -p deno_net -p deno_node -p deno_node_crypto -p deno_node_sqlite -p denort_helper -p deno_signals -p deno_telemetry -p deno_url -p deno_web -p deno_webgpu -p deno_webidl -p deno_websocket -p deno_webstorage -p deno_cache_dir -p deno_config -p deno_crypto_provider -p deno_dotenv -p eszip -p deno_http_h1 -p deno_inspector_server -p deno_lockfile -p deno_maybe_sync -p napi_sys -p node_resolver -p deno_npm -p deno_npm_cache -p deno_npm_installer -p deno_npmrc -p deno_package_json -p deno_resolver -p deno_typescript_go_client_rust -p deno_runtime -p deno_features -p deno_permissions -p deno_subprocess_windows - name: Cache cargo home uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 if: '!startsWith(github.ref, ''refs/tags/'') && github.ref == ''refs/heads/main''' diff --git a/Cargo.lock b/Cargo.lock index a9eb84e0e862b9..7cd49cac3a748e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2653,6 +2653,13 @@ dependencies = [ "dashmap", ] +[[package]] +name = "deno_mcp" +version = "0.1.0" +dependencies = [ + "deno_core", +] + [[package]] name = "deno_media_type" version = "0.4.0" @@ -3177,6 +3184,7 @@ dependencies = [ "deno_inspector_server", "deno_io", "deno_kv", + "deno_mcp", "deno_napi", "deno_net", "deno_node", diff --git a/Cargo.toml b/Cargo.toml index f293925e6767b4..f039623fc41665 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ members = [ "ext/image", "ext/io", "ext/kv", + "ext/mcp", "ext/napi", "ext/napi/sym", "ext/net", @@ -123,6 +124,7 @@ deno_http = { version = "0.249.0", path = "./ext/http" } deno_image = { version = "0.27.0", path = "./ext/image" } deno_io = { version = "0.161.0", path = "./ext/io" } deno_kv = { version = "0.159.0", path = "./ext/kv" } +deno_mcp = { version = "0.1.0", path = "./ext/mcp" } deno_napi = { version = "0.182.0", path = "./ext/napi" } deno_net = { version = "0.243.0", path = "./ext/net" } deno_node = { version = "0.189.0", path = "./ext/node" } diff --git a/cli/tsc/dts/lib.deno.unstable.d.ts b/cli/tsc/dts/lib.deno.unstable.d.ts index d283022cd216db..331cb7714a8ff9 100644 --- a/cli/tsc/dts/lib.deno.unstable.d.ts +++ b/cli/tsc/dts/lib.deno.unstable.d.ts @@ -503,6 +503,181 @@ declare namespace Deno { handler: () => Promise | void, ): Promise; + /** **UNSTABLE**: New API, yet to be vetted. + * + * Options for creating a {@linkcode Deno.McpServer}. + * + * @category MCP + * @experimental + */ + export interface McpServerOptions { + /** The server name reported to clients during the initialize + * handshake. */ + name: string; + /** The server version reported to clients during the initialize + * handshake. */ + version: string; + /** Optional usage instructions surfaced to the client (and typically to + * the model) after initialization. */ + instructions?: string; + } + + /** **UNSTABLE**: New API, yet to be vetted. + * + * Definition of an MCP tool, as reported by `tools/list`. + * + * @category MCP + * @experimental + */ + export interface McpToolDefinition { + /** Optional human-readable title of the tool. */ + title?: string; + /** Description of what the tool does. Used by models to decide when to + * call the tool. */ + description?: string; + /** JSON schema describing the tool input. Defaults to + * `{ "type": "object" }`. */ + inputSchema?: Record; + /** JSON schema describing the structured output of the tool. */ + outputSchema?: Record; + /** Optional tool annotations (e.g. `readOnlyHint`). */ + annotations?: Record; + } + + /** **UNSTABLE**: New API, yet to be vetted. + * + * Metadata of an MCP resource, as reported by `resources/list`. + * + * @category MCP + * @experimental + */ + export interface McpResourceMetadata { + /** Resource name. Defaults to the resource URI. */ + name?: string; + /** Optional human-readable title of the resource. */ + title?: string; + /** Description of the resource. */ + description?: string; + /** MIME type of the resource contents. */ + mimeType?: string; + } + + /** **UNSTABLE**: New API, yet to be vetted. + * + * Definition of an MCP prompt, as reported by `prompts/list`. + * + * @category MCP + * @experimental + */ + export interface McpPromptDefinition { + /** Optional human-readable title of the prompt. */ + title?: string; + /** Description of the prompt. */ + description?: string; + /** Arguments accepted by the prompt. */ + arguments?: { + name: string; + description?: string; + required?: boolean; + }[]; + } + + /** **UNSTABLE**: New API, yet to be vetted. + * + * A Model Context Protocol (MCP) server. Register tools, resources and + * prompts, then serve them over the stdio transport with + * {@linkcode Deno.McpServer.serve}, or over the streamable HTTP transport + * by passing {@linkcode Deno.McpServer.fetch} to {@linkcode Deno.serve}. + * + * ```ts + * const server = new Deno.McpServer({ + * name: "calculator", + * version: "1.0.0", + * }); + * + * server.tool("add", { + * description: "Add two numbers", + * inputSchema: { + * type: "object", + * properties: { a: { type: "number" }, b: { type: "number" } }, + * required: ["a", "b"], + * }, + * }, ({ a, b }: { a: number; b: number }) => `${a + b}`); + * + * await server.serve(); + * ``` + * + * @category MCP + * @experimental + */ + export class McpServer { + constructor(options: McpServerOptions); + + /** Register a tool. The handler receives the tool call arguments and may + * return a string, a JSON-serializable value (reported as structured + * content), or a full MCP `CallToolResult` object. Errors thrown by the + * handler are reported to the client as tool execution errors. */ + tool( + name: string, + definition: McpToolDefinition, + // deno-lint-ignore no-explicit-any + handler: (args: any) => unknown | Promise, + ): this; + /** Register a tool without a definition. */ + // deno-lint-ignore no-explicit-any + tool( + name: string, + handler: (args: any) => unknown | Promise, + ): this; + + /** Register a resource. The handler may return a string, a + * `Uint8Array` (reported as a base64 blob), or a full MCP + * `ReadResourceResult` object. */ + resource( + uri: string, + metadata: McpResourceMetadata, + handler: (uri: string) => unknown | Promise, + ): this; + /** Register a resource without metadata. */ + resource( + uri: string, + handler: (uri: string) => unknown | Promise, + ): this; + + /** Register a prompt. The handler receives the prompt arguments and may + * return a string (used as a single user message), an array of messages, + * or a full MCP `GetPromptResult` object. */ + prompt( + name: string, + definition: McpPromptDefinition, + // deno-lint-ignore no-explicit-any + handler: (args: any) => unknown | Promise, + ): this; + /** Register a prompt without a definition. */ + // deno-lint-ignore no-explicit-any + prompt( + name: string, + handler: (args: any) => unknown | Promise, + ): this; + + /** Serve the MCP server over the stdio transport (newline-delimited + * JSON-RPC messages on stdin/stdout). Resolves when stdin is closed. + * + * Note: when using the stdio transport nothing else may write to stdout; + * use `console.error` for logging. */ + serve(options?: { transport?: "stdio" }): Promise; + + /** Handler for the streamable HTTP transport. Pass it to + * {@linkcode Deno.serve}: + * + * ```ts + * const server = new Deno.McpServer({ name: "demo", version: "1.0.0" }); + * Deno.serve(server.fetch); + * ``` + */ + fetch: (request: Request) => Promise; + } + /** **UNSTABLE**: New API, yet to be vetted. * * A key to be persisted in a {@linkcode Deno.Kv}. A key is a sequence diff --git a/ext/mcp/01_mcp.ts b/ext/mcp/01_mcp.ts new file mode 100644 index 00000000000000..d180d99738069d --- /dev/null +++ b/ext/mcp/01_mcp.ts @@ -0,0 +1,576 @@ +// Copyright 2018-2026 the Deno authors. MIT license. + +// Implementation of the unstable `Deno.McpServer` API: a minimal Model +// Context Protocol server (JSON-RPC 2.0 lifecycle, tools, resources and +// prompts) with two transports: +// +// - stdio: newline-delimited JSON-RPC messages over stdin/stdout +// - streamable HTTP: a `fetch`-style handler usable with `Deno.serve` + +(function () { +const { core, primordials } = __bootstrap; +const { op_base64_encode } = core.ops; +const { + ArrayIsArray, + ArrayPrototypePush, + JSONParse, + JSONStringify, + Error, + SafeMap, + SafeMapIterator, + Symbol, + TypeError, + TypedArrayPrototypeGetLength, + TypedArrayPrototypeIndexOf, + TypedArrayPrototypeSet, + TypedArrayPrototypeSubarray, + Uint8Array, + Uint8ArrayPrototype, + ObjectPrototypeIsPrototypeOf, +} = primordials; + +const { TextDecoder, TextEncoder } = core.loadExtScript( + "ext:deno_web/08_text_encoding.js", +); +const io = core.loadExtScript("ext:deno_io/12_io.js"); + +let _responseImpl; +function lazyResponse() { + return _responseImpl ?? + (_responseImpl = core.loadExtScript("ext:deno_fetch/23_response.js")); +} + +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +// Most recent protocol revision this implementation targets, plus older +// revisions that are wire-compatible with the subset implemented here. +const LATEST_PROTOCOL_VERSION = "2025-06-18"; +const SUPPORTED_PROTOCOL_VERSIONS = [ + "2025-11-25", + "2025-06-18", + "2025-03-26", + "2024-11-05", +]; + +// JSON-RPC 2.0 error codes +const PARSE_ERROR = -32700; +const INVALID_REQUEST = -32600; +const METHOD_NOT_FOUND = -32601; +const INVALID_PARAMS = -32602; +const INTERNAL_ERROR = -32603; +// MCP-specific: resource not found +const RESOURCE_NOT_FOUND = -32002; + +const NEWLINE = 10; + +function concatBytes(a: Uint8Array, b: Uint8Array): Uint8Array { + const aLen = TypedArrayPrototypeGetLength(a); + const bLen = TypedArrayPrototypeGetLength(b); + const out = new Uint8Array(aLen + bLen); + TypedArrayPrototypeSet(out, a, 0); + TypedArrayPrototypeSet(out, b, aLen); + return out; +} + +function rpcResult(id: unknown, result: unknown) { + return { jsonrpc: "2.0", id, result }; +} + +function rpcError(id: unknown, code: number, message: string) { + return { jsonrpc: "2.0", id, error: { code, message } }; +} + +function isPlainText(value: unknown): value is string { + return typeof value === "string"; +} + +// Normalizes a tool handler return value into a CallToolResult. +function normalizeToolResult(value: unknown) { + if (value === undefined || value === null) { + return { content: [] }; + } + if (isPlainText(value)) { + return { content: [{ type: "text", text: value }] }; + } + if ( + typeof value === "object" && + ArrayIsArray((value as { content: unknown }).content) + ) { + // Already a CallToolResult shaped object. + return value; + } + // Structured value: serialize it for the text fallback and pass it + // through as structuredContent. + const text = JSONStringify(value); + if (typeof value === "object" && !ArrayIsArray(value)) { + return { content: [{ type: "text", text }], structuredContent: value }; + } + return { content: [{ type: "text", text }] }; +} + +// Normalizes a resource handler return value into resource contents. +function normalizeResourceContents( + uri: string, + mimeType: string | undefined, + value: unknown, +) { + if (typeof value === "object" && value !== null) { + const contents = (value as { contents: unknown }).contents; + if (ArrayIsArray(contents)) { + // Already a ReadResourceResult shaped object. + return value; + } + if (ObjectPrototypeIsPrototypeOf(Uint8ArrayPrototype, value)) { + return { + contents: [{ + uri, + mimeType: mimeType ?? "application/octet-stream", + blob: op_base64_encode(value), + }], + }; + } + } + return { + contents: [{ + uri, + mimeType: mimeType ?? "text/plain", + text: isPlainText(value) ? value : JSONStringify(value), + }], + }; +} + +// Normalizes a prompt handler return value into a GetPromptResult. +function normalizePromptResult( + description: string | undefined, + value: unknown, +) { + let messages; + if (isPlainText(value)) { + messages = [{ role: "user", content: { type: "text", text: value } }]; + } else if (ArrayIsArray(value)) { + messages = []; + for (let i = 0; i < value.length; i++) { + const entry = value[i]; + if (isPlainText(entry)) { + ArrayPrototypePush(messages, { + role: "user", + content: { type: "text", text: entry }, + }); + } else { + ArrayPrototypePush(messages, entry); + } + } + } else if ( + typeof value === "object" && value !== null && + ArrayIsArray((value as { messages: unknown }).messages) + ) { + // Already a GetPromptResult shaped object. + return value; + } else { + throw new TypeError( + "Prompt handler must return a string, an array of messages, or a result object", + ); + } + const result: { messages: unknown[]; description?: string } = { messages }; + if (description !== undefined) { + result.description = description; + } + return result; +} + +class McpServer { + #name: string; + #version: string; + #instructions: string | undefined; + #tools = new SafeMap(); + #resources = new SafeMap(); + #prompts = new SafeMap(); + + fetch; + + constructor(options: { + name: string; + version: string; + instructions?: string; + }) { + if (typeof options !== "object" || options === null) { + throw new TypeError("McpServer requires an options object"); + } + if (typeof options.name !== "string") { + throw new TypeError("McpServer requires a string 'name' option"); + } + if (typeof options.version !== "string") { + throw new TypeError("McpServer requires a string 'version' option"); + } + this.#name = options.name; + this.#version = options.version; + this.#instructions = options.instructions; + this.fetch = (request: Request) => this.#handleHttpRequest(request); + } + + tool(name: string, definition, handler?) { + if (typeof definition === "function") { + handler = definition; + definition = { __proto__: null }; + } + if (typeof name !== "string") { + throw new TypeError("Tool name must be a string"); + } + if (typeof handler !== "function") { + throw new TypeError(`Tool "${name}" requires a handler function`); + } + this.#tools.set(name, { definition, handler }); + return this; + } + + resource(uri: string, metadata, handler?) { + if (typeof metadata === "function") { + handler = metadata; + metadata = { __proto__: null }; + } + if (typeof uri !== "string") { + throw new TypeError("Resource URI must be a string"); + } + if (typeof handler !== "function") { + throw new TypeError(`Resource "${uri}" requires a handler function`); + } + this.#resources.set(uri, { metadata, handler }); + return this; + } + + prompt(name: string, definition, handler?) { + if (typeof definition === "function") { + handler = definition; + definition = { __proto__: null }; + } + if (typeof name !== "string") { + throw new TypeError("Prompt name must be a string"); + } + if (typeof handler !== "function") { + throw new TypeError(`Prompt "${name}" requires a handler function`); + } + this.#prompts.set(name, { definition, handler }); + return this; + } + + // Handles a single decoded JSON-RPC message. Returns a response object, + // or null if the message was a notification (or a client response). + async #handleMessage(message): Promise { + if ( + typeof message !== "object" || message === null || + message.jsonrpc !== "2.0" + ) { + return rpcError(null, INVALID_REQUEST, "Invalid request"); + } + if (typeof message.method !== "string") { + // A response to a server-initiated request; this server never sends + // requests, so there is nothing to correlate it with. + return null; + } + const isNotification = message.id === undefined; + if (isNotification) { + // notifications/initialized, notifications/cancelled, etc. None of + // them require action in this implementation. + return null; + } + const { id, method, params } = message; + try { + const result = await this.#dispatch(method, params ?? {}); + if (result === METHOD_NOT_FOUND_SENTINEL) { + return rpcError(id, METHOD_NOT_FOUND, `Method not found: ${method}`); + } + return rpcResult(id, result); + } catch (error) { + if (ObjectPrototypeIsPrototypeOf(McpErrorPrototype, error)) { + return rpcError(id, error.code, error.message); + } + return rpcError( + id, + INTERNAL_ERROR, + `Internal error: ${error?.message ?? error}`, + ); + } + } + + async #dispatch(method: string, params): Promise { + switch (method) { + case "initialize": + return this.#initialize(params); + case "ping": + return {}; + case "tools/list": + return this.#listTools(); + case "tools/call": + return await this.#callTool(params); + case "resources/list": + return this.#listResources(); + case "resources/templates/list": + return { resourceTemplates: [] }; + case "resources/read": + return await this.#readResource(params); + case "prompts/list": + return this.#listPrompts(); + case "prompts/get": + return await this.#getPrompt(params); + default: + return METHOD_NOT_FOUND_SENTINEL; + } + } + + #initialize(params) { + let protocolVersion = LATEST_PROTOCOL_VERSION; + const requested = params?.protocolVersion; + for (let i = 0; i < SUPPORTED_PROTOCOL_VERSIONS.length; i++) { + if (SUPPORTED_PROTOCOL_VERSIONS[i] === requested) { + protocolVersion = requested; + break; + } + } + const result = { + protocolVersion, + capabilities: { + tools: { listChanged: false }, + resources: { subscribe: false, listChanged: false }, + prompts: { listChanged: false }, + }, + serverInfo: { name: this.#name, version: this.#version }, + }; + if (this.#instructions !== undefined) { + result.instructions = this.#instructions; + } + return result; + } + + #listTools() { + const tools = []; + for (const { 0: name, 1: entry } of new SafeMapIterator(this.#tools)) { + const definition = entry.definition; + const tool = { + name, + inputSchema: definition.inputSchema ?? { type: "object" }, + }; + if (definition.title !== undefined) tool.title = definition.title; + // deno-lint-ignore prefer-primordials + if (definition?.description !== undefined) { + // deno-lint-ignore prefer-primordials + tool.description = definition?.description; + } + if (definition.outputSchema !== undefined) { + tool.outputSchema = definition.outputSchema; + } + if (definition.annotations !== undefined) { + tool.annotations = definition.annotations; + } + ArrayPrototypePush(tools, tool); + } + return { tools }; + } + + async #callTool(params) { + const name = params?.name; + const entry = this.#tools.get(name); + if (entry === undefined) { + throw new McpError(INVALID_PARAMS, `Unknown tool: ${name}`); + } + try { + const value = await entry.handler(params?.arguments ?? {}); + return normalizeToolResult(value); + } catch (error) { + // Tool execution errors are reported in-band so the model can see + // them, per the MCP specification. + return { + content: [{ type: "text", text: `${error?.message ?? error}` }], + isError: true, + }; + } + } + + #listResources() { + const resources = []; + for ( + const { 0: uri, 1: entry } of new SafeMapIterator(this.#resources) + ) { + const metadata = entry.metadata; + const resource = { uri, name: metadata.name ?? uri }; + if (metadata.title !== undefined) resource.title = metadata.title; + // deno-lint-ignore prefer-primordials + if (metadata?.description !== undefined) { + // deno-lint-ignore prefer-primordials + resource.description = metadata?.description; + } + if (metadata.mimeType !== undefined) { + resource.mimeType = metadata.mimeType; + } + ArrayPrototypePush(resources, resource); + } + return { resources }; + } + + async #readResource(params) { + const uri = params?.uri; + const entry = this.#resources.get(uri); + if (entry === undefined) { + throw new McpError(RESOURCE_NOT_FOUND, `Resource not found: ${uri}`); + } + const value = await entry.handler(uri); + return normalizeResourceContents(uri, entry.metadata.mimeType, value); + } + + #listPrompts() { + const prompts = []; + for (const { 0: name, 1: entry } of new SafeMapIterator(this.#prompts)) { + const definition = entry.definition; + const prompt = { name }; + if (definition.title !== undefined) prompt.title = definition.title; + // deno-lint-ignore prefer-primordials + if (definition?.description !== undefined) { + // deno-lint-ignore prefer-primordials + prompt.description = definition?.description; + } + if (definition.arguments !== undefined) { + prompt.arguments = definition.arguments; + } + ArrayPrototypePush(prompts, prompt); + } + return { prompts }; + } + + async #getPrompt(params) { + const name = params?.name; + const entry = this.#prompts.get(name); + if (entry === undefined) { + throw new McpError(INVALID_PARAMS, `Unknown prompt: ${name}`); + } + const value = await entry.handler(params?.arguments ?? {}); + // deno-lint-ignore prefer-primordials + return normalizePromptResult(entry.definition?.description, value); + } + + // stdio transport: newline-delimited JSON-RPC messages on stdin/stdout. + async serve(options = { __proto__: null }) { + const transport = options?.transport ?? "stdio"; + if (transport !== "stdio") { + throw new TypeError( + `Unsupported transport: "${transport}". Use "stdio", or pass server.fetch to Deno.serve() for HTTP`, + ); + } + let buffer = new Uint8Array(0); + const chunk = new Uint8Array(16 * 1024); + while (true) { + const n = await io.stdin.read(chunk); + if (n === null) { + break; + } + buffer = concatBytes(buffer, TypedArrayPrototypeSubarray(chunk, 0, n)); + while (true) { + const newlineIndex = TypedArrayPrototypeIndexOf(buffer, NEWLINE); + if (newlineIndex === -1) { + break; + } + const line = TypedArrayPrototypeSubarray(buffer, 0, newlineIndex); + buffer = TypedArrayPrototypeSubarray(buffer, newlineIndex + 1); + if (TypedArrayPrototypeGetLength(line) === 0) { + continue; + } + await this.#handleStdioLine(line); + } + } + } + + async #handleStdioLine(line: Uint8Array) { + let message; + try { + message = JSONParse(decoder.decode(line)); + } catch { + await this.#writeStdioMessage( + rpcError(null, PARSE_ERROR, "Parse error"), + ); + return; + } + if (ArrayIsArray(message)) { + for (let i = 0; i < message.length; i++) { + const response = await this.#handleMessage(message[i]); + if (response !== null) { + await this.#writeStdioMessage(response); + } + } + return; + } + const response = await this.#handleMessage(message); + if (response !== null) { + await this.#writeStdioMessage(response); + } + } + + async #writeStdioMessage(message: object) { + let bytes = encoder.encode(JSONStringify(message) + "\n"); + while (TypedArrayPrototypeGetLength(bytes) > 0) { + const written = await io.stdout.write(bytes); + bytes = TypedArrayPrototypeSubarray(bytes, written); + } + } + + // Streamable HTTP transport (stateless): JSON-RPC messages are POSTed to + // the endpoint and answered with `application/json` responses. Pass + // `server.fetch` to `Deno.serve()`. + async #handleHttpRequest(request: Request): Promise { + const { Response } = lazyResponse(); + if (request.method !== "POST") { + return new Response(null, { + status: 405, + headers: { "allow": "POST" }, + }); + } + let body; + try { + body = JSONParse(await request.text()); + } catch { + return this.#jsonResponse( + rpcError(null, PARSE_ERROR, "Parse error"), + 400, + ); + } + if (ArrayIsArray(body)) { + const responses = []; + for (let i = 0; i < body.length; i++) { + const response = await this.#handleMessage(body[i]); + if (response !== null) { + ArrayPrototypePush(responses, response); + } + } + if (responses.length === 0) { + return new Response(null, { status: 202 }); + } + return this.#jsonResponse(responses, 200); + } + const response = await this.#handleMessage(body); + if (response === null) { + return new Response(null, { status: 202 }); + } + return this.#jsonResponse(response, 200); + } + + #jsonResponse(value: unknown, status: number): Response { + const { Response } = lazyResponse(); + return new Response(JSONStringify(value), { + status, + headers: { "content-type": "application/json" }, + }); + } +} + +const METHOD_NOT_FOUND_SENTINEL = Symbol("methodNotFound"); + +class McpError extends Error { + code: number; + + constructor(code: number, message: string) { + super(message); + this.code = code; + } +} +const McpErrorPrototype = McpError.prototype; + +return { McpServer }; +})(); diff --git a/ext/mcp/Cargo.toml b/ext/mcp/Cargo.toml new file mode 100644 index 00000000000000..da213df6e4d7a5 --- /dev/null +++ b/ext/mcp/Cargo.toml @@ -0,0 +1,17 @@ +# Copyright 2018-2026 the Deno authors. MIT license. + +[package] +name = "deno_mcp" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +readme = "README.md" +repository.workspace = true +description = "Implementation of the Deno MCP server API" + +[lib] +path = "lib.rs" + +[dependencies] +deno_core.workspace = true diff --git a/ext/mcp/README.md b/ext/mcp/README.md new file mode 100644 index 00000000000000..7069af44ddd826 --- /dev/null +++ b/ext/mcp/README.md @@ -0,0 +1,12 @@ +# deno_mcp + +This crate implements the unstable `Deno.McpServer` API, which makes it easy to +write Model Context Protocol (MCP) servers in Deno. + +It implements the MCP JSON-RPC lifecycle (initialize handshake, tools, resources +and prompts) and two transports: + +- stdio (newline-delimited JSON-RPC over stdin/stdout) +- streamable HTTP (a `fetch`-style handler usable with `Deno.serve`) + +Enable with `--unstable-mcp`. diff --git a/ext/mcp/clippy.toml b/ext/mcp/clippy.toml new file mode 100644 index 00000000000000..2d4edaf8c66c2a --- /dev/null +++ b/ext/mcp/clippy.toml @@ -0,0 +1,35 @@ +disallowed-methods = [ + { path = "std::path::Path::canonicalize", reason = "File system operations should be done using FileSystem trait" }, + { path = "std::path::Path::is_dir", reason = "File system operations should be done using FileSystem trait" }, + { path = "std::path::Path::is_file", reason = "File system operations should be done using FileSystem trait" }, + { path = "std::path::Path::is_symlink", reason = "File system operations should be done using FileSystem trait" }, + { path = "std::path::Path::metadata", reason = "File system operations should be done using FileSystem trait" }, + { path = "std::path::Path::read_dir", reason = "File system operations should be done using FileSystem trait" }, + { path = "std::path::Path::read_link", reason = "File system operations should be done using FileSystem trait" }, + { path = "std::path::Path::symlink_metadata", reason = "File system operations should be done using FileSystem trait" }, + { path = "std::path::Path::try_exists", reason = "File system operations should be done using FileSystem trait" }, + { path = "std::fs::canonicalize", reason = "File system operations should be done using FileSystem trait" }, + { path = "std::fs::copy", reason = "File system operations should be done using FileSystem trait" }, + { path = "std::fs::create_dir_all", reason = "File system operations should be done using FileSystem trait" }, + { path = "std::fs::create_dir", reason = "File system operations should be done using FileSystem trait" }, + { path = "std::fs::DirBuilder::new", reason = "File system operations should be done using FileSystem trait" }, + { path = "std::fs::hard_link", reason = "File system operations should be done using FileSystem trait" }, + { path = "std::fs::metadata", reason = "File system operations should be done using FileSystem trait" }, + { path = "std::fs::OpenOptions::new", reason = "File system operations should be done using FileSystem trait" }, + { path = "std::fs::read_dir", reason = "File system operations should be done using FileSystem trait" }, + { path = "std::fs::read_link", reason = "File system operations should be done using FileSystem trait" }, + { path = "std::fs::read_to_string", reason = "File system operations should be done using FileSystem trait" }, + { path = "std::fs::read", reason = "File system operations should be done using FileSystem trait" }, + { path = "std::fs::remove_dir_all", reason = "File system operations should be done using FileSystem trait" }, + { path = "std::fs::remove_dir", reason = "File system operations should be done using FileSystem trait" }, + { path = "std::fs::remove_file", reason = "File system operations should be done using FileSystem trait" }, + { path = "std::fs::rename", reason = "File system operations should be done using FileSystem trait" }, + { path = "std::fs::set_permissions", reason = "File system operations should be done using FileSystem trait" }, + { path = "std::fs::symlink_metadata", reason = "File system operations should be done using FileSystem trait" }, + { path = "std::fs::write", reason = "File system operations should be done using FileSystem trait" }, + { path = "std::path::Path::canonicalize", reason = "File system operations should be done using FileSystem trait" }, + { path = "std::path::Path::exists", reason = "File system operations should be done using FileSystem trait" }, + { path = "url::Url::to_file_path", reason = "Use deno_path_util instead" }, + { path = "url::Url::from_file_path", reason = "Use deno_path_util instead" }, + { path = "url::Url::from_directory_path", reason = "Use deno_path_util instead" }, +] diff --git a/ext/mcp/lib.rs b/ext/mcp/lib.rs new file mode 100644 index 00000000000000..42ee4be0b90589 --- /dev/null +++ b/ext/mcp/lib.rs @@ -0,0 +1,5 @@ +// Copyright 2018-2026 the Deno authors. MIT license. + +pub const UNSTABLE_FEATURE_NAME: &str = "mcp"; + +deno_core::extension!(deno_mcp, lazy_loaded_js = ["01_mcp.ts"],); diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index c495b93df2f6c5..dd48df9e1dd379 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -48,6 +48,7 @@ deno_image.workspace = true deno_inspector_server.workspace = true deno_io.workspace = true deno_kv.workspace = true +deno_mcp.workspace = true deno_napi.workspace = true deno_net.workspace = true deno_node.workspace = true diff --git a/runtime/features/data.rs b/runtime/features/data.rs index 678febd3c16b86..6dd3fed2f662f4 100644 --- a/runtime/features/data.rs +++ b/runtime/features/data.rs @@ -94,6 +94,13 @@ pub static FEATURE_DESCRIPTIONS: &[UnstableFeatureDescription] = &[ kind: UnstableFeatureKind::Cli, env_var: Some("DENO_UNSTABLE_LOCKFILE_V5"), }, + UnstableFeatureDescription { + name: "mcp", + help_text: "Enable unstable `Deno.McpServer` API", + show_in_help: true, + kind: UnstableFeatureKind::Runtime, + env_var: None, + }, UnstableFeatureDescription { name: "net", help_text: "enable unstable net APIs", diff --git a/runtime/features/gen.js b/runtime/features/gen.js index ee2e0ef185a40d..8eb15da733dd00 100644 --- a/runtime/features/gen.js +++ b/runtime/features/gen.js @@ -14,15 +14,16 @@ export const unstableIds = { fs: 7, http: 8, kv: 9, - net: 12, - noLegacyAbort: 13, - nodeGlobals: 14, - otel: 16, - process: 17, - rawImports: 18, - temporal: 21, - unsafeProto: 23, - vsock: 24, - webgpu: 25, - workerOptions: 26, + mcp: 12, + net: 13, + noLegacyAbort: 14, + nodeGlobals: 15, + otel: 17, + process: 18, + rawImports: 19, + temporal: 22, + unsafeProto: 24, + vsock: 25, + webgpu: 26, + workerOptions: 27, }; diff --git a/runtime/features/gen.rs b/runtime/features/gen.rs index 72dd1df3300153..07df7d69116c2a 100644 --- a/runtime/features/gen.rs +++ b/runtime/features/gen.rs @@ -103,12 +103,20 @@ pub static UNSTABLE_FEATURES: &[UnstableFeatureDefinition] = &[ id: 11, kind: UnstableFeatureKind::Cli, }, + UnstableFeatureDefinition { + name: "mcp", + flag_name: "unstable-mcp", + help_text: "Enable unstable `Deno.McpServer` API", + show_in_help: true, + id: 12, + kind: UnstableFeatureKind::Runtime, + }, UnstableFeatureDefinition { name: "net", flag_name: "unstable-net", help_text: "enable unstable net APIs", show_in_help: true, - id: 12, + id: 13, kind: UnstableFeatureKind::Runtime, }, UnstableFeatureDefinition { @@ -116,7 +124,7 @@ pub static UNSTABLE_FEATURES: &[UnstableFeatureDefinition] = &[ flag_name: "unstable-no-legacy-abort", help_text: "Enable abort signal in Deno.serve without legacy behavior. This will not abort the server when the request is handled successfully.", show_in_help: true, - id: 13, + id: 14, kind: UnstableFeatureKind::Runtime, }, UnstableFeatureDefinition { @@ -124,7 +132,7 @@ pub static UNSTABLE_FEATURES: &[UnstableFeatureDefinition] = &[ flag_name: "unstable-node-globals", help_text: "Prefer Node.js globals over Deno globals - currently this refers to `setTimeout` and `setInterval` APIs.", show_in_help: true, - id: 14, + id: 15, kind: UnstableFeatureKind::Runtime, }, UnstableFeatureDefinition { @@ -132,7 +140,7 @@ pub static UNSTABLE_FEATURES: &[UnstableFeatureDefinition] = &[ flag_name: "unstable-npm-lazy-caching", help_text: "Enable unstable lazy caching of npm dependencies, downloading them only as needed (disabled: all npm packages in package.json are installed on startup; enabled: only npm packages that are actually referenced in an import are installed", show_in_help: true, - id: 15, + id: 16, kind: UnstableFeatureKind::Cli, }, UnstableFeatureDefinition { @@ -140,7 +148,7 @@ pub static UNSTABLE_FEATURES: &[UnstableFeatureDefinition] = &[ flag_name: "unstable-otel", help_text: "Enable unstable OpenTelemetry features", show_in_help: false, - id: 16, + id: 17, kind: UnstableFeatureKind::Runtime, }, UnstableFeatureDefinition { @@ -148,7 +156,7 @@ pub static UNSTABLE_FEATURES: &[UnstableFeatureDefinition] = &[ flag_name: "unstable-process", help_text: "Enable unstable process APIs", show_in_help: false, - id: 17, + id: 18, kind: UnstableFeatureKind::Runtime, }, UnstableFeatureDefinition { @@ -156,7 +164,7 @@ pub static UNSTABLE_FEATURES: &[UnstableFeatureDefinition] = &[ flag_name: "unstable-raw-imports", help_text: "Enable unstable 'bytes' imports.", show_in_help: true, - id: 18, + id: 19, kind: UnstableFeatureKind::Runtime, }, UnstableFeatureDefinition { @@ -164,7 +172,7 @@ pub static UNSTABLE_FEATURES: &[UnstableFeatureDefinition] = &[ flag_name: "unstable-sloppy-imports", help_text: "Enable unstable resolving of specifiers by extension probing, .js to .ts, and directory probing", show_in_help: true, - id: 19, + id: 20, kind: UnstableFeatureKind::Cli, }, UnstableFeatureDefinition { @@ -172,7 +180,7 @@ pub static UNSTABLE_FEATURES: &[UnstableFeatureDefinition] = &[ flag_name: "unstable-subdomain-wildcards", help_text: "Enable subdomain wildcards support for the `--allow-net` flag", show_in_help: false, - id: 20, + id: 21, kind: UnstableFeatureKind::Cli, }, UnstableFeatureDefinition { @@ -180,7 +188,7 @@ pub static UNSTABLE_FEATURES: &[UnstableFeatureDefinition] = &[ flag_name: "unstable-temporal", help_text: "Enable unstable Temporal API", show_in_help: false, - id: 21, + id: 22, kind: UnstableFeatureKind::Runtime, }, UnstableFeatureDefinition { @@ -188,7 +196,7 @@ pub static UNSTABLE_FEATURES: &[UnstableFeatureDefinition] = &[ flag_name: "unstable-tsgo", help_text: "Enable unstable TypeScript Go integration", show_in_help: true, - id: 22, + id: 23, kind: UnstableFeatureKind::Cli, }, UnstableFeatureDefinition { @@ -196,7 +204,7 @@ pub static UNSTABLE_FEATURES: &[UnstableFeatureDefinition] = &[ flag_name: "unstable-unsafe-proto", help_text: "Enable unsafe __proto__ support. This is a security risk.", show_in_help: true, - id: 23, + id: 24, kind: UnstableFeatureKind::Runtime, }, UnstableFeatureDefinition { @@ -204,7 +212,7 @@ pub static UNSTABLE_FEATURES: &[UnstableFeatureDefinition] = &[ flag_name: "unstable-vsock", help_text: "Enable unstable VSOCK APIs", show_in_help: false, - id: 24, + id: 25, kind: UnstableFeatureKind::Runtime, }, UnstableFeatureDefinition { @@ -212,7 +220,7 @@ pub static UNSTABLE_FEATURES: &[UnstableFeatureDefinition] = &[ flag_name: "unstable-webgpu", help_text: "Enable unstable WebGPU APIs", show_in_help: true, - id: 25, + id: 26, kind: UnstableFeatureKind::Runtime, }, UnstableFeatureDefinition { @@ -220,7 +228,7 @@ pub static UNSTABLE_FEATURES: &[UnstableFeatureDefinition] = &[ flag_name: "unstable-worker-options", help_text: "Enable unstable Web Worker APIs", show_in_help: true, - id: 26, + id: 27, kind: UnstableFeatureKind::Runtime, }, ]; diff --git a/runtime/js/90_deno_ns.js b/runtime/js/90_deno_ns.js index 1afae1e4b9caff..cb08392d06a981 100644 --- a/runtime/js/90_deno_ns.js +++ b/runtime/js/90_deno_ns.js @@ -56,6 +56,11 @@ function lazyKv() { return _kvImpl ?? (_kvImpl = core.loadExtScript("ext:deno_kv/01_db.ts")); } const cron = core.loadExtScript("ext:deno_cron/01_cron.ts"); +// Deno.McpServer is a niche API and pulls in 06_streams via deno_fetch. Defer. +let _mcpImpl; +function lazyMcp() { + return _mcpImpl ?? (_mcpImpl = core.loadExtScript("ext:deno_mcp/01_mcp.ts")); +} const surface = core.loadExtScript("ext:deno_canvas/02_surface.js"); const telemetry = core.loadExtScript("ext:deno_telemetry/telemetry.ts"); import { unstableIds } from "ext:deno_features/flags.js"; @@ -315,6 +320,12 @@ denoNsUnstableById[unstableIds.kv] = { }, }; +denoNsUnstableById[unstableIds.mcp] = { + get McpServer() { + return lazyMcp().McpServer; + }, +}; + denoNsUnstableById[unstableIds.net] = { listenDatagram: net.createListenDatagram( op_net_listen_udp, diff --git a/runtime/lib.rs b/runtime/lib.rs index 6ef6d9960879fd..501ce2eb87ad10 100644 --- a/runtime/lib.rs +++ b/runtime/lib.rs @@ -13,6 +13,7 @@ pub use deno_image; pub use deno_inspector_server; pub use deno_io; pub use deno_kv; +pub use deno_mcp; pub use deno_napi; pub use deno_net; pub use deno_node; diff --git a/runtime/snapshot.rs b/runtime/snapshot.rs index 96c07354922ea8..383d2ee38569dc 100644 --- a/runtime/snapshot.rs +++ b/runtime/snapshot.rs @@ -75,6 +75,7 @@ pub fn create_runtime_snapshot( deno_cron::deno_cron::init(Box::new( deno_cron::local::LocalCronHandler::new(), )), + deno_mcp::deno_mcp::lazy_init(), deno_napi::deno_napi::lazy_init(), deno_http::deno_http::lazy_init(), deno_io::deno_io::lazy_init(), diff --git a/runtime/snapshot_info.rs b/runtime/snapshot_info.rs index 4b8691cefed769..67ab1e1fdc7b7d 100644 --- a/runtime/snapshot_info.rs +++ b/runtime/snapshot_info.rs @@ -41,6 +41,7 @@ pub fn get_extensions_in_snapshot() -> Vec { deno_cron::deno_cron::init(Box::new( deno_cron::local::LocalCronHandler::new(), )), + deno_mcp::deno_mcp::init(), deno_napi::deno_napi::init(None), deno_http::deno_http::init(deno_http::Options::default()), deno_io::deno_io::init(Some(Default::default())), diff --git a/runtime/web_worker.rs b/runtime/web_worker.rs index 204685ad1cb516..74da3084f9e6f7 100644 --- a/runtime/web_worker.rs +++ b/runtime/web_worker.rs @@ -559,6 +559,7 @@ impl WebWorker { deno_kv::KvConfig::builder().build(), ), deno_cron::deno_cron::init(Box::new(CronHandlerImpl::create_from_env())), + deno_mcp::deno_mcp::init(), deno_napi::deno_napi::init(services.deno_rt_native_addon_loader.clone()), deno_http::deno_http::init(deno_http::Options { no_legacy_abort: options.bootstrap.no_legacy_abort, diff --git a/runtime/worker.rs b/runtime/worker.rs index db5554daa3d42c..61ec7e5a486ccc 100644 --- a/runtime/worker.rs +++ b/runtime/worker.rs @@ -569,6 +569,7 @@ impl MainWorker { )), deno_kv::KvConfig::builder().build(), ), + deno_mcp::deno_mcp::args(), deno_napi::deno_napi::args( services.deno_rt_native_addon_loader.clone(), ), @@ -1118,6 +1119,7 @@ fn common_extensions< deno_tls::deno_tls::init(), deno_kv::deno_kv::lazy_init(), deno_cron::deno_cron::init(Box::new(CronHandlerImpl::create_from_env())), + deno_mcp::deno_mcp::lazy_init(), deno_napi::deno_napi::lazy_init(), deno_http::deno_http::lazy_init(), deno_io::deno_io::lazy_init(),