From 02236ca3e7fa85878410f65d450b91aa102c16c4 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Sun, 1 Mar 2026 01:27:40 +0200 Subject: [PATCH 1/4] ci: add Bun and Deno integration tests Add integration tests that run MCP server/client round-trips natively on Bun and Deno using their respective test runners, verifying the SDK works across runtimes beyond Node.js. - Add bun.test.ts using bun:test + Bun.serve() - Add deno.test.ts using Deno.test + Deno.serve() + node:assert - Add deno.json with nodeModulesDir: "manual" for pnpm compatibility - Add test-runtimes CI job (Bun 1.x + Deno v2.x matrix) - Gate publish job on test-runtimes passing - Exclude runtime-specific tests from vitest and tsconfig Co-Authored-By: Claude Opus 4.6 --- .github/workflows/main.yml | 38 +++++++++++++++- test/integration/deno.json | 3 ++ test/integration/package.json | 4 +- test/integration/test/server/bun.test.ts | 54 +++++++++++++++++++++++ test/integration/test/server/deno.test.ts | 50 +++++++++++++++++++++ test/integration/tsconfig.json | 2 +- test/integration/vitest.config.js | 7 ++- 7 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 test/integration/deno.json create mode 100644 test/integration/test/server/bun.test.ts create mode 100644 test/integration/test/server/deno.test.ts diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index baa1b6680..4fd4d35e9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -58,11 +58,47 @@ jobs: - run: pnpm test:all + test-runtimes: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - runtime: bun + version: "1.x" + - runtime: deno + version: v2.x + steps: + - uses: actions/checkout@v6 + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + run_install: false + - uses: actions/setup-node@v6 + with: + node-version: 24 + cache: pnpm + cache-dependency-path: pnpm-lock.yaml + - name: Set up Bun + if: matrix.runtime == 'bun' + uses: oven-sh/setup-bun@v2 + with: + bun-version: ${{ matrix.version }} + - name: Set up Deno + if: matrix.runtime == 'deno' + uses: denoland/setup-deno@v2 + with: + deno-version: ${{ matrix.version }} + - run: pnpm install + - run: pnpm build:all + - name: Run ${{ matrix.runtime }} integration tests + run: pnpm --filter @modelcontextprotocol/test-integration test:integration:${{ matrix.runtime }} + publish: runs-on: ubuntu-latest if: github.event_name == 'release' environment: release - needs: [build, test] + needs: [build, test, test-runtimes] permissions: contents: read diff --git a/test/integration/deno.json b/test/integration/deno.json new file mode 100644 index 000000000..fde86a1ef --- /dev/null +++ b/test/integration/deno.json @@ -0,0 +1,3 @@ +{ + "nodeModulesDir": "manual" +} diff --git a/test/integration/package.json b/test/integration/package.json index f4b97e4ca..afdfdc6e2 100644 --- a/test/integration/package.json +++ b/test/integration/package.json @@ -29,7 +29,9 @@ "test:watch": "vitest", "start": "npm run server", "server": "tsx watch --clear-screen=false scripts/cli.ts server", - "client": "tsx scripts/cli.ts client" + "client": "tsx scripts/cli.ts client", + "test:integration:bun": "bun test test/server/bun.test.ts", + "test:integration:deno": "deno test --config deno.json --no-check --allow-net --allow-read --allow-env test/server/deno.test.ts" }, "devDependencies": { "@modelcontextprotocol/core": "workspace:^", diff --git a/test/integration/test/server/bun.test.ts b/test/integration/test/server/bun.test.ts new file mode 100644 index 000000000..66f37f12d --- /dev/null +++ b/test/integration/test/server/bun.test.ts @@ -0,0 +1,54 @@ +/** + * Bun integration test + * + * Verifies the MCP server and client packages work natively on Bun. + * Run with: bun test test/server/bun.test.ts + */ + +import { describe, it, expect, beforeAll, afterAll } from 'bun:test'; +import { McpServer, WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client'; +import * as z from 'zod/v4'; + +describe('MCP on Bun', () => { + let httpServer: ReturnType; + let transport: WebStandardStreamableHTTPServerTransport; + + beforeAll(async () => { + const mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }); + + mcpServer.registerTool('greet', { + description: 'Greet someone', + inputSchema: z.object({ name: z.string() }), + }, async ({ name }) => ({ + content: [{ type: 'text' as const, text: `Hello, ${name}!` }], + })); + + transport = new WebStandardStreamableHTTPServerTransport(); + await mcpServer.connect(transport); + + httpServer = Bun.serve({ + port: 0, + fetch: (req) => transport.handleRequest(req), + }); + }); + + afterAll(async () => { + await transport?.close(); + httpServer?.stop(); + }); + + it('should handle MCP tool calls', async () => { + const client = new Client({ name: 'test-client', version: '1.0.0' }); + const clientTransport = new StreamableHTTPClientTransport( + new URL(`http://localhost:${httpServer.port}`) + ); + + await client.connect(clientTransport); + + const result = await client.callTool({ name: 'greet', arguments: { name: 'Bun' } }); + expect(result.content).toEqual([{ type: 'text', text: 'Hello, Bun!' }]); + + await client.close(); + }); +}); diff --git a/test/integration/test/server/deno.test.ts b/test/integration/test/server/deno.test.ts new file mode 100644 index 000000000..5c1fd4a67 --- /dev/null +++ b/test/integration/test/server/deno.test.ts @@ -0,0 +1,50 @@ +/** + * Deno integration test + * + * Verifies the MCP server and client packages work natively on Deno. + * Run with: deno test --config deno.json --no-check --allow-net --allow-read --allow-env test/server/deno.test.ts + */ + +import assert from 'node:assert/strict'; +import { McpServer, WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client'; +import * as z from 'zod/v4'; + +Deno.test({ + name: 'MCP tool calls work on Deno', + sanitizeOps: false, + sanitizeResources: false, + async fn() { + const mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }); + + mcpServer.registerTool('greet', { + description: 'Greet someone', + inputSchema: z.object({ name: z.string() }), + }, async ({ name }) => ({ + content: [{ type: 'text' as const, text: `Hello, ${name}!` }], + })); + + const transport = new WebStandardStreamableHTTPServerTransport(); + await mcpServer.connect(transport); + + const httpServer = Deno.serve({ port: 0 }, (req) => transport.handleRequest(req)); + const port = httpServer.addr.port; + + try { + const client = new Client({ name: 'test-client', version: '1.0.0' }); + const clientTransport = new StreamableHTTPClientTransport( + new URL(`http://localhost:${port}`) + ); + + await client.connect(clientTransport); + + const result = await client.callTool({ name: 'greet', arguments: { name: 'Deno' } }); + assert.deepStrictEqual(result.content, [{ type: 'text', text: 'Hello, Deno!' }]); + + await client.close(); + } finally { + await transport.close(); + await httpServer.shutdown(); + } + }, +}); diff --git a/test/integration/tsconfig.json b/test/integration/tsconfig.json index 353e6a708..5d68aa999 100644 --- a/test/integration/tsconfig.json +++ b/test/integration/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "@modelcontextprotocol/tsconfig", "include": ["./"], - "exclude": ["node_modules", "dist"], + "exclude": ["node_modules", "dist", "test/server/bun.test.ts", "test/server/deno.test.ts"], "compilerOptions": { "paths": { "*": ["./*"], diff --git a/test/integration/vitest.config.js b/test/integration/vitest.config.js index 38f030fdd..85a00a0e6 100644 --- a/test/integration/vitest.config.js +++ b/test/integration/vitest.config.js @@ -1,3 +1,8 @@ +import { defineConfig, mergeConfig } from 'vitest/config'; import baseConfig from '../../common/vitest-config/vitest.config.js'; -export default baseConfig; +export default mergeConfig(baseConfig, defineConfig({ + test: { + exclude: ['**/dist/**', '**/bun.test.ts', '**/deno.test.ts'], + }, +})); From 024d9cd9dfa397a5654d2d22dbe5c7721d0c7d5d Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Sun, 1 Mar 2026 01:37:07 +0200 Subject: [PATCH 2/4] fix: resolve lint and formatting issues in integration tests Fix import sorting, suppress bun:test unresolved import, and apply Prettier formatting to integration test files. Co-Authored-By: Claude Opus 4.6 --- test/integration/deno.json | 2 +- test/integration/test/server/bun.test.ts | 75 ++++++++++++----------- test/integration/test/server/deno.test.ts | 75 ++++++++++++----------- test/integration/vitest.config.js | 13 ++-- 4 files changed, 87 insertions(+), 78 deletions(-) diff --git a/test/integration/deno.json b/test/integration/deno.json index fde86a1ef..026959001 100644 --- a/test/integration/deno.json +++ b/test/integration/deno.json @@ -1,3 +1,3 @@ { - "nodeModulesDir": "manual" + "nodeModulesDir": "manual" } diff --git a/test/integration/test/server/bun.test.ts b/test/integration/test/server/bun.test.ts index 66f37f12d..229145aa8 100644 --- a/test/integration/test/server/bun.test.ts +++ b/test/integration/test/server/bun.test.ts @@ -5,50 +5,53 @@ * Run with: bun test test/server/bun.test.ts */ -import { describe, it, expect, beforeAll, afterAll } from 'bun:test'; -import { McpServer, WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client'; +import { McpServer, WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; +// eslint-disable-next-line import/no-unresolved +import { afterAll, beforeAll, describe, expect, it } from 'bun:test'; import * as z from 'zod/v4'; describe('MCP on Bun', () => { - let httpServer: ReturnType; - let transport: WebStandardStreamableHTTPServerTransport; - - beforeAll(async () => { - const mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }); - - mcpServer.registerTool('greet', { - description: 'Greet someone', - inputSchema: z.object({ name: z.string() }), - }, async ({ name }) => ({ - content: [{ type: 'text' as const, text: `Hello, ${name}!` }], - })); - - transport = new WebStandardStreamableHTTPServerTransport(); - await mcpServer.connect(transport); - - httpServer = Bun.serve({ - port: 0, - fetch: (req) => transport.handleRequest(req), + let httpServer: ReturnType; + let transport: WebStandardStreamableHTTPServerTransport; + + beforeAll(async () => { + const mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }); + + mcpServer.registerTool( + 'greet', + { + description: 'Greet someone', + inputSchema: z.object({ name: z.string() }) + }, + async ({ name }) => ({ + content: [{ type: 'text' as const, text: `Hello, ${name}!` }] + }) + ); + + transport = new WebStandardStreamableHTTPServerTransport(); + await mcpServer.connect(transport); + + httpServer = Bun.serve({ + port: 0, + fetch: req => transport.handleRequest(req) + }); }); - }); - afterAll(async () => { - await transport?.close(); - httpServer?.stop(); - }); + afterAll(async () => { + await transport?.close(); + httpServer?.stop(); + }); - it('should handle MCP tool calls', async () => { - const client = new Client({ name: 'test-client', version: '1.0.0' }); - const clientTransport = new StreamableHTTPClientTransport( - new URL(`http://localhost:${httpServer.port}`) - ); + it('should handle MCP tool calls', async () => { + const client = new Client({ name: 'test-client', version: '1.0.0' }); + const clientTransport = new StreamableHTTPClientTransport(new URL(`http://localhost:${httpServer.port}`)); - await client.connect(clientTransport); + await client.connect(clientTransport); - const result = await client.callTool({ name: 'greet', arguments: { name: 'Bun' } }); - expect(result.content).toEqual([{ type: 'text', text: 'Hello, Bun!' }]); + const result = await client.callTool({ name: 'greet', arguments: { name: 'Bun' } }); + expect(result.content).toEqual([{ type: 'text', text: 'Hello, Bun!' }]); - await client.close(); - }); + await client.close(); + }); }); diff --git a/test/integration/test/server/deno.test.ts b/test/integration/test/server/deno.test.ts index 5c1fd4a67..ea5cd425b 100644 --- a/test/integration/test/server/deno.test.ts +++ b/test/integration/test/server/deno.test.ts @@ -6,45 +6,48 @@ */ import assert from 'node:assert/strict'; -import { McpServer, WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; + import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client'; +import { McpServer, WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/server'; import * as z from 'zod/v4'; Deno.test({ - name: 'MCP tool calls work on Deno', - sanitizeOps: false, - sanitizeResources: false, - async fn() { - const mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }); - - mcpServer.registerTool('greet', { - description: 'Greet someone', - inputSchema: z.object({ name: z.string() }), - }, async ({ name }) => ({ - content: [{ type: 'text' as const, text: `Hello, ${name}!` }], - })); - - const transport = new WebStandardStreamableHTTPServerTransport(); - await mcpServer.connect(transport); - - const httpServer = Deno.serve({ port: 0 }, (req) => transport.handleRequest(req)); - const port = httpServer.addr.port; - - try { - const client = new Client({ name: 'test-client', version: '1.0.0' }); - const clientTransport = new StreamableHTTPClientTransport( - new URL(`http://localhost:${port}`) - ); - - await client.connect(clientTransport); - - const result = await client.callTool({ name: 'greet', arguments: { name: 'Deno' } }); - assert.deepStrictEqual(result.content, [{ type: 'text', text: 'Hello, Deno!' }]); - - await client.close(); - } finally { - await transport.close(); - await httpServer.shutdown(); + name: 'MCP tool calls work on Deno', + sanitizeOps: false, + sanitizeResources: false, + async fn() { + const mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }); + + mcpServer.registerTool( + 'greet', + { + description: 'Greet someone', + inputSchema: z.object({ name: z.string() }) + }, + async ({ name }) => ({ + content: [{ type: 'text' as const, text: `Hello, ${name}!` }] + }) + ); + + const transport = new WebStandardStreamableHTTPServerTransport(); + await mcpServer.connect(transport); + + const httpServer = Deno.serve({ port: 0 }, req => transport.handleRequest(req)); + const port = httpServer.addr.port; + + try { + const client = new Client({ name: 'test-client', version: '1.0.0' }); + const clientTransport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}`)); + + await client.connect(clientTransport); + + const result = await client.callTool({ name: 'greet', arguments: { name: 'Deno' } }); + assert.deepStrictEqual(result.content, [{ type: 'text', text: 'Hello, Deno!' }]); + + await client.close(); + } finally { + await transport.close(); + await httpServer.shutdown(); + } } - }, }); diff --git a/test/integration/vitest.config.js b/test/integration/vitest.config.js index 85a00a0e6..5aad0051c 100644 --- a/test/integration/vitest.config.js +++ b/test/integration/vitest.config.js @@ -1,8 +1,11 @@ import { defineConfig, mergeConfig } from 'vitest/config'; import baseConfig from '../../common/vitest-config/vitest.config.js'; -export default mergeConfig(baseConfig, defineConfig({ - test: { - exclude: ['**/dist/**', '**/bun.test.ts', '**/deno.test.ts'], - }, -})); +export default mergeConfig( + baseConfig, + defineConfig({ + test: { + exclude: ['**/dist/**', '**/bun.test.ts', '**/deno.test.ts'] + } + }) +); From 3a8a56cd9eba65d30d8b9f59487292eff880e05c Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Sun, 1 Mar 2026 01:41:53 +0200 Subject: [PATCH 3/4] chore: remove unnecessary deno.json config Deno v2.x auto-detects package.json and node_modules without needing explicit nodeModulesDir configuration. Co-Authored-By: Claude Opus 4.6 --- test/integration/deno.json | 3 --- test/integration/package.json | 2 +- test/integration/test/server/deno.test.ts | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) delete mode 100644 test/integration/deno.json diff --git a/test/integration/deno.json b/test/integration/deno.json deleted file mode 100644 index 026959001..000000000 --- a/test/integration/deno.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "nodeModulesDir": "manual" -} diff --git a/test/integration/package.json b/test/integration/package.json index afdfdc6e2..da21238ed 100644 --- a/test/integration/package.json +++ b/test/integration/package.json @@ -31,7 +31,7 @@ "server": "tsx watch --clear-screen=false scripts/cli.ts server", "client": "tsx scripts/cli.ts client", "test:integration:bun": "bun test test/server/bun.test.ts", - "test:integration:deno": "deno test --config deno.json --no-check --allow-net --allow-read --allow-env test/server/deno.test.ts" + "test:integration:deno": "deno test --no-check --allow-net --allow-read --allow-env test/server/deno.test.ts" }, "devDependencies": { "@modelcontextprotocol/core": "workspace:^", diff --git a/test/integration/test/server/deno.test.ts b/test/integration/test/server/deno.test.ts index ea5cd425b..7d45f815c 100644 --- a/test/integration/test/server/deno.test.ts +++ b/test/integration/test/server/deno.test.ts @@ -2,7 +2,7 @@ * Deno integration test * * Verifies the MCP server and client packages work natively on Deno. - * Run with: deno test --config deno.json --no-check --allow-net --allow-read --allow-env test/server/deno.test.ts + * Run with: deno test --no-check --allow-net --allow-read --allow-env test/server/deno.test.ts */ import assert from 'node:assert/strict'; From cc4397acfc41d64ad31227be535386f175b03a33 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Sun, 1 Mar 2026 01:48:43 +0200 Subject: [PATCH 4/4] docs: add Bun and Deno runtime support to README and quickstarts Co-Authored-By: Claude Opus 4.6 --- README.md | 10 +++++++++- docs/client-quickstart.md | 5 ++++- docs/server-quickstart.md | 3 +++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 21dbec6a6..f068ec915 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ The Model Context Protocol (MCP) allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction. -This repository contains the TypeScript SDK implementation of the MCP specification and ships: +This repository contains the TypeScript SDK implementation of the MCP specification. It runs on **Node.js**, **Bun**, and **Deno**, and ships: - MCP **server** libraries (tools/resources/prompts, Streamable HTTP, stdio, auth helpers) - MCP **client** libraries (transports, high-level helpers, OAuth helpers) @@ -57,12 +57,20 @@ They are intentionally thin adapters: they should not introduce new MCP function ```bash npm install @modelcontextprotocol/server zod +# or +bun add @modelcontextprotocol/server zod +# or +deno add npm:@modelcontextprotocol/server npm:zod ``` ### Client ```bash npm install @modelcontextprotocol/client zod +# or +bun add @modelcontextprotocol/client zod +# or +deno add npm:@modelcontextprotocol/client npm:zod ``` ### Optional middleware packages diff --git a/docs/client-quickstart.md b/docs/client-quickstart.md index bd695c7bb..0669ab8a7 100644 --- a/docs/client-quickstart.md +++ b/docs/client-quickstart.md @@ -19,10 +19,13 @@ This quickstart assumes you have familiarity with: Before starting, ensure your system meets these requirements: -- Node.js 20 or higher installed +- Node.js 20 or higher installed (or **Bun** / **Deno** — the SDK supports all three runtimes) - Latest version of `npm` installed - An Anthropic API key from the [Anthropic Console](https://console.anthropic.com/settings/keys) +> [!TIP] +> This tutorial uses Node.js and npm, but you can substitute `bun` or `deno` commands where appropriate. For example, use `bun add` instead of `npm install`, or run the client with `bun run` / `deno run`. + ## Set up your environment First, let's create and set up our project: diff --git a/docs/server-quickstart.md b/docs/server-quickstart.md index ffa49cec5..4ee04fd9c 100644 --- a/docs/server-quickstart.md +++ b/docs/server-quickstart.md @@ -36,6 +36,9 @@ node --version npm --version ``` +> [!TIP] +> The MCP SDK also works with **Bun** and **Deno**. This tutorial uses Node.js, but you can substitute `bun` or `deno` commands where appropriate. For HTTP-based servers on Bun or Deno, use `WebStandardStreamableHTTPServerTransport` instead of the Node.js-specific transport — see the [server guide](./server.md) for details. + ## Set up your environment First, let's install Node.js and npm if you haven't already. You can download them from [nodejs.org](https://nodejs.org/).