diff --git a/src/mcp-server.ts b/src/mcp-server.ts index f7417235e..7486f60fa 100644 --- a/src/mcp-server.ts +++ b/src/mcp-server.ts @@ -19,10 +19,10 @@ export interface McpToolDef { // MCP SDK subpath imports use wildcard exports (./*) that NodeNext resolves // at runtime but TypeScript cannot statically type-check. We construct the // specifiers dynamically so tsc treats them as `any`. -// Use createRequire to resolve wildcard subpaths — CJS resolver auto-appends -// .js, which the ESM wildcard export map does not (#3603). -import { createRequire } from 'node:module' -const _require = createRequire(import.meta.url) +// +// Use explicit .js subpaths for modules that are loaded dynamically at runtime. +// Recent Node / SDK combinations do not reliably resolve the extensionless +// wildcard targets for `server/stdio` and `types` (#3914). const MCP_PKG = '@modelcontextprotocol/sdk' /** @@ -46,8 +46,8 @@ export async function startMcpServer(options: { const { tools, version = '0.0.0' } = options const serverMod = await import(`${MCP_PKG}/server`) - const stdioMod = await import(_require.resolve(`${MCP_PKG}/server/stdio`)) - const typesMod = await import(_require.resolve(`${MCP_PKG}/types`)) + const stdioMod = await import(`${MCP_PKG}/server/stdio.js`) + const typesMod = await import(`${MCP_PKG}/types.js`) const Server = serverMod.Server const StdioServerTransport = stdioMod.StdioServerTransport diff --git a/src/tests/mcp-createRequire.test.ts b/src/tests/mcp-createRequire.test.ts index 5f1292866..d16ebacd6 100644 --- a/src/tests/mcp-createRequire.test.ts +++ b/src/tests/mcp-createRequire.test.ts @@ -1,12 +1,9 @@ /** - * Regression test for #3603 — MCP server subpath imports via createRequire + * Regression test for #3914 — MCP server uses explicit .js SDK subpaths. * - * The ESM wildcard export map in @modelcontextprotocol/sdk does not resolve - * subpath imports correctly. The fix uses createRequire from node:module to - * resolve wildcard subpaths via the CJS resolver which auto-appends .js. - * - * Structural verification test — reads source to confirm createRequire import - * and _require.resolve usage exist. + * Extensionless wildcard exports for `server/stdio` and `types` do not resolve + * reliably across current Node / SDK combinations. The runtime import strings + * must include `.js`. */ import { describe, test } from 'node:test'; @@ -20,29 +17,19 @@ const __dirname = dirname(__filename); const source = readFileSync(join(__dirname, '..', 'mcp-server.ts'), 'utf-8'); -describe('MCP server createRequire subpath resolution (#3603)', () => { - test('createRequire is imported from node:module', () => { - assert.match(source, /import\s*\{\s*createRequire\s*\}\s*from\s*['"]node:module['"]/, - 'createRequire should be imported from node:module'); +describe('MCP server SDK subpath imports (#3914)', () => { + test('server/stdio import uses explicit .js subpath', () => { + assert.match(source, /await import\(`\$\{MCP_PKG\}\/server\/stdio\.js`\)/, + 'server/stdio import should include the .js suffix'); }); - test('_require is created from import.meta.url', () => { - assert.match(source, /createRequire\(import\.meta\.url\)/, - '_require should be created using createRequire(import.meta.url)'); + test('types import uses explicit .js subpath', () => { + assert.match(source, /await import\(`\$\{MCP_PKG\}\/types\.js`\)/, + 'types import should include the .js suffix'); }); - test('_require.resolve is used for subpath imports', () => { - assert.match(source, /_require\.resolve\(/, - '_require.resolve should be used for subpath resolution'); - }); - - test('server/stdio subpath uses _require.resolve', () => { - assert.match(source, /_require\.resolve\(`\$\{MCP_PKG\}\/server\/stdio`\)/, - 'server/stdio import should use _require.resolve'); - }); - - test('types subpath uses _require.resolve', () => { - assert.match(source, /_require\.resolve\(`\$\{MCP_PKG\}\/types`\)/, - 'types import should use _require.resolve'); + test('legacy createRequire-based resolution is gone', () => { + assert.doesNotMatch(source, /createRequire|_require\.resolve/, + 'legacy createRequire-based subpath resolution should not remain'); }); }); diff --git a/src/tests/mcp-server.test.ts b/src/tests/mcp-server.test.ts index 9581809dd..8a6a672f9 100644 --- a/src/tests/mcp-server.test.ts +++ b/src/tests/mcp-server.test.ts @@ -30,25 +30,11 @@ test('startMcpServer accepts the correct argument shape', async () => { assert.strictEqual(startMcpServer.length, 1, 'startMcpServer should accept one argument') }) -test('startMcpServer can be called with mock tools', async () => { - const { startMcpServer } = await import(distUrl('mcp-server.js')) +test('compiled MCP runtime dependencies resolve with explicit .js subpaths', async () => { + const stdioMod = await import('@modelcontextprotocol/sdk/server/stdio.js') + const typesMod = await import('@modelcontextprotocol/sdk/types.js') - // Create a mock tool matching the McpToolDef interface - const mockTool = { - name: 'test_tool', - description: 'A test tool', - parameters: { type: 'object', properties: {} }, - execute: async () => ({ - content: [{ type: 'text', text: 'hello' }], - }), - } - - // Verify the function can be called with the correct signature - // without throwing during argument validation. It will attempt to - // connect to stdin/stdout as an MCP transport, which won't work in - // a test environment, but the Server instance is created successfully. - assert.doesNotThrow(() => { - void startMcpServer({ tools: [mockTool], version: '0.0.0-test' }) - .catch(() => { /* expected: no MCP client on stdin */ }) - }) + assert.strictEqual(typeof stdioMod.StdioServerTransport, 'function') + assert.ok(typesMod.ListToolsRequestSchema, 'ListToolsRequestSchema should be exported') + assert.ok(typesMod.CallToolRequestSchema, 'CallToolRequestSchema should be exported') })