From 29711d07d3fd34158d518807952589c39257f29e Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 18 Jul 2025 18:31:00 -0700 Subject: [PATCH] chore: use streamable http by default (#716) Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> --- README.md | 6 +- package-lock.json | 18 ++-- package.json | 2 +- src/transport.ts | 39 ++++--- tests/http.spec.ts | 259 +++++++++++++++++++++++++++++++++++++++++++++ tests/sse.spec.ts | 32 ++---- 6 files changed, 305 insertions(+), 51 deletions(-) create mode 100644 tests/http.spec.ts diff --git a/README.md b/README.md index 0448f12..631495a 100644 --- a/README.md +++ b/README.md @@ -303,19 +303,19 @@ npx @playwright/mcp@latest --config path/to/config.json ### Standalone MCP server When running headed browser on system w/o display or from worker processes of the IDEs, -run the MCP server from environment with the DISPLAY and pass the `--port` flag to enable SSE transport. +run the MCP server from environment with the DISPLAY and pass the `--port` flag to enable HTTP transport. ```bash npx @playwright/mcp@latest --port 8931 ``` -And then in MCP client config, set the `url` to the SSE endpoint: +And then in MCP client config, set the `url` to the HTTP endpoint: ```js { "mcpServers": { "playwright": { - "url": "http://localhost:8931/sse" + "url": "http://localhost:8931/mcp" } } } diff --git a/package-lock.json b/package-lock.json index c4c64af..68b3aad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.31", "license": "Apache-2.0", "dependencies": { - "@modelcontextprotocol/sdk": "^1.11.0", + "@modelcontextprotocol/sdk": "^1.16.0", "commander": "^13.1.0", "debug": "^4.4.1", "mime": "^4.0.7", @@ -234,15 +234,17 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.0.tgz", - "integrity": "sha512-k/1pb70eD638anoi0e8wUGAlbMJXyvdV4p62Ko+EZ7eBe1xMx8Uhak1R5DgfoofsK5IBBnRwsYGTaLZl+6/+RQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.16.0.tgz", + "integrity": "sha512-8ofX7gkZcLj9H9rSd50mCgm3SSF8C7XoclxJuLoV0Cz3rEQ1tv9MZRYYvJtm9n1BiEQQMzSmE/w2AEkNacLYfg==", "license": "MIT", "dependencies": { + "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", - "cross-spawn": "^7.0.3", + "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", @@ -713,7 +715,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -1850,7 +1851,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -1887,7 +1887,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { @@ -2808,7 +2807,6 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { @@ -3367,7 +3365,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -4161,7 +4158,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" diff --git a/package.json b/package.json index 8ca2237..3a46577 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ } }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.11.0", + "@modelcontextprotocol/sdk": "^1.16.0", "commander": "^13.1.0", "debug": "^4.4.1", "mime": "^4.0.7", diff --git a/src/transport.ts b/src/transport.ts index b645a1f..14858e9 100644 --- a/src/transport.ts +++ b/src/transport.ts @@ -23,8 +23,11 @@ import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { logUnhandledError } from './log.js'; + import type { AddressInfo } from 'node:net'; import type { Server } from './server.js'; +import type { Connection } from './connection.js'; export async function startStdioTransport(server: Server) { return await server.createConnection(new StdioServerTransport()); @@ -55,8 +58,7 @@ async function handleSSE(server: Server, req: http.IncomingMessage, res: http.Se res.on('close', () => { testDebug(`delete SSE session: ${transport.sessionId}`); sessions.delete(transport.sessionId); - // eslint-disable-next-line no-console - void connection.close().catch(e => console.error(e)); + void connection.close().catch(logUnhandledError); }); return; } @@ -65,10 +67,10 @@ async function handleSSE(server: Server, req: http.IncomingMessage, res: http.Se res.end('Method not allowed'); } -async function handleStreamable(server: Server, req: http.IncomingMessage, res: http.ServerResponse, sessions: Map) { +async function handleStreamable(server: Server, req: http.IncomingMessage, res: http.ServerResponse, sessions: Map) { const sessionId = req.headers['mcp-session-id'] as string | undefined; if (sessionId) { - const transport = sessions.get(sessionId); + const { transport } = sessions.get(sessionId) ?? {}; if (!transport) { res.statusCode = 404; res.end('Session not found'); @@ -80,15 +82,22 @@ async function handleStreamable(server: Server, req: http.IncomingMessage, res: if (req.method === 'POST') { const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => crypto.randomUUID(), - onsessioninitialized: sessionId => { - sessions.set(sessionId, transport); + onsessioninitialized: async sessionId => { + testDebug(`create http session: ${transport.sessionId}`); + const connection = await server.createConnection(transport); + sessions.set(sessionId, { transport, connection }); } }); + transport.onclose = () => { - if (transport.sessionId) - sessions.delete(transport.sessionId); + const result = transport.sessionId ? sessions.get(transport.sessionId) : undefined; + if (!result) + return; + sessions.delete(result.transport.sessionId!); + testDebug(`delete http session: ${transport.sessionId}`); + result.connection.close().catch(logUnhandledError); }; - await server.createConnection(transport); + await transport.handleRequest(req, res); return; } @@ -112,13 +121,13 @@ export async function startHttpServer(config: { host?: string, port?: number }): export function startHttpTransport(httpServer: http.Server, mcpServer: Server) { const sseSessions = new Map(); - const streamableSessions = new Map(); + const streamableSessions = new Map(); httpServer.on('request', async (req, res) => { const url = new URL(`http://localhost${req.url}`); - if (url.pathname.startsWith('/mcp')) - await handleStreamable(mcpServer, req, res, streamableSessions); - else + if (url.pathname.startsWith('/sse')) await handleSSE(mcpServer, req, res, url, sseSessions); + else + await handleStreamable(mcpServer, req, res, streamableSessions); }); const url = httpAddressToString(httpServer.address()); const message = [ @@ -127,11 +136,11 @@ export function startHttpTransport(httpServer: http.Server, mcpServer: Server) { JSON.stringify({ 'mcpServers': { 'playwright': { - 'url': `${url}/sse` + 'url': `${url}/mcp` } } }, undefined, 2), - 'If your client supports streamable HTTP, you can use the /mcp endpoint instead.', + 'For legacy SSE transport support, you can use the /sse endpoint instead.', ].join('\n'); // eslint-disable-next-line no-console console.error(message); diff --git a/tests/http.spec.ts b/tests/http.spec.ts new file mode 100644 index 0000000..4ff9ac9 --- /dev/null +++ b/tests/http.spec.ts @@ -0,0 +1,259 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'node:fs'; +import url from 'node:url'; + +import { ChildProcess, spawn } from 'node:child_process'; +import path from 'node:path'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; + +import { test as baseTest, expect } from './fixtures.js'; +import type { Config } from '../config.d.ts'; + +// NOTE: Can be removed when we drop Node.js 18 support and changed to import.meta.filename. +const __filename = url.fileURLToPath(import.meta.url); + +const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noPort?: boolean }) => Promise<{ url: URL, stderr: () => string }> }>({ + serverEndpoint: async ({ mcpHeadless }, use, testInfo) => { + let cp: ChildProcess | undefined; + const userDataDir = testInfo.outputPath('user-data-dir'); + await use(async (options?: { args?: string[], noPort?: boolean }) => { + if (cp) + throw new Error('Process already running'); + + cp = spawn('node', [ + path.join(path.dirname(__filename), '../cli.js'), + ...(options?.noPort ? [] : ['--port=0']), + '--user-data-dir=' + userDataDir, + ...(mcpHeadless ? ['--headless'] : []), + ...(options?.args || []), + ], { + stdio: 'pipe', + env: { + ...process.env, + DEBUG: 'pw:mcp:test', + DEBUG_COLORS: '0', + DEBUG_HIDE_DATE: '1', + }, + }); + let stderr = ''; + const url = await new Promise(resolve => cp!.stderr?.on('data', data => { + stderr += data.toString(); + const match = stderr.match(/Listening on (http:\/\/.*)/); + if (match) + resolve(match[1]); + })); + + return { url: new URL(url), stderr: () => stderr }; + }); + cp?.kill('SIGTERM'); + }, +}); + +test('http transport', async ({ serverEndpoint }) => { + const { url } = await serverEndpoint(); + const transport = new StreamableHTTPClientTransport(new URL('/mcp', url)); + const client = new Client({ name: 'test', version: '1.0.0' }); + await client.connect(transport); + await client.ping(); +}); + +test('http transport (config)', async ({ serverEndpoint }) => { + const config: Config = { + server: { + port: 0, + } + }; + const configFile = test.info().outputPath('config.json'); + await fs.promises.writeFile(configFile, JSON.stringify(config, null, 2)); + + const { url } = await serverEndpoint({ noPort: true, args: ['--config=' + configFile] }); + const transport = new StreamableHTTPClientTransport(new URL('/mcp', url)); + const client = new Client({ name: 'test', version: '1.0.0' }); + await client.connect(transport); + await client.ping(); +}); + +test('http transport browser lifecycle (isolated)', async ({ serverEndpoint, server }) => { + const { url, stderr } = await serverEndpoint({ args: ['--isolated'] }); + + const transport1 = new StreamableHTTPClientTransport(new URL('/mcp', url)); + const client1 = new Client({ name: 'test', version: '1.0.0' }); + await client1.connect(transport1); + await client1.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + /** + * src/client/streamableHttp.ts + * Clients that no longer need a particular session + * (e.g., because the user is leaving the client application) SHOULD send an + * HTTP DELETE to the MCP endpoint with the Mcp-Session-Id header to explicitly + * terminate the session. + */ + await transport1.terminateSession(); + await client1.close(); + + const transport2 = new StreamableHTTPClientTransport(new URL('/mcp', url)); + const client2 = new Client({ name: 'test', version: '1.0.0' }); + await client2.connect(transport2); + await client2.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + await transport2.terminateSession(); + await client2.close(); + + await expect(async () => { + const lines = stderr().split('\n'); + expect(lines.filter(line => line.match(/create http session/)).length).toBe(2); + expect(lines.filter(line => line.match(/delete http session/)).length).toBe(2); + + expect(lines.filter(line => line.match(/create context/)).length).toBe(2); + expect(lines.filter(line => line.match(/close context/)).length).toBe(2); + + expect(lines.filter(line => line.match(/create browser context \(isolated\)/)).length).toBe(2); + expect(lines.filter(line => line.match(/close browser context \(isolated\)/)).length).toBe(2); + + expect(lines.filter(line => line.match(/obtain browser \(isolated\)/)).length).toBe(2); + expect(lines.filter(line => line.match(/close browser \(isolated\)/)).length).toBe(2); + }).toPass(); +}); + +test('http transport browser lifecycle (isolated, multiclient)', async ({ serverEndpoint, server }) => { + const { url, stderr } = await serverEndpoint({ args: ['--isolated'] }); + + const transport1 = new StreamableHTTPClientTransport(new URL('/mcp', url)); + const client1 = new Client({ name: 'test', version: '1.0.0' }); + await client1.connect(transport1); + await client1.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + const transport2 = new StreamableHTTPClientTransport(new URL('/mcp', url)); + const client2 = new Client({ name: 'test', version: '1.0.0' }); + await client2.connect(transport2); + await client2.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + await transport1.terminateSession(); + await client1.close(); + + const transport3 = new StreamableHTTPClientTransport(new URL('/mcp', url)); + const client3 = new Client({ name: 'test', version: '1.0.0' }); + await client3.connect(transport3); + await client3.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + await transport2.terminateSession(); + await client2.close(); + await transport3.terminateSession(); + await client3.close(); + + await expect(async () => { + const lines = stderr().split('\n'); + expect(lines.filter(line => line.match(/create http session/)).length).toBe(3); + expect(lines.filter(line => line.match(/delete http session/)).length).toBe(3); + + expect(lines.filter(line => line.match(/create context/)).length).toBe(3); + expect(lines.filter(line => line.match(/close context/)).length).toBe(3); + + expect(lines.filter(line => line.match(/create browser context \(isolated\)/)).length).toBe(3); + expect(lines.filter(line => line.match(/close browser context \(isolated\)/)).length).toBe(3); + + expect(lines.filter(line => line.match(/obtain browser \(isolated\)/)).length).toBe(1); + expect(lines.filter(line => line.match(/close browser \(isolated\)/)).length).toBe(1); + }).toPass(); +}); + +test('http transport browser lifecycle (persistent)', async ({ serverEndpoint, server }) => { + const { url, stderr } = await serverEndpoint(); + + const transport1 = new StreamableHTTPClientTransport(new URL('/mcp', url)); + const client1 = new Client({ name: 'test', version: '1.0.0' }); + await client1.connect(transport1); + await client1.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + await transport1.terminateSession(); + await client1.close(); + + const transport2 = new StreamableHTTPClientTransport(new URL('/mcp', url)); + const client2 = new Client({ name: 'test', version: '1.0.0' }); + await client2.connect(transport2); + await client2.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + await transport2.terminateSession(); + await client2.close(); + + await expect(async () => { + const lines = stderr().split('\n'); + expect(lines.filter(line => line.match(/create http session/)).length).toBe(2); + expect(lines.filter(line => line.match(/delete http session/)).length).toBe(2); + + expect(lines.filter(line => line.match(/create context/)).length).toBe(2); + expect(lines.filter(line => line.match(/close context/)).length).toBe(2); + + expect(lines.filter(line => line.match(/create browser context \(persistent\)/)).length).toBe(2); + expect(lines.filter(line => line.match(/close browser context \(persistent\)/)).length).toBe(2); + + expect(lines.filter(line => line.match(/lock user data dir/)).length).toBe(2); + expect(lines.filter(line => line.match(/release user data dir/)).length).toBe(2); + }).toPass(); +}); + +test('http transport browser lifecycle (persistent, multiclient)', async ({ serverEndpoint, server }) => { + const { url } = await serverEndpoint(); + + const transport1 = new StreamableHTTPClientTransport(new URL('/mcp', url)); + const client1 = new Client({ name: 'test', version: '1.0.0' }); + await client1.connect(transport1); + await client1.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + const transport2 = new StreamableHTTPClientTransport(new URL('/mcp', url)); + const client2 = new Client({ name: 'test', version: '1.0.0' }); + await client2.connect(transport2); + const response = await client2.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + expect(response.isError).toBe(true); + expect(response.content?.[0].text).toContain('use --isolated to run multiple instances of the same browser'); + + await client1.close(); + await client2.close(); +}); + +test('http transport (default)', async ({ serverEndpoint }) => { + const { url } = await serverEndpoint(); + const transport = new StreamableHTTPClientTransport(url); + const client = new Client({ name: 'test', version: '1.0.0' }); + await client.connect(transport); + await client.ping(); + expect(transport.sessionId, 'has session support').toBeDefined(); +}); diff --git a/tests/sse.spec.ts b/tests/sse.spec.ts index 9e888a8..60cc740 100644 --- a/tests/sse.spec.ts +++ b/tests/sse.spec.ts @@ -20,7 +20,6 @@ import url from 'node:url'; import { ChildProcess, spawn } from 'node:child_process'; import path from 'node:path'; import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; -import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { test as baseTest, expect } from './fixtures.js'; @@ -68,7 +67,7 @@ const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noP test('sse transport', async ({ serverEndpoint }) => { const { url } = await serverEndpoint(); - const transport = new SSEClientTransport(url); + const transport = new SSEClientTransport(new URL('/sse', url)); const client = new Client({ name: 'test', version: '1.0.0' }); await client.connect(transport); await client.ping(); @@ -84,7 +83,7 @@ test('sse transport (config)', async ({ serverEndpoint }) => { await fs.promises.writeFile(configFile, JSON.stringify(config, null, 2)); const { url } = await serverEndpoint({ noPort: true, args: ['--config=' + configFile] }); - const transport = new SSEClientTransport(url); + const transport = new SSEClientTransport(new URL('/sse', url)); const client = new Client({ name: 'test', version: '1.0.0' }); await client.connect(transport); await client.ping(); @@ -93,7 +92,7 @@ test('sse transport (config)', async ({ serverEndpoint }) => { test('sse transport browser lifecycle (isolated)', async ({ serverEndpoint, server }) => { const { url, stderr } = await serverEndpoint({ args: ['--isolated'] }); - const transport1 = new SSEClientTransport(url); + const transport1 = new SSEClientTransport(new URL('/sse', url)); const client1 = new Client({ name: 'test', version: '1.0.0' }); await client1.connect(transport1); await client1.callTool({ @@ -102,7 +101,7 @@ test('sse transport browser lifecycle (isolated)', async ({ serverEndpoint, serv }); await client1.close(); - const transport2 = new SSEClientTransport(url); + const transport2 = new SSEClientTransport(new URL('/sse', url)); const client2 = new Client({ name: 'test', version: '1.0.0' }); await client2.connect(transport2); await client2.callTool({ @@ -130,7 +129,7 @@ test('sse transport browser lifecycle (isolated)', async ({ serverEndpoint, serv test('sse transport browser lifecycle (isolated, multiclient)', async ({ serverEndpoint, server }) => { const { url, stderr } = await serverEndpoint({ args: ['--isolated'] }); - const transport1 = new SSEClientTransport(url); + const transport1 = new SSEClientTransport(new URL('/sse', url)); const client1 = new Client({ name: 'test', version: '1.0.0' }); await client1.connect(transport1); await client1.callTool({ @@ -138,7 +137,7 @@ test('sse transport browser lifecycle (isolated, multiclient)', async ({ serverE arguments: { url: server.HELLO_WORLD }, }); - const transport2 = new SSEClientTransport(url); + const transport2 = new SSEClientTransport(new URL('/sse', url)); const client2 = new Client({ name: 'test', version: '1.0.0' }); await client2.connect(transport2); await client2.callTool({ @@ -147,7 +146,7 @@ test('sse transport browser lifecycle (isolated, multiclient)', async ({ serverE }); await client1.close(); - const transport3 = new SSEClientTransport(url); + const transport3 = new SSEClientTransport(new URL('/sse', url)); const client3 = new Client({ name: 'test', version: '1.0.0' }); await client3.connect(transport3); await client3.callTool({ @@ -177,7 +176,7 @@ test('sse transport browser lifecycle (isolated, multiclient)', async ({ serverE test('sse transport browser lifecycle (persistent)', async ({ serverEndpoint, server }) => { const { url, stderr } = await serverEndpoint(); - const transport1 = new SSEClientTransport(url); + const transport1 = new SSEClientTransport(new URL('/sse', url)); const client1 = new Client({ name: 'test', version: '1.0.0' }); await client1.connect(transport1); await client1.callTool({ @@ -186,7 +185,7 @@ test('sse transport browser lifecycle (persistent)', async ({ serverEndpoint, se }); await client1.close(); - const transport2 = new SSEClientTransport(url); + const transport2 = new SSEClientTransport(new URL('/sse', url)); const client2 = new Client({ name: 'test', version: '1.0.0' }); await client2.connect(transport2); await client2.callTool({ @@ -214,7 +213,7 @@ test('sse transport browser lifecycle (persistent)', async ({ serverEndpoint, se test('sse transport browser lifecycle (persistent, multiclient)', async ({ serverEndpoint, server }) => { const { url } = await serverEndpoint(); - const transport1 = new SSEClientTransport(url); + const transport1 = new SSEClientTransport(new URL('/sse', url)); const client1 = new Client({ name: 'test', version: '1.0.0' }); await client1.connect(transport1); await client1.callTool({ @@ -222,7 +221,7 @@ test('sse transport browser lifecycle (persistent, multiclient)', async ({ serve arguments: { url: server.HELLO_WORLD }, }); - const transport2 = new SSEClientTransport(url); + const transport2 = new SSEClientTransport(new URL('/sse', url)); const client2 = new Client({ name: 'test', version: '1.0.0' }); await client2.connect(transport2); const response = await client2.callTool({ @@ -235,12 +234,3 @@ test('sse transport browser lifecycle (persistent, multiclient)', async ({ serve await client1.close(); await client2.close(); }); - -test('streamable http transport', async ({ serverEndpoint }) => { - const { url } = await serverEndpoint(); - const transport = new StreamableHTTPClientTransport(new URL('/mcp', url)); - const client = new Client({ name: 'test', version: '1.0.0' }); - await client.connect(transport); - await client.ping(); - expect(transport.sessionId, 'has session support').toBeDefined(); -});