nuwax-mcp-stdio-proxy 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bridge.d.ts +6 -1
- package/dist/bridge.js +8 -5
- package/dist/constants.d.ts +7 -0
- package/dist/constants.js +7 -0
- package/dist/detect.d.ts +14 -0
- package/dist/detect.js +95 -0
- package/dist/filter.d.ts +16 -0
- package/dist/filter.js +19 -0
- package/dist/index.d.ts +12 -15
- package/dist/index.js +5147 -2620
- package/dist/lib.d.ts +8 -2
- package/dist/lib.js +5 -1
- package/dist/modes/convert.d.ts +16 -0
- package/dist/modes/convert.js +117 -0
- package/dist/modes/proxy.d.ts +12 -0
- package/dist/modes/proxy.js +37 -0
- package/dist/modes/stdio.d.ts +8 -0
- package/dist/modes/stdio.js +95 -0
- package/dist/shared.d.ts +39 -0
- package/dist/shared.js +83 -0
- package/dist/transport.d.ts +21 -8
- package/dist/transport.js +50 -9
- package/dist/types.d.ts +29 -11
- package/dist/types.js +11 -4
- package/package.json +2 -2
package/dist/lib.d.ts
CHANGED
|
@@ -8,6 +8,12 @@ export { PersistentMcpBridge } from './bridge.js';
|
|
|
8
8
|
export type { BridgeLogger } from './bridge.js';
|
|
9
9
|
export { CustomStdioClientTransport } from './customStdio.js';
|
|
10
10
|
export type { CustomStdioServerParameters } from './customStdio.js';
|
|
11
|
-
export { buildBaseEnv, connectStdio, connectBridge } from './transport.js';
|
|
11
|
+
export { buildBaseEnv, buildRequestHeaders, connectStdio, connectStreamable, connectSse, connectBridge, } from './transport.js';
|
|
12
12
|
export type { ConnectedClient } from './transport.js';
|
|
13
|
-
export type { StdioServerEntry, BridgeServerEntry, McpServerEntry, McpServersConfig } from './types.js';
|
|
13
|
+
export type { StdioServerEntry, StreamableServerEntry, SseServerEntry, BridgeServerEntry, HttpServerEntry, McpServerEntry, McpServersConfig, } from './types.js';
|
|
14
|
+
export { isSseEntry, isStreamableEntry, isBridgeEntry } from './types.js';
|
|
15
|
+
export { filterTools } from './filter.js';
|
|
16
|
+
export type { ToolFilter } from './filter.js';
|
|
17
|
+
export { detectProtocol } from './detect.js';
|
|
18
|
+
export { discoverTools, createToolProxyServer, setupGracefulShutdown } from './shared.js';
|
|
19
|
+
export type { ToolResolver, ToolProxyServerOptions } from './shared.js';
|
package/dist/lib.js
CHANGED
|
@@ -6,4 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { PersistentMcpBridge } from './bridge.js';
|
|
8
8
|
export { CustomStdioClientTransport } from './customStdio.js';
|
|
9
|
-
export { buildBaseEnv, connectStdio, connectBridge } from './transport.js';
|
|
9
|
+
export { buildBaseEnv, buildRequestHeaders, connectStdio, connectStreamable, connectSse, connectBridge, } from './transport.js';
|
|
10
|
+
export { isSseEntry, isStreamableEntry, isBridgeEntry } from './types.js';
|
|
11
|
+
export { filterTools } from './filter.js';
|
|
12
|
+
export { detectProtocol } from './detect.js';
|
|
13
|
+
export { discoverTools, createToolProxyServer, setupGracefulShutdown } from './shared.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mode: convert (remote URL → stdio)
|
|
3
|
+
*
|
|
4
|
+
* Connects to a single remote MCP service (SSE or Streamable HTTP)
|
|
5
|
+
* and exposes it as a stdio MCP endpoint. Supports tool filtering.
|
|
6
|
+
*/
|
|
7
|
+
import type { McpServersConfig } from '../types.js';
|
|
8
|
+
export interface ConvertArgs {
|
|
9
|
+
url?: string;
|
|
10
|
+
config?: McpServersConfig;
|
|
11
|
+
name?: string;
|
|
12
|
+
protocol?: 'sse' | 'stream';
|
|
13
|
+
allowTools?: string[];
|
|
14
|
+
denyTools?: string[];
|
|
15
|
+
}
|
|
16
|
+
export declare function runConvert(args: ConvertArgs): Promise<void>;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mode: convert (remote URL → stdio)
|
|
3
|
+
*
|
|
4
|
+
* Connects to a single remote MCP service (SSE or Streamable HTTP)
|
|
5
|
+
* and exposes it as a stdio MCP endpoint. Supports tool filtering.
|
|
6
|
+
*/
|
|
7
|
+
import { logInfo, logWarn, logError } from '../logger.js';
|
|
8
|
+
import { connectStreamable, connectSse, buildRequestHeaders } from '../transport.js';
|
|
9
|
+
import { filterTools } from '../filter.js';
|
|
10
|
+
import { detectProtocol } from '../detect.js';
|
|
11
|
+
import { discoverTools, createToolProxyServer, setupGracefulShutdown } from '../shared.js';
|
|
12
|
+
export async function runConvert(args) {
|
|
13
|
+
// 1. Resolve the target URL and headers
|
|
14
|
+
let targetUrl;
|
|
15
|
+
let targetHeaders;
|
|
16
|
+
let protocolHint = args.protocol;
|
|
17
|
+
if (args.url) {
|
|
18
|
+
targetUrl = args.url;
|
|
19
|
+
}
|
|
20
|
+
else if (args.config) {
|
|
21
|
+
const serverEntries = Object.entries(args.config.mcpServers);
|
|
22
|
+
if (serverEntries.length === 0) {
|
|
23
|
+
logError('No servers found in config');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
// If --name specified, find that entry; otherwise use the first entry
|
|
27
|
+
let selected;
|
|
28
|
+
if (args.name) {
|
|
29
|
+
const found = serverEntries.find(([id]) => id === args.name);
|
|
30
|
+
if (!found) {
|
|
31
|
+
logError(`Server "${args.name}" not found in config. Available: ${serverEntries.map(([id]) => id).join(', ')}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
selected = found;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
if (serverEntries.length > 1) {
|
|
38
|
+
logWarn(`Multiple servers in config, using first: "${serverEntries[0][0]}". Use --name to select.`);
|
|
39
|
+
}
|
|
40
|
+
selected = serverEntries[0];
|
|
41
|
+
}
|
|
42
|
+
const [, entry] = selected;
|
|
43
|
+
if (!('url' in entry) || typeof entry.url !== 'string') {
|
|
44
|
+
logError('Selected server entry must have a "url" field for convert mode');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
targetUrl = entry.url;
|
|
48
|
+
// Extract headers/authToken from entry
|
|
49
|
+
const httpEntry = entry;
|
|
50
|
+
targetHeaders = buildRequestHeaders(httpEntry);
|
|
51
|
+
// If entry has explicit transport, use it as protocol hint
|
|
52
|
+
if ('transport' in entry && entry.transport === 'sse' && !protocolHint) {
|
|
53
|
+
protocolHint = 'sse';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
logError('Either URL or --config is required');
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
// 2. Detect protocol
|
|
61
|
+
let protocol = protocolHint;
|
|
62
|
+
if (!protocol) {
|
|
63
|
+
protocol = await detectProtocol(targetUrl, targetHeaders);
|
|
64
|
+
}
|
|
65
|
+
logInfo(`Connecting to ${targetUrl} via ${protocol === 'sse' ? 'SSE' : 'Streamable HTTP'}...`);
|
|
66
|
+
// 3. Connect to the remote server
|
|
67
|
+
const entryId = 'remote';
|
|
68
|
+
let connected;
|
|
69
|
+
if (protocol === 'sse') {
|
|
70
|
+
const sseEntry = { url: targetUrl, transport: 'sse' };
|
|
71
|
+
if (targetHeaders)
|
|
72
|
+
sseEntry.headers = targetHeaders;
|
|
73
|
+
connected = await connectSse(entryId, sseEntry);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const streamEntry = { url: targetUrl };
|
|
77
|
+
if (targetHeaders)
|
|
78
|
+
streamEntry.headers = targetHeaders;
|
|
79
|
+
connected = await connectStreamable(entryId, streamEntry);
|
|
80
|
+
}
|
|
81
|
+
const { client: remoteClient, cleanup } = connected;
|
|
82
|
+
// 4. Discover tools
|
|
83
|
+
const allTools = await discoverTools(remoteClient);
|
|
84
|
+
logInfo(`Remote server has ${allTools.length} tool(s)`);
|
|
85
|
+
// 5. Apply tool filtering
|
|
86
|
+
const toolFilter = {};
|
|
87
|
+
if (args.allowTools)
|
|
88
|
+
toolFilter.allowTools = new Set(args.allowTools);
|
|
89
|
+
if (args.denyTools)
|
|
90
|
+
toolFilter.denyTools = new Set(args.denyTools);
|
|
91
|
+
const filteredTools = filterTools(allTools, toolFilter);
|
|
92
|
+
const filteredNames = new Set(filteredTools.map((t) => t.name));
|
|
93
|
+
if (filteredTools.length !== allTools.length) {
|
|
94
|
+
logInfo(`After filtering: ${filteredTools.length} tool(s)`);
|
|
95
|
+
}
|
|
96
|
+
// 6. Create stdio MCP server that proxies to the remote client
|
|
97
|
+
const { server } = await createToolProxyServer({
|
|
98
|
+
tools: filteredTools,
|
|
99
|
+
resolveClient: (name) => filteredNames.has(name) ? remoteClient : undefined,
|
|
100
|
+
});
|
|
101
|
+
logInfo('Convert proxy running on stdio');
|
|
102
|
+
// Graceful shutdown
|
|
103
|
+
setupGracefulShutdown(async () => {
|
|
104
|
+
try {
|
|
105
|
+
await remoteClient.close();
|
|
106
|
+
}
|
|
107
|
+
catch { /* ignore */ }
|
|
108
|
+
try {
|
|
109
|
+
await cleanup();
|
|
110
|
+
}
|
|
111
|
+
catch { /* ignore */ }
|
|
112
|
+
try {
|
|
113
|
+
await server.close();
|
|
114
|
+
}
|
|
115
|
+
catch { /* ignore */ }
|
|
116
|
+
});
|
|
117
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mode: proxy (Streamable HTTP server)
|
|
3
|
+
*
|
|
4
|
+
* Starts PersistentMcpBridge as a Streamable HTTP server,
|
|
5
|
+
* exposing stdio MCP servers over HTTP on a specified port.
|
|
6
|
+
*/
|
|
7
|
+
import type { McpServersConfig } from '../types.js';
|
|
8
|
+
export interface ProxyArgs {
|
|
9
|
+
port: number;
|
|
10
|
+
config: McpServersConfig;
|
|
11
|
+
}
|
|
12
|
+
export declare function runProxy(args: ProxyArgs): Promise<void>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mode: proxy (Streamable HTTP server)
|
|
3
|
+
*
|
|
4
|
+
* Starts PersistentMcpBridge as a Streamable HTTP server,
|
|
5
|
+
* exposing stdio MCP servers over HTTP on a specified port.
|
|
6
|
+
*/
|
|
7
|
+
import { logInfo, logWarn, logError } from '../logger.js';
|
|
8
|
+
import { PersistentMcpBridge } from '../bridge.js';
|
|
9
|
+
import { setupGracefulShutdown } from '../shared.js';
|
|
10
|
+
export async function runProxy(args) {
|
|
11
|
+
const { port, config } = args;
|
|
12
|
+
// Filter to only stdio entries for PersistentMcpBridge
|
|
13
|
+
const stdioEntries = {};
|
|
14
|
+
for (const [id, entry] of Object.entries(config.mcpServers)) {
|
|
15
|
+
if (!('url' in entry)) {
|
|
16
|
+
stdioEntries[id] = entry;
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
logWarn(`Skipping non-stdio server "${id}" in proxy mode (only stdio servers supported)`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (Object.keys(stdioEntries).length === 0) {
|
|
23
|
+
logError('No stdio servers found in config for proxy mode');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
logInfo(`Starting proxy HTTP server on port ${port} with ${Object.keys(stdioEntries).length} server(s)...`);
|
|
27
|
+
const bridge = new PersistentMcpBridge({
|
|
28
|
+
info: (...a) => logInfo(a.map(String).join(' ')),
|
|
29
|
+
warn: (...a) => logWarn(a.map(String).join(' ')),
|
|
30
|
+
error: (...a) => logError(a.map(String).join(' ')),
|
|
31
|
+
});
|
|
32
|
+
await bridge.start(stdioEntries, { port });
|
|
33
|
+
logInfo(`Proxy HTTP server running on port ${port}`);
|
|
34
|
+
setupGracefulShutdown(async () => {
|
|
35
|
+
await bridge.stop();
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mode: stdio aggregation
|
|
3
|
+
*
|
|
4
|
+
* Aggregates multiple MCP servers (stdio + streamable-http + SSE)
|
|
5
|
+
* into a single stdio MCP endpoint.
|
|
6
|
+
*/
|
|
7
|
+
import type { McpServersConfig } from '../types.js';
|
|
8
|
+
export declare function runStdio(config: McpServersConfig): Promise<void>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mode: stdio aggregation
|
|
3
|
+
*
|
|
4
|
+
* Aggregates multiple MCP servers (stdio + streamable-http + SSE)
|
|
5
|
+
* into a single stdio MCP endpoint.
|
|
6
|
+
*/
|
|
7
|
+
import { isSseEntry, isStreamableEntry } from '../types.js';
|
|
8
|
+
import { logInfo, logWarn, logError } from '../logger.js';
|
|
9
|
+
import { buildBaseEnv, connectStdio, connectStreamable, connectSse } from '../transport.js';
|
|
10
|
+
import { discoverTools, createToolProxyServer, setupGracefulShutdown } from '../shared.js';
|
|
11
|
+
export async function runStdio(config) {
|
|
12
|
+
const entries = Object.entries(config.mcpServers);
|
|
13
|
+
if (entries.length === 0) {
|
|
14
|
+
logError('No MCP servers configured in mcpServers');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
logInfo(`Starting proxy with ${entries.length} server(s): ${entries.map(([id]) => id).join(', ')}`);
|
|
18
|
+
// ---- Phase 1: Connect to all MCP servers (stdio + streamable + sse) ----
|
|
19
|
+
const baseEnv = buildBaseEnv();
|
|
20
|
+
const clients = new Map();
|
|
21
|
+
const cleanups = new Map();
|
|
22
|
+
const toolToClient = new Map();
|
|
23
|
+
const toolToServer = new Map();
|
|
24
|
+
const toolsByName = new Map();
|
|
25
|
+
for (const [id, entry] of entries) {
|
|
26
|
+
try {
|
|
27
|
+
let connected;
|
|
28
|
+
if (isSseEntry(entry)) {
|
|
29
|
+
connected = await connectSse(id, entry);
|
|
30
|
+
}
|
|
31
|
+
else if (isStreamableEntry(entry)) {
|
|
32
|
+
connected = await connectStreamable(id, entry);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
connected = await connectStdio(id, entry, baseEnv);
|
|
36
|
+
}
|
|
37
|
+
const { client, cleanup } = connected;
|
|
38
|
+
clients.set(id, client);
|
|
39
|
+
cleanups.set(id, cleanup);
|
|
40
|
+
const allServerTools = await discoverTools(client);
|
|
41
|
+
logInfo(`Server "${id}": ${allServerTools.length} tool(s)${allServerTools.length > 0 ? ' — ' + allServerTools.map((t) => t.name).join(', ') : ''}`);
|
|
42
|
+
for (const tool of allServerTools) {
|
|
43
|
+
if (toolToClient.has(tool.name)) {
|
|
44
|
+
logWarn(`Tool "${tool.name}" from "${id}" shadows existing tool from "${toolToServer.get(tool.name)}"`);
|
|
45
|
+
}
|
|
46
|
+
toolToClient.set(tool.name, client);
|
|
47
|
+
toolToServer.set(tool.name, id);
|
|
48
|
+
toolsByName.set(tool.name, tool);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
logError(`Failed to connect to server "${id}": ${e}`);
|
|
53
|
+
// Continue with remaining servers — partial startup is acceptable
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (clients.size === 0) {
|
|
57
|
+
logError('Failed to connect to any MCP server');
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
const aggregatedTools = Array.from(toolsByName.values());
|
|
61
|
+
logInfo(`Aggregated ${aggregatedTools.length} unique tool(s) from ${clients.size} server(s)`);
|
|
62
|
+
// ---- Phase 2: Create the aggregating MCP server ----
|
|
63
|
+
const { server } = await createToolProxyServer({
|
|
64
|
+
tools: aggregatedTools,
|
|
65
|
+
resolveClient: (name) => toolToClient.get(name),
|
|
66
|
+
errorLabel: (name) => `"${name}" (server: "${toolToServer.get(name) || 'unknown'}")`,
|
|
67
|
+
});
|
|
68
|
+
logInfo('Proxy server running on stdio');
|
|
69
|
+
// ---- Graceful shutdown ----
|
|
70
|
+
setupGracefulShutdown(async () => {
|
|
71
|
+
for (const [id, client] of clients) {
|
|
72
|
+
try {
|
|
73
|
+
await client.close();
|
|
74
|
+
logInfo(`Closed client "${id}"`);
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
logError(`Failed to close client "${id}": ${e}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for (const [id, cleanup] of cleanups) {
|
|
81
|
+
try {
|
|
82
|
+
await cleanup();
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
logError(`Failed cleanup for "${id}": ${e}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
await server.close();
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// Ignore close errors during shutdown
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
package/dist/shared.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers — deduplicated logic used across modes
|
|
3
|
+
*/
|
|
4
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
5
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
7
|
+
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Discover all tools from a connected MCP client, handling pagination.
|
|
10
|
+
*/
|
|
11
|
+
export declare function discoverTools(client: Client): Promise<Tool[]>;
|
|
12
|
+
/**
|
|
13
|
+
* Callback that resolves a tool name to the client that owns it.
|
|
14
|
+
* Returns undefined if the tool is unknown or filtered.
|
|
15
|
+
*/
|
|
16
|
+
export type ToolResolver = (toolName: string) => Client | undefined;
|
|
17
|
+
export interface ToolProxyServerOptions {
|
|
18
|
+
/** Tools to expose via ListTools */
|
|
19
|
+
tools: Tool[];
|
|
20
|
+
/** Resolves a tool name to the upstream client */
|
|
21
|
+
resolveClient: ToolResolver;
|
|
22
|
+
/** Optional label for error logging (e.g. server name) */
|
|
23
|
+
errorLabel?: (toolName: string) => string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Create a stdio MCP server that proxies tool calls to upstream clients.
|
|
27
|
+
*
|
|
28
|
+
* Sets up Server with ListTools + CallTool handlers, connects to a
|
|
29
|
+
* StdioServerTransport, and returns both for lifecycle management.
|
|
30
|
+
*/
|
|
31
|
+
export declare function createToolProxyServer(opts: ToolProxyServerOptions): Promise<{
|
|
32
|
+
server: Server;
|
|
33
|
+
transport: StdioServerTransport;
|
|
34
|
+
}>;
|
|
35
|
+
/**
|
|
36
|
+
* Register SIGINT/SIGTERM handlers that run a cleanup function once,
|
|
37
|
+
* then exit. Guards against double-invocation.
|
|
38
|
+
*/
|
|
39
|
+
export declare function setupGracefulShutdown(cleanupFn: () => Promise<void>): void;
|
package/dist/shared.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers — deduplicated logic used across modes
|
|
3
|
+
*/
|
|
4
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
5
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
|
+
import { ListToolsRequestSchema, CallToolRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
import { logInfo, logError } from './logger.js';
|
|
8
|
+
import { PKG_NAME, PKG_VERSION } from './constants.js';
|
|
9
|
+
// ========== Tool Discovery ==========
|
|
10
|
+
/**
|
|
11
|
+
* Discover all tools from a connected MCP client, handling pagination.
|
|
12
|
+
*/
|
|
13
|
+
export async function discoverTools(client) {
|
|
14
|
+
const tools = [];
|
|
15
|
+
let cursor;
|
|
16
|
+
do {
|
|
17
|
+
const page = await client.listTools(cursor ? { cursor } : undefined);
|
|
18
|
+
tools.push(...page.tools);
|
|
19
|
+
cursor = page.nextCursor;
|
|
20
|
+
} while (cursor);
|
|
21
|
+
return tools;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Create a stdio MCP server that proxies tool calls to upstream clients.
|
|
25
|
+
*
|
|
26
|
+
* Sets up Server with ListTools + CallTool handlers, connects to a
|
|
27
|
+
* StdioServerTransport, and returns both for lifecycle management.
|
|
28
|
+
*/
|
|
29
|
+
export async function createToolProxyServer(opts) {
|
|
30
|
+
const { tools, resolveClient, errorLabel } = opts;
|
|
31
|
+
const server = new Server({ name: PKG_NAME, version: PKG_VERSION }, { capabilities: { tools: {} } });
|
|
32
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
33
|
+
return { tools };
|
|
34
|
+
});
|
|
35
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
36
|
+
const { name, arguments: toolArgs } = request.params;
|
|
37
|
+
const client = resolveClient(name);
|
|
38
|
+
if (!client) {
|
|
39
|
+
return {
|
|
40
|
+
content: [{ type: 'text', text: `Unknown tool: "${name}"` }],
|
|
41
|
+
isError: true,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const result = await client.callTool({ name, arguments: toolArgs });
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
const label = errorLabel ? errorLabel(name) : `"${name}"`;
|
|
50
|
+
logError(`Tool ${label} call failed: ${e}`);
|
|
51
|
+
return {
|
|
52
|
+
content: [{ type: 'text', text: `Tool call failed: ${e}` }],
|
|
53
|
+
isError: true,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
const transport = new StdioServerTransport();
|
|
58
|
+
await server.connect(transport);
|
|
59
|
+
return { server, transport };
|
|
60
|
+
}
|
|
61
|
+
// ========== Graceful Shutdown ==========
|
|
62
|
+
/**
|
|
63
|
+
* Register SIGINT/SIGTERM handlers that run a cleanup function once,
|
|
64
|
+
* then exit. Guards against double-invocation.
|
|
65
|
+
*/
|
|
66
|
+
export function setupGracefulShutdown(cleanupFn) {
|
|
67
|
+
let isShuttingDown = false;
|
|
68
|
+
const shutdown = async (signal) => {
|
|
69
|
+
if (isShuttingDown)
|
|
70
|
+
return;
|
|
71
|
+
isShuttingDown = true;
|
|
72
|
+
logInfo(`Received ${signal}, shutting down...`);
|
|
73
|
+
try {
|
|
74
|
+
await cleanupFn();
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
logError(`Shutdown cleanup error: ${e}`);
|
|
78
|
+
}
|
|
79
|
+
process.exit(0);
|
|
80
|
+
};
|
|
81
|
+
process.on('SIGINT', () => void shutdown('SIGINT'));
|
|
82
|
+
process.on('SIGTERM', () => void shutdown('SIGTERM'));
|
|
83
|
+
}
|
package/dist/transport.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Transport layer — connect to upstream MCP servers via stdio or
|
|
2
|
+
* Transport layer — connect to upstream MCP servers via stdio, Streamable HTTP, or SSE
|
|
3
3
|
*/
|
|
4
4
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
5
|
-
import type { StdioServerEntry,
|
|
5
|
+
import type { StdioServerEntry, StreamableServerEntry, SseServerEntry } from './types.js';
|
|
6
6
|
export interface ConnectedClient {
|
|
7
7
|
client: Client;
|
|
8
8
|
cleanup: () => Promise<void>;
|
|
@@ -11,16 +11,29 @@ export interface ConnectedClient {
|
|
|
11
11
|
* Build a clean env for child processes (strips ELECTRON_RUN_AS_NODE)
|
|
12
12
|
*/
|
|
13
13
|
export declare function buildBaseEnv(): Record<string, string>;
|
|
14
|
+
/**
|
|
15
|
+
* Build HTTP headers from entry config (merge headers + authToken)
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildRequestHeaders(entry: StreamableServerEntry | SseServerEntry): Record<string, string> | undefined;
|
|
14
18
|
/**
|
|
15
19
|
* Connect to a stdio MCP server (spawn child process)
|
|
16
20
|
*/
|
|
17
21
|
export declare function connectStdio(id: string, entry: StdioServerEntry, baseEnv: Record<string, string>): Promise<ConnectedClient>;
|
|
18
22
|
/**
|
|
19
|
-
* Connect to a
|
|
23
|
+
* Connect to a Streamable HTTP MCP server
|
|
24
|
+
*
|
|
25
|
+
* Supports PersistentMcpBridge endpoints and any remote MCP service using
|
|
26
|
+
* the Streamable HTTP transport protocol.
|
|
27
|
+
*/
|
|
28
|
+
export declare function connectStreamable(id: string, entry: StreamableServerEntry): Promise<ConnectedClient>;
|
|
29
|
+
/**
|
|
30
|
+
* Connect to an SSE MCP server
|
|
20
31
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
32
|
+
* Uses the legacy SSE (Server-Sent Events) transport for MCP servers
|
|
33
|
+
* that don't support the newer Streamable HTTP protocol.
|
|
23
34
|
*/
|
|
24
|
-
export declare function
|
|
25
|
-
/** @deprecated Use
|
|
26
|
-
export declare const
|
|
35
|
+
export declare function connectSse(id: string, entry: SseServerEntry): Promise<ConnectedClient>;
|
|
36
|
+
/** @deprecated Use connectStreamable instead */
|
|
37
|
+
export declare const connectBridge: typeof connectStreamable;
|
|
38
|
+
/** @deprecated Use connectStreamable instead */
|
|
39
|
+
export declare const connectHttp: typeof connectStreamable;
|
package/dist/transport.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Transport layer — connect to upstream MCP servers via stdio or
|
|
2
|
+
* Transport layer — connect to upstream MCP servers via stdio, Streamable HTTP, or SSE
|
|
3
3
|
*/
|
|
4
4
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
5
5
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
6
|
+
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
6
7
|
import { CustomStdioClientTransport } from './customStdio.js';
|
|
7
8
|
import { logInfo } from './logger.js';
|
|
8
9
|
/**
|
|
@@ -19,6 +20,19 @@ export function buildBaseEnv() {
|
|
|
19
20
|
delete env.ELECTRON_RUN_AS_NODE;
|
|
20
21
|
return env;
|
|
21
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Build HTTP headers from entry config (merge headers + authToken)
|
|
25
|
+
*/
|
|
26
|
+
export function buildRequestHeaders(entry) {
|
|
27
|
+
const headers = {};
|
|
28
|
+
if (entry.headers) {
|
|
29
|
+
Object.assign(headers, entry.headers);
|
|
30
|
+
}
|
|
31
|
+
if (entry.authToken) {
|
|
32
|
+
headers['Authorization'] = `Bearer ${entry.authToken}`;
|
|
33
|
+
}
|
|
34
|
+
return Object.keys(headers).length > 0 ? headers : undefined;
|
|
35
|
+
}
|
|
22
36
|
/**
|
|
23
37
|
* Connect to a stdio MCP server (spawn child process)
|
|
24
38
|
*/
|
|
@@ -52,14 +66,39 @@ export async function connectStdio(id, entry, baseEnv) {
|
|
|
52
66
|
};
|
|
53
67
|
}
|
|
54
68
|
/**
|
|
55
|
-
* Connect to a
|
|
69
|
+
* Connect to a Streamable HTTP MCP server
|
|
70
|
+
*
|
|
71
|
+
* Supports PersistentMcpBridge endpoints and any remote MCP service using
|
|
72
|
+
* the Streamable HTTP transport protocol.
|
|
73
|
+
*/
|
|
74
|
+
export async function connectStreamable(id, entry) {
|
|
75
|
+
logInfo(`Connecting to "${id}" (streamable-http): ${entry.url}`);
|
|
76
|
+
const headers = buildRequestHeaders(entry);
|
|
77
|
+
const url = new URL(entry.url);
|
|
78
|
+
const transport = new StreamableHTTPClientTransport(url, headers ? { requestInit: { headers } } : undefined);
|
|
79
|
+
const client = new Client({ name: `proxy-${id}`, version: '1.0.0' });
|
|
80
|
+
await client.connect(transport);
|
|
81
|
+
return {
|
|
82
|
+
client,
|
|
83
|
+
cleanup: async () => {
|
|
84
|
+
try {
|
|
85
|
+
await transport.close();
|
|
86
|
+
}
|
|
87
|
+
catch { /* ignore */ }
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Connect to an SSE MCP server
|
|
56
93
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
94
|
+
* Uses the legacy SSE (Server-Sent Events) transport for MCP servers
|
|
95
|
+
* that don't support the newer Streamable HTTP protocol.
|
|
59
96
|
*/
|
|
60
|
-
export async function
|
|
61
|
-
logInfo(`Connecting to "${id}" (
|
|
62
|
-
const
|
|
97
|
+
export async function connectSse(id, entry) {
|
|
98
|
+
logInfo(`Connecting to "${id}" (sse): ${entry.url}`);
|
|
99
|
+
const headers = buildRequestHeaders(entry);
|
|
100
|
+
const url = new URL(entry.url);
|
|
101
|
+
const transport = new SSEClientTransport(url, headers ? { requestInit: { headers } } : undefined);
|
|
63
102
|
const client = new Client({ name: `proxy-${id}`, version: '1.0.0' });
|
|
64
103
|
await client.connect(transport);
|
|
65
104
|
return {
|
|
@@ -72,5 +111,7 @@ export async function connectBridge(id, entry) {
|
|
|
72
111
|
},
|
|
73
112
|
};
|
|
74
113
|
}
|
|
75
|
-
/** @deprecated Use
|
|
76
|
-
export const
|
|
114
|
+
/** @deprecated Use connectStreamable instead */
|
|
115
|
+
export const connectBridge = connectStreamable;
|
|
116
|
+
/** @deprecated Use connectStreamable instead */
|
|
117
|
+
export const connectHttp = connectStreamable;
|
package/dist/types.d.ts
CHANGED
|
@@ -8,21 +8,39 @@ export interface StdioServerEntry {
|
|
|
8
8
|
env?: Record<string, string>;
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
11
|
+
* Streamable HTTP 类型: 连接远程 MCP server (Streamable HTTP)
|
|
12
12
|
*
|
|
13
|
-
* 用于连接 PersistentMcpBridge 等长生命周期的 MCP server
|
|
14
|
-
*
|
|
15
|
-
* 其生命周期独立于 proxy 进程。
|
|
13
|
+
* 用于连接 PersistentMcpBridge 等长生命周期的 MCP server,
|
|
14
|
+
* 或直接连接支持 Streamable HTTP 协议的远程 MCP 服务。
|
|
16
15
|
*/
|
|
17
|
-
export interface
|
|
16
|
+
export interface StreamableServerEntry {
|
|
18
17
|
url: string;
|
|
18
|
+
transport?: 'streamable-http';
|
|
19
|
+
headers?: Record<string, string>;
|
|
20
|
+
authToken?: string;
|
|
19
21
|
}
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
export
|
|
22
|
+
/**
|
|
23
|
+
* SSE 类型: 连接远程 MCP server (Server-Sent Events)
|
|
24
|
+
*
|
|
25
|
+
* 用于连接支持 SSE 传输的远程 MCP 服务(旧版 MCP 协议)。
|
|
26
|
+
*/
|
|
27
|
+
export interface SseServerEntry {
|
|
28
|
+
url: string;
|
|
29
|
+
transport: 'sse';
|
|
30
|
+
headers?: Record<string, string>;
|
|
31
|
+
authToken?: string;
|
|
32
|
+
}
|
|
33
|
+
export type McpServerEntry = StdioServerEntry | StreamableServerEntry | SseServerEntry;
|
|
34
|
+
export declare function isSseEntry(entry: McpServerEntry): entry is SseServerEntry;
|
|
35
|
+
export declare function isStreamableEntry(entry: McpServerEntry): entry is StreamableServerEntry;
|
|
36
|
+
/** @deprecated Use StreamableServerEntry instead */
|
|
37
|
+
export type BridgeServerEntry = StreamableServerEntry;
|
|
38
|
+
/** @deprecated Use StreamableServerEntry instead */
|
|
39
|
+
export type HttpServerEntry = StreamableServerEntry;
|
|
40
|
+
/** @deprecated Use isStreamableEntry instead */
|
|
41
|
+
export declare const isBridgeEntry: typeof isStreamableEntry;
|
|
42
|
+
/** @deprecated Use isStreamableEntry instead */
|
|
43
|
+
export declare const isHttpEntry: typeof isStreamableEntry;
|
|
26
44
|
export interface McpServersConfig {
|
|
27
45
|
mcpServers: Record<string, McpServerEntry>;
|
|
28
46
|
}
|
package/dist/types.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Types for MCP server configuration
|
|
3
3
|
*/
|
|
4
|
-
export function
|
|
5
|
-
return 'url' in entry &&
|
|
4
|
+
export function isSseEntry(entry) {
|
|
5
|
+
return 'url' in entry && entry.transport === 'sse';
|
|
6
6
|
}
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
export function isStreamableEntry(entry) {
|
|
8
|
+
return ('url' in entry &&
|
|
9
|
+
typeof entry.url === 'string' &&
|
|
10
|
+
!isSseEntry(entry));
|
|
11
|
+
}
|
|
12
|
+
/** @deprecated Use isStreamableEntry instead */
|
|
13
|
+
export const isBridgeEntry = isStreamableEntry;
|
|
14
|
+
/** @deprecated Use isStreamableEntry instead */
|
|
15
|
+
export const isHttpEntry = isStreamableEntry;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuwax-mcp-stdio-proxy",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "TypeScript
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "TypeScript MCP proxy — aggregates multiple MCP servers (stdio + streamable-http + SSE) with convert & proxy modes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"nuwax-mcp-stdio-proxy": "./dist/index.js"
|