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/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
+ }
@@ -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
+ }
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Transport layer — connect to upstream MCP servers via stdio or bridge (HTTP)
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, BridgeServerEntry } from './types.js';
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 bridge MCP server (StreamableHTTP → PersistentMcpBridge)
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
- * Bridge connections target long-lived MCP servers managed by PersistentMcpBridge
22
- * in the Electron main process, accessed via HTTP endpoints.
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 connectBridge(id: string, entry: BridgeServerEntry): Promise<ConnectedClient>;
25
- /** @deprecated Use connectBridge instead */
26
- export declare const connectHttp: typeof connectBridge;
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 bridge (HTTP)
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 bridge MCP server (StreamableHTTP → PersistentMcpBridge)
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
- * Bridge connections target long-lived MCP servers managed by PersistentMcpBridge
58
- * in the Electron main process, accessed via HTTP endpoints.
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 connectBridge(id, entry) {
61
- logInfo(`Connecting to "${id}" (bridge): ${entry.url}`);
62
- const transport = new StreamableHTTPClientTransport(new URL(entry.url));
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 connectBridge instead */
76
- export const connectHttp = connectBridge;
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
- * Bridge 类型: 连接持久化 MCP Bridge (HTTP)
11
+ * Streamable HTTP 类型: 连接远程 MCP server (Streamable HTTP)
12
12
  *
13
- * 用于连接 PersistentMcpBridge 等长生命周期的 MCP server
14
- * Bridge server 通过 StreamableHTTPClientTransport 访问,
15
- * 其生命周期独立于 proxy 进程。
13
+ * 用于连接 PersistentMcpBridge 等长生命周期的 MCP server
14
+ * 或直接连接支持 Streamable HTTP 协议的远程 MCP 服务。
16
15
  */
17
- export interface BridgeServerEntry {
16
+ export interface StreamableServerEntry {
18
17
  url: string;
18
+ transport?: 'streamable-http';
19
+ headers?: Record<string, string>;
20
+ authToken?: string;
19
21
  }
20
- /** @deprecated Use BridgeServerEntry instead */
21
- export type HttpServerEntry = BridgeServerEntry;
22
- export type McpServerEntry = StdioServerEntry | BridgeServerEntry;
23
- export declare function isBridgeEntry(entry: McpServerEntry): entry is BridgeServerEntry;
24
- /** @deprecated Use isBridgeEntry instead */
25
- export declare const isHttpEntry: typeof isBridgeEntry;
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 isBridgeEntry(entry) {
5
- return 'url' in entry && typeof entry.url === 'string';
4
+ export function isSseEntry(entry) {
5
+ return 'url' in entry && entry.transport === 'sse';
6
6
  }
7
- /** @deprecated Use isBridgeEntry instead */
8
- export const isHttpEntry = isBridgeEntry;
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.3.1",
4
- "description": "TypeScript stdio MCP proxy — aggregates multiple MCP servers (stdio + bridge) into one",
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"