Merge pull request #17 from liuxiaopai-ai/fix/mcp-sse-http-transport

Code review approved: correct SDK type usage, backward compatible, comprehensive tests (11 cases).
This commit is contained in:
歸藏 authored and GitHub committed 2026-02-08 20:27:23 +08:00
commit 7d52a508a4
2 files changed
+326 -8

No files matched your search

+269
View File
@@ -0,0 +1,269 @@
/**
* Unit tests for MCP server config conversion (toSdkMcpConfig).
*
* Run with: npx tsx --test src/__tests__/unit/mcp-config.test.ts
*
* Tests verify that:
* 1. stdio transport configs are correctly converted
* 2. SSE transport configs include type, url, and headers
* 3. HTTP transport configs include type, url, and headers
* 4. Missing url for SSE/HTTP servers are skipped with warning
* 5. Missing command for stdio servers are skipped
* 6. Default transport type is stdio when not specified
* 7. Mixed transport types work together
*/
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
// We test toSdkMcpConfig indirectly by importing the module.
// Since toSdkMcpConfig is not exported, we test the behavior
// through the MCPServerConfig type contract and validate the
// conversion logic matches SDK expectations.
import type { MCPServerConfig } from '../../types';
// Re-implement the conversion logic for testing (mirrors claude-client.ts)
// This ensures our test validates the same logic without needing to
// export internal functions.
function toSdkMcpConfig(
servers: Record<string, MCPServerConfig>
): Record<string, unknown> {
const result: Record<string, unknown> = {};
for (const [name, config] of Object.entries(servers)) {
const transport = config.type || 'stdio';
switch (transport) {
case 'sse': {
if (!config.url) continue;
const sseConfig: Record<string, unknown> = {
type: 'sse',
url: config.url,
};
if (config.headers && Object.keys(config.headers).length > 0) {
sseConfig.headers = config.headers;
}
result[name] = sseConfig;
break;
}
case 'http': {
if (!config.url) continue;
const httpConfig: Record<string, unknown> = {
type: 'http',
url: config.url,
};
if (config.headers && Object.keys(config.headers).length > 0) {
httpConfig.headers = config.headers;
}
result[name] = httpConfig;
break;
}
case 'stdio':
default: {
if (!config.command) continue;
result[name] = {
command: config.command,
args: config.args,
env: config.env,
};
break;
}
}
}
return result;
}
describe('toSdkMcpConfig', () => {
it('should convert stdio servers correctly', () => {
const input: Record<string, MCPServerConfig> = {
'my-server': {
command: 'node',
args: ['./server.js'],
env: { API_KEY: 'test123' },
type: 'stdio',
},
};
const result = toSdkMcpConfig(input);
assert.deepEqual(result['my-server'], {
command: 'node',
args: ['./server.js'],
env: { API_KEY: 'test123' },
});
});
it('should default to stdio when type is not specified', () => {
const input: Record<string, MCPServerConfig> = {
'default-server': {
command: 'python',
args: ['server.py'],
},
};
const result = toSdkMcpConfig(input);
assert.deepEqual(result['default-server'], {
command: 'python',
args: ['server.py'],
env: undefined,
});
});
it('should convert SSE servers with type and url', () => {
const input: Record<string, MCPServerConfig> = {
'sse-server': {
command: '',
type: 'sse',
url: 'http://localhost:8080/sse',
},
};
const result = toSdkMcpConfig(input);
assert.deepEqual(result['sse-server'], {
type: 'sse',
url: 'http://localhost:8080/sse',
});
});
it('should include headers for SSE servers when provided', () => {
const input: Record<string, MCPServerConfig> = {
'sse-auth': {
command: '',
type: 'sse',
url: 'https://api.example.com/mcp/sse',
headers: { Authorization: 'Bearer token123' },
},
};
const result = toSdkMcpConfig(input) as Record<string, Record<string, unknown>>;
assert.equal(result['sse-auth'].type, 'sse');
assert.equal(result['sse-auth'].url, 'https://api.example.com/mcp/sse');
assert.deepEqual(result['sse-auth'].headers, { Authorization: 'Bearer token123' });
});
it('should convert HTTP servers with type and url', () => {
const input: Record<string, MCPServerConfig> = {
'http-server': {
command: '',
type: 'http',
url: 'http://localhost:9090/mcp',
},
};
const result = toSdkMcpConfig(input);
assert.deepEqual(result['http-server'], {
type: 'http',
url: 'http://localhost:9090/mcp',
});
});
it('should include headers for HTTP servers when provided', () => {
const input: Record<string, MCPServerConfig> = {
'http-auth': {
command: '',
type: 'http',
url: 'https://api.example.com/mcp',
headers: { 'X-API-Key': 'key123', 'Content-Type': 'application/json' },
},
};
const result = toSdkMcpConfig(input) as Record<string, Record<string, unknown>>;
assert.equal(result['http-auth'].type, 'http');
assert.equal(result['http-auth'].url, 'https://api.example.com/mcp');
assert.deepEqual(result['http-auth'].headers, {
'X-API-Key': 'key123',
'Content-Type': 'application/json',
});
});
it('should skip SSE servers without url', () => {
const input: Record<string, MCPServerConfig> = {
'broken-sse': {
command: '',
type: 'sse',
// url is missing
},
};
const result = toSdkMcpConfig(input);
assert.equal(result['broken-sse'], undefined);
});
it('should skip HTTP servers without url', () => {
const input: Record<string, MCPServerConfig> = {
'broken-http': {
command: '',
type: 'http',
// url is missing
},
};
const result = toSdkMcpConfig(input);
assert.equal(result['broken-http'], undefined);
});
it('should skip stdio servers without command', () => {
const input: Record<string, MCPServerConfig> = {
'broken-stdio': {
command: '',
type: 'stdio',
},
};
const result = toSdkMcpConfig(input);
assert.equal(result['broken-stdio'], undefined);
});
it('should handle mixed transport types', () => {
const input: Record<string, MCPServerConfig> = {
'local-tools': {
command: 'node',
args: ['tools.js'],
type: 'stdio',
},
'remote-sse': {
command: '',
type: 'sse',
url: 'http://remote:8080/sse',
},
'remote-http': {
command: '',
type: 'http',
url: 'http://remote:9090/mcp',
headers: { Authorization: 'Bearer xyz' },
},
};
const result = toSdkMcpConfig(input);
assert.equal(Object.keys(result).length, 3);
assert.deepEqual(result['local-tools'], {
command: 'node',
args: ['tools.js'],
env: undefined,
});
assert.deepEqual(result['remote-sse'], {
type: 'sse',
url: 'http://remote:8080/sse',
});
assert.deepEqual(result['remote-http'], {
type: 'http',
url: 'http://remote:9090/mcp',
headers: { Authorization: 'Bearer xyz' },
});
});
it('should not include empty headers object', () => {
const input: Record<string, MCPServerConfig> = {
'sse-no-headers': {
command: '',
type: 'sse',
url: 'http://localhost:8080/sse',
headers: {},
},
};
const result = toSdkMcpConfig(input) as Record<string, Record<string, unknown>>;
assert.equal(result['sse-no-headers'].headers, undefined);
});
});
+57 -8
View File
@@ -9,6 +9,9 @@ import type {
SDKToolProgressMessage,
Options,
McpStdioServerConfig,
McpSSEServerConfig,
McpHttpServerConfig,
McpServerConfig,
NotificationHookInput,
PostToolUseHookInput,
} from '@anthropic-ai/claude-agent-sdk';
@@ -28,18 +31,64 @@ function findClaudePath(): string | undefined {
}
/**
* Convert our MCPServerConfig to the SDK's McpStdioServerConfig format
* Convert our MCPServerConfig to the SDK's McpServerConfig format.
* Supports stdio, sse, and http transport types.
*/
function toSdkMcpConfig(
servers: Record<string, MCPServerConfig>
): Record<string, McpStdioServerConfig> {
const result: Record<string, McpStdioServerConfig> = {};
): Record<string, McpServerConfig> {
const result: Record<string, McpServerConfig> = {};
for (const [name, config] of Object.entries(servers)) {
result[name] = {
command: config.command,
args: config.args,
env: config.env,
};
const transport = config.type || 'stdio';
switch (transport) {
case 'sse': {
if (!config.url) {
console.warn(`[mcp] SSE server "${name}" is missing url, skipping`);
continue;
}
const sseConfig: McpSSEServerConfig = {
type: 'sse',
url: config.url,
};
if (config.headers && Object.keys(config.headers).length > 0) {
sseConfig.headers = config.headers;
}
result[name] = sseConfig;
break;
}
case 'http': {
if (!config.url) {
console.warn(`[mcp] HTTP server "${name}" is missing url, skipping`);
continue;
}
const httpConfig: McpHttpServerConfig = {
type: 'http',
url: config.url,
};
if (config.headers && Object.keys(config.headers).length > 0) {
httpConfig.headers = config.headers;
}
result[name] = httpConfig;
break;
}
case 'stdio':
default: {
if (!config.command) {
console.warn(`[mcp] stdio server "${name}" is missing command, skipping`);
continue;
}
const stdioConfig: McpStdioServerConfig = {
command: config.command,
args: config.args,
env: config.env,
};
result[name] = stdioConfig;
break;
}
}
}
return result;
}