fix(mcp-server): use explicit sdk js subpaths

This commit is contained in:
mastertyko 2026-04-11 18:10:03 +02:00
parent 23f1e72868
commit a335a87703
3 changed files with 26 additions and 53 deletions

View file

@ -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

View file

@ -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');
});
});

View file

@ -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')
})