diff --git a/src/config.ts b/src/config.ts index e550516..cb7825e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -68,6 +68,7 @@ const defaultConfig: FullConfig = { allowedOrigins: undefined, blockedOrigins: undefined, }, + server: {}, outputDir: path.join(os.tmpdir(), 'playwright-mcp-output', sanitizeForFilePath(new Date().toISOString())), }; @@ -81,6 +82,7 @@ export type FullConfig = Config & { }, network: NonNullable, outputDir: string; + server: NonNullable, }; export async function resolveConfig(config: Config): Promise { @@ -256,6 +258,10 @@ function mergeConfig(base: FullConfig, overrides: Config): FullConfig { network: { ...pickDefined(base.network), ...pickDefined(overrides.network), - } + }, + server: { + ...pickDefined(base.server), + ...pickDefined(overrides.server), + }, } as FullConfig; } diff --git a/src/program.ts b/src/program.ts index 2c1c7a4..8f1dd7c 100644 --- a/src/program.ts +++ b/src/program.ts @@ -56,8 +56,8 @@ program const server = new Server(config); server.setupExitWatchdog(); - if (options.port) - startHttpTransport(server, +options.port, options.host); + if (config.server.port !== undefined) + startHttpTransport(server); else await startStdioTransport(server); diff --git a/src/transport.ts b/src/transport.ts index 32a94c3..14f6a8d 100644 --- a/src/transport.ts +++ b/src/transport.ts @@ -96,7 +96,7 @@ async function handleStreamable(server: Server, req: http.IncomingMessage, res: res.end('Invalid request'); } -export function startHttpTransport(server: Server, port: number, hostname: string | undefined) { +export function startHttpTransport(server: Server) { const sseSessions = new Map(); const streamableSessions = new Map(); const httpServer = http.createServer(async (req, res) => { @@ -106,7 +106,8 @@ export function startHttpTransport(server: Server, port: number, hostname: strin else await handleSSE(server, req, res, url, sseSessions); }); - httpServer.listen(port, hostname, () => { + const { host, port } = server.config.server; + httpServer.listen(port, host, () => { const address = httpServer.address(); assert(address, 'Could not bind server socket'); let url: string; diff --git a/tests/sse.spec.ts b/tests/sse.spec.ts index a164327..9e888a8 100644 --- a/tests/sse.spec.ts +++ b/tests/sse.spec.ts @@ -14,7 +14,9 @@ * 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 { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; @@ -22,24 +24,25 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/ 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: (args?: string[]) => Promise<{ url: URL, stderr: () => string }> }>({ +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 (args?: string[]) => { + 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'), - '--port=0', + ...(options?.noPort ? [] : ['--port=0']), '--user-data-dir=' + userDataDir, ...(mcpHeadless ? ['--headless'] : []), - ...(args || []), + ...(options?.args || []), ], { stdio: 'pipe', env: { @@ -71,8 +74,24 @@ test('sse transport', async ({ serverEndpoint }) => { await client.ping(); }); +test('sse 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 SSEClientTransport(url); + const client = new Client({ name: 'test', version: '1.0.0' }); + await client.connect(transport); + await client.ping(); +}); + test('sse transport browser lifecycle (isolated)', async ({ serverEndpoint, server }) => { - const { url, stderr } = await serverEndpoint(['--isolated']); + const { url, stderr } = await serverEndpoint({ args: ['--isolated'] }); const transport1 = new SSEClientTransport(url); const client1 = new Client({ name: 'test', version: '1.0.0' }); @@ -109,7 +128,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(['--isolated']); + const { url, stderr } = await serverEndpoint({ args: ['--isolated'] }); const transport1 = new SSEClientTransport(url); const client1 = new Client({ name: 'test', version: '1.0.0' });