@aiwerk/mcp-bridge 1.0.0 → 1.0.2
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/bin/mcp-bridge.d.ts +2 -0
- package/dist/bin/mcp-bridge.js +320 -0
- package/dist/src/config.d.ts +19 -0
- package/dist/src/config.js +145 -0
- package/{src/index.ts → dist/src/index.d.ts} +1 -30
- package/dist/src/index.js +21 -0
- package/dist/src/mcp-router.d.ts +65 -0
- package/dist/src/mcp-router.js +271 -0
- package/dist/src/protocol.d.ts +4 -0
- package/dist/src/protocol.js +58 -0
- package/dist/src/schema-convert.d.ts +11 -0
- package/dist/src/schema-convert.js +150 -0
- package/dist/src/standalone-server.d.ts +30 -0
- package/dist/src/standalone-server.js +312 -0
- package/dist/src/tool-naming.d.ts +3 -0
- package/dist/src/tool-naming.js +38 -0
- package/dist/src/transport-base.d.ts +76 -0
- package/dist/src/transport-base.js +163 -0
- package/dist/src/transport-sse.d.ts +16 -0
- package/dist/src/transport-sse.js +207 -0
- package/dist/src/transport-stdio.d.ts +20 -0
- package/dist/src/transport-stdio.js +281 -0
- package/dist/src/transport-streamable-http.d.ts +11 -0
- package/dist/src/transport-streamable-http.js +164 -0
- package/dist/src/types.d.ts +72 -0
- package/dist/src/types.js +4 -0
- package/dist/src/update-checker.d.ts +25 -0
- package/dist/src/update-checker.js +132 -0
- package/package.json +19 -4
- package/scripts/install-server.ps1 +25 -58
- package/scripts/install-server.sh +37 -90
- package/servers/apify/README.md +6 -6
- package/servers/github/README.md +6 -6
- package/servers/google-maps/README.md +6 -6
- package/servers/hetzner/README.md +6 -6
- package/servers/hostinger/README.md +6 -6
- package/servers/linear/README.md +6 -6
- package/servers/miro/README.md +6 -6
- package/servers/notion/README.md +6 -6
- package/servers/stripe/README.md +6 -6
- package/servers/tavily/README.md +6 -6
- package/servers/todoist/README.md +6 -6
- package/servers/wise/README.md +6 -6
- package/bin/mcp-bridge.js +0 -9
- package/bin/mcp-bridge.ts +0 -335
- package/src/config.ts +0 -168
- package/src/mcp-router.ts +0 -366
- package/src/protocol.ts +0 -69
- package/src/schema-convert.ts +0 -178
- package/src/standalone-server.ts +0 -385
- package/src/tool-naming.ts +0 -51
- package/src/transport-base.ts +0 -199
- package/src/transport-sse.ts +0 -230
- package/src/transport-stdio.ts +0 -312
- package/src/transport-streamable-http.ts +0 -188
- package/src/types.ts +0 -88
- package/src/update-checker.ts +0 -155
- package/tests/collision.test.ts +0 -60
- package/tests/env-resolve.test.ts +0 -68
- package/tests/mcp-router.test.ts +0 -301
- package/tests/schema-convert.test.ts +0 -70
- package/tests/transport-base.test.ts +0 -214
- package/tsconfig.json +0 -15
package/src/schema-convert.ts
DELETED
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
// TSchema is intentionally loose — TypeBox returns dynamic types
|
|
2
|
-
// that don't have a single static interface
|
|
3
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4
|
-
type TSchema = any;
|
|
5
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
|
-
type TypeBoxMod = { Type: any } | null;
|
|
7
|
-
|
|
8
|
-
let cachedTypeBoxPromise: Promise<TypeBoxMod> | null = null;
|
|
9
|
-
|
|
10
|
-
// Overridable loader for dependency injection (used by tests)
|
|
11
|
-
let typeBoxLoader: (() => Promise<TypeBoxMod>) | null = null;
|
|
12
|
-
|
|
13
|
-
export function setTypeBoxLoader(loader: (() => Promise<TypeBoxMod>) | null): void {
|
|
14
|
-
typeBoxLoader = loader;
|
|
15
|
-
cachedTypeBoxPromise = null; // auto-reset cache
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async function getTypeBox(): Promise<TypeBoxMod> {
|
|
19
|
-
// If a test loader is set, always use it (bypass cache)
|
|
20
|
-
if (typeBoxLoader) {
|
|
21
|
-
return typeBoxLoader();
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (cachedTypeBoxPromise) {
|
|
25
|
-
return cachedTypeBoxPromise;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
cachedTypeBoxPromise = (async () => {
|
|
29
|
-
try {
|
|
30
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
31
|
-
const mod: any = await import("@sinclair/typebox");
|
|
32
|
-
const Type = mod?.Type ?? mod?.default?.Type;
|
|
33
|
-
if (!Type) {
|
|
34
|
-
throw new Error("TypeBox module missing Type export");
|
|
35
|
-
}
|
|
36
|
-
return { Type };
|
|
37
|
-
} catch (error) {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
})();
|
|
41
|
-
|
|
42
|
-
return cachedTypeBoxPromise;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
async function anyFallback(): Promise<TSchema> {
|
|
48
|
-
const typeBox = await getTypeBox();
|
|
49
|
-
if (typeBox?.Type) {
|
|
50
|
-
return typeBox.Type.Any();
|
|
51
|
-
}
|
|
52
|
-
// Empty schema {} is valid JSON Schema (matches anything), unlike { type: "any" } which is invalid.
|
|
53
|
-
// This only triggers if @sinclair/typebox is missing — run `npm install` in the plugin directory.
|
|
54
|
-
schemaLogger.warn("[mcp-bridge] TypeBox unavailable — using permissive empty schema fallback. Run `npm install` to fix.");
|
|
55
|
-
return {};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Logger can be injected via setSchemaLogger(); defaults to console
|
|
59
|
-
let schemaLogger: { warn: (...args: unknown[]) => void } = console;
|
|
60
|
-
export function setSchemaLogger(logger: { warn: (...args: unknown[]) => void }): void {
|
|
61
|
-
schemaLogger = logger;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
65
|
-
export async function convertJsonSchemaToTypeBox(schema: any, depth = 0): Promise<TSchema> {
|
|
66
|
-
const typeBox = await getTypeBox();
|
|
67
|
-
const Type = typeBox?.Type;
|
|
68
|
-
if (!Type) {
|
|
69
|
-
return anyFallback();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
if (depth > 10) {
|
|
74
|
-
schemaLogger.warn("[mcp-bridge] JSON schema depth limit exceeded (>10), falling back to Type.Any()");
|
|
75
|
-
return Type.Any();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (!schema || typeof schema !== "object") {
|
|
79
|
-
return Type.Any();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// anyOf and oneOf both map to Type.Union (TypeBox doesn't distinguish)
|
|
83
|
-
const unionSource = schema.anyOf || schema.oneOf;
|
|
84
|
-
if (Array.isArray(unionSource) && unionSource.length > 0) {
|
|
85
|
-
const variants = await Promise.all(
|
|
86
|
-
unionSource.map((item: Record<string, unknown>) => convertJsonSchemaToTypeBox(item, depth + 1))
|
|
87
|
-
);
|
|
88
|
-
return Type.Union(variants);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// allOf maps to Type.Intersect
|
|
92
|
-
if (Array.isArray(schema.allOf) && schema.allOf.length > 0) {
|
|
93
|
-
const parts = await Promise.all(
|
|
94
|
-
schema.allOf.map((item: Record<string, unknown>) => convertJsonSchemaToTypeBox(item, depth + 1))
|
|
95
|
-
);
|
|
96
|
-
return Type.Intersect(parts);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
switch (schema.type) {
|
|
100
|
-
case "string": {
|
|
101
|
-
if (schema.enum) {
|
|
102
|
-
return Type.Union(schema.enum.map((value: string) => Type.Literal(value)));
|
|
103
|
-
}
|
|
104
|
-
const stringOptions: Record<string, unknown> = {};
|
|
105
|
-
if (schema.minLength !== undefined) stringOptions.minLength = schema.minLength;
|
|
106
|
-
if (schema.maxLength !== undefined) stringOptions.maxLength = schema.maxLength;
|
|
107
|
-
if (schema.pattern !== undefined) stringOptions.pattern = schema.pattern;
|
|
108
|
-
return Type.String(stringOptions);
|
|
109
|
-
}
|
|
110
|
-
case "number":
|
|
111
|
-
case "integer": {
|
|
112
|
-
const numberOptions: Record<string, unknown> = {};
|
|
113
|
-
if (schema.minimum !== undefined) numberOptions.minimum = schema.minimum;
|
|
114
|
-
if (schema.maximum !== undefined) numberOptions.maximum = schema.maximum;
|
|
115
|
-
return Type.Number(numberOptions);
|
|
116
|
-
}
|
|
117
|
-
case "boolean":
|
|
118
|
-
return Type.Boolean();
|
|
119
|
-
case "array":
|
|
120
|
-
if (schema.items) {
|
|
121
|
-
return Type.Array(await convertJsonSchemaToTypeBox(schema.items, depth + 1));
|
|
122
|
-
}
|
|
123
|
-
return Type.Array(Type.Any());
|
|
124
|
-
case "object":
|
|
125
|
-
if (schema.properties) {
|
|
126
|
-
const propertyEntries = Object.entries(schema.properties);
|
|
127
|
-
if (propertyEntries.length > 100) {
|
|
128
|
-
schemaLogger.warn("[mcp-bridge] JSON schema object has too many properties (>100), falling back to Type.Any()");
|
|
129
|
-
return Type.Any();
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const properties: Record<string, TSchema> = {};
|
|
133
|
-
const requiredSet = new Set<string>(
|
|
134
|
-
Array.isArray(schema.required) ? schema.required : []
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
for (const [key, value] of propertyEntries) {
|
|
138
|
-
const converted = await convertJsonSchemaToTypeBox(value as Record<string, unknown>, depth + 1);
|
|
139
|
-
properties[key] = requiredSet.has(key) ? converted : Type.Optional(converted);
|
|
140
|
-
}
|
|
141
|
-
return Type.Object(properties);
|
|
142
|
-
}
|
|
143
|
-
return Type.Object({});
|
|
144
|
-
case "null":
|
|
145
|
-
return Type.Null();
|
|
146
|
-
default:
|
|
147
|
-
return Type.Any();
|
|
148
|
-
}
|
|
149
|
-
} catch (error) {
|
|
150
|
-
schemaLogger.warn("[mcp-bridge] Failed to convert JSON schema, falling back to Type.Any()");
|
|
151
|
-
return Type.Any();
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
156
|
-
export async function createToolParameters(inputSchema: any): Promise<TSchema> {
|
|
157
|
-
const typeBox = await getTypeBox();
|
|
158
|
-
const Type = typeBox?.Type;
|
|
159
|
-
if (!Type) {
|
|
160
|
-
// TypeBox missing — return the raw JSON Schema as-is
|
|
161
|
-
schemaLogger.warn("[mcp-bridge] TypeBox unavailable — passing raw JSON Schema. Run `npm install` to fix.");
|
|
162
|
-
return inputSchema ?? {};
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (!inputSchema) {
|
|
166
|
-
return Type.Object({});
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// If the inputSchema is already a proper object schema, convert it
|
|
170
|
-
if (inputSchema.type === "object") {
|
|
171
|
-
return convertJsonSchemaToTypeBox(inputSchema, 0);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// If it's not an object, wrap it in an object
|
|
175
|
-
return Type.Object({
|
|
176
|
-
input: await convertJsonSchemaToTypeBox(inputSchema, 0)
|
|
177
|
-
});
|
|
178
|
-
}
|
package/src/standalone-server.ts
DELETED
|
@@ -1,385 +0,0 @@
|
|
|
1
|
-
import { McpRouter } from "./mcp-router.js";
|
|
2
|
-
import { fetchToolsList, initializeProtocol, PACKAGE_VERSION } from "./protocol.js";
|
|
3
|
-
import { pickRegisteredToolName } from "./tool-naming.js";
|
|
4
|
-
import {
|
|
5
|
-
BridgeConfig,
|
|
6
|
-
Logger,
|
|
7
|
-
McpRequest,
|
|
8
|
-
McpResponse,
|
|
9
|
-
McpServerConfig,
|
|
10
|
-
McpTool,
|
|
11
|
-
McpTransport,
|
|
12
|
-
} from "./types.js";
|
|
13
|
-
import { SseTransport } from "./transport-sse.js";
|
|
14
|
-
import { StdioTransport } from "./transport-stdio.js";
|
|
15
|
-
import { StreamableHttpTransport } from "./transport-streamable-http.js";
|
|
16
|
-
|
|
17
|
-
interface DirectToolEntry {
|
|
18
|
-
serverName: string;
|
|
19
|
-
originalName: string;
|
|
20
|
-
registeredName: string;
|
|
21
|
-
description: string;
|
|
22
|
-
inputSchema: any;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Standalone MCP server that wraps the router.
|
|
27
|
-
* Implements the MCP protocol (initialize, tools/list, tools/call)
|
|
28
|
-
* and forwards tool calls to backend MCP servers.
|
|
29
|
-
*/
|
|
30
|
-
export class StandaloneServer {
|
|
31
|
-
private config: BridgeConfig;
|
|
32
|
-
private logger: Logger;
|
|
33
|
-
private router: McpRouter | null = null;
|
|
34
|
-
private initialized = false;
|
|
35
|
-
|
|
36
|
-
// Direct mode state
|
|
37
|
-
private directTools: DirectToolEntry[] = [];
|
|
38
|
-
private directConnections = new Map<string, { transport: McpTransport; initialized: boolean }>();
|
|
39
|
-
|
|
40
|
-
constructor(config: BridgeConfig, logger: Logger) {
|
|
41
|
-
this.config = config;
|
|
42
|
-
this.logger = logger;
|
|
43
|
-
|
|
44
|
-
if (this.isRouterMode()) {
|
|
45
|
-
this.router = new McpRouter(config.servers || {}, config, logger);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
private isRouterMode(): boolean {
|
|
50
|
-
return (this.config.mode ?? "router") === "router";
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Start stdio mode: read JSON-RPC from stdin, write responses to stdout. */
|
|
54
|
-
async startStdio(): Promise<void> {
|
|
55
|
-
const stdin = process.stdin;
|
|
56
|
-
const stdout = process.stdout;
|
|
57
|
-
|
|
58
|
-
stdin.setEncoding("utf8");
|
|
59
|
-
let buffer = "";
|
|
60
|
-
|
|
61
|
-
stdin.on("data", (chunk: string) => {
|
|
62
|
-
buffer += chunk;
|
|
63
|
-
const lines = buffer.split("\n");
|
|
64
|
-
buffer = lines.pop() || "";
|
|
65
|
-
|
|
66
|
-
for (const line of lines) {
|
|
67
|
-
const trimmed = line.trim();
|
|
68
|
-
if (!trimmed) continue;
|
|
69
|
-
this.processLine(trimmed, stdout);
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
stdin.on("end", () => {
|
|
74
|
-
this.logger.info("[mcp-bridge] stdin closed, shutting down");
|
|
75
|
-
this.shutdown().catch(err => {
|
|
76
|
-
this.logger.error("[mcp-bridge] Shutdown error:", err);
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
this.logger.info("[mcp-bridge] Stdio server ready");
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
private processLine(line: string, stdout: NodeJS.WriteStream): void {
|
|
84
|
-
let request: any;
|
|
85
|
-
try {
|
|
86
|
-
request = JSON.parse(line);
|
|
87
|
-
} catch {
|
|
88
|
-
this.writeResponse(stdout, {
|
|
89
|
-
jsonrpc: "2.0",
|
|
90
|
-
id: 0,
|
|
91
|
-
error: { code: -32700, message: "Parse error" }
|
|
92
|
-
});
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Notifications (no id) — just acknowledge
|
|
97
|
-
if (request.id === undefined || request.id === null) {
|
|
98
|
-
// notifications/initialized, etc. — no response needed
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
this.handleRequest(request).then(response => {
|
|
103
|
-
this.writeResponse(stdout, response);
|
|
104
|
-
}).catch(err => {
|
|
105
|
-
this.writeResponse(stdout, {
|
|
106
|
-
jsonrpc: "2.0",
|
|
107
|
-
id: request.id,
|
|
108
|
-
error: { code: -32603, message: err instanceof Error ? err.message : String(err) }
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
private writeResponse(stdout: NodeJS.WriteStream, response: any): void {
|
|
114
|
-
stdout.write(JSON.stringify(response) + "\n");
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** Handle a single MCP JSON-RPC request. */
|
|
118
|
-
async handleRequest(request: McpRequest): Promise<McpResponse> {
|
|
119
|
-
const id = request.id ?? 0;
|
|
120
|
-
|
|
121
|
-
switch (request.method) {
|
|
122
|
-
case "initialize":
|
|
123
|
-
return this.handleInitialize(id);
|
|
124
|
-
|
|
125
|
-
case "tools/list":
|
|
126
|
-
return this.handleToolsList(id);
|
|
127
|
-
|
|
128
|
-
case "tools/call":
|
|
129
|
-
return this.handleToolsCall(id, request.params);
|
|
130
|
-
|
|
131
|
-
case "ping":
|
|
132
|
-
return { jsonrpc: "2.0", id, result: {} };
|
|
133
|
-
|
|
134
|
-
default:
|
|
135
|
-
return {
|
|
136
|
-
jsonrpc: "2.0",
|
|
137
|
-
id,
|
|
138
|
-
error: { code: -32601, message: `Method not found: ${request.method}` }
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
private handleInitialize(id: number): McpResponse {
|
|
144
|
-
this.initialized = true;
|
|
145
|
-
return {
|
|
146
|
-
jsonrpc: "2.0",
|
|
147
|
-
id,
|
|
148
|
-
result: {
|
|
149
|
-
protocolVersion: "2024-11-05",
|
|
150
|
-
capabilities: {
|
|
151
|
-
tools: {}
|
|
152
|
-
},
|
|
153
|
-
serverInfo: {
|
|
154
|
-
name: "mcp-bridge",
|
|
155
|
-
version: PACKAGE_VERSION
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
private async handleToolsList(id: number): Promise<McpResponse> {
|
|
162
|
-
if (this.isRouterMode()) {
|
|
163
|
-
return {
|
|
164
|
-
jsonrpc: "2.0",
|
|
165
|
-
id,
|
|
166
|
-
result: {
|
|
167
|
-
tools: [{
|
|
168
|
-
name: "mcp",
|
|
169
|
-
description: McpRouter.generateDescription(this.config.servers),
|
|
170
|
-
inputSchema: {
|
|
171
|
-
type: "object",
|
|
172
|
-
properties: {
|
|
173
|
-
server: { type: "string", description: "Server name" },
|
|
174
|
-
action: { type: "string", description: "list | call | refresh | status" },
|
|
175
|
-
tool: { type: "string", description: "Tool name for action=call" },
|
|
176
|
-
params: { type: "object", description: "Tool arguments" }
|
|
177
|
-
},
|
|
178
|
-
required: ["server"]
|
|
179
|
-
}
|
|
180
|
-
}]
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Direct mode: discover all tools from all servers
|
|
186
|
-
await this.discoverDirectTools();
|
|
187
|
-
const tools = this.directTools.map(t => ({
|
|
188
|
-
name: t.registeredName,
|
|
189
|
-
description: t.description,
|
|
190
|
-
inputSchema: t.inputSchema
|
|
191
|
-
}));
|
|
192
|
-
|
|
193
|
-
return { jsonrpc: "2.0", id, result: { tools } };
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
private async handleToolsCall(id: number, params: any): Promise<McpResponse> {
|
|
197
|
-
const toolName = params?.name;
|
|
198
|
-
const toolArgs = params?.arguments ?? {};
|
|
199
|
-
|
|
200
|
-
if (!toolName) {
|
|
201
|
-
return {
|
|
202
|
-
jsonrpc: "2.0",
|
|
203
|
-
id,
|
|
204
|
-
error: { code: -32602, message: "Missing tool name" }
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (this.isRouterMode()) {
|
|
209
|
-
if (toolName !== "mcp") {
|
|
210
|
-
return {
|
|
211
|
-
jsonrpc: "2.0",
|
|
212
|
-
id,
|
|
213
|
-
error: { code: -32004, message: `Unknown tool: ${toolName}. In router mode, use the 'mcp' tool.` }
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const result = await this.router!.dispatch(
|
|
218
|
-
toolArgs.server,
|
|
219
|
-
toolArgs.action,
|
|
220
|
-
toolArgs.tool,
|
|
221
|
-
toolArgs.params
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
// Check if result is an error
|
|
225
|
-
if ("error" in result) {
|
|
226
|
-
return {
|
|
227
|
-
jsonrpc: "2.0",
|
|
228
|
-
id,
|
|
229
|
-
result: {
|
|
230
|
-
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return {
|
|
236
|
-
jsonrpc: "2.0",
|
|
237
|
-
id,
|
|
238
|
-
result: {
|
|
239
|
-
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
240
|
-
}
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Direct mode: find and call the tool
|
|
245
|
-
const entry = this.directTools.find(t => t.registeredName === toolName);
|
|
246
|
-
if (!entry) {
|
|
247
|
-
return {
|
|
248
|
-
jsonrpc: "2.0",
|
|
249
|
-
id,
|
|
250
|
-
error: {
|
|
251
|
-
code: -32004,
|
|
252
|
-
message: `Unknown tool: ${toolName}`,
|
|
253
|
-
data: { errorType: "unknown_tool" }
|
|
254
|
-
}
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
try {
|
|
259
|
-
const conn = this.directConnections.get(entry.serverName);
|
|
260
|
-
if (!conn || !conn.transport.isConnected()) {
|
|
261
|
-
return {
|
|
262
|
-
jsonrpc: "2.0",
|
|
263
|
-
id,
|
|
264
|
-
error: {
|
|
265
|
-
code: -32001,
|
|
266
|
-
message: `Server '${entry.serverName}' not connected`,
|
|
267
|
-
data: { errorType: "connection_failed", server: entry.serverName, retriable: true }
|
|
268
|
-
}
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const response = await conn.transport.sendRequest({
|
|
273
|
-
jsonrpc: "2.0",
|
|
274
|
-
method: "tools/call",
|
|
275
|
-
params: { name: entry.originalName, arguments: toolArgs }
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
if (response.error) {
|
|
279
|
-
return {
|
|
280
|
-
jsonrpc: "2.0",
|
|
281
|
-
id,
|
|
282
|
-
error: {
|
|
283
|
-
code: -32005,
|
|
284
|
-
message: response.error.message,
|
|
285
|
-
data: { errorType: "mcp_error", server: entry.serverName }
|
|
286
|
-
}
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
return { jsonrpc: "2.0", id, result: response.result };
|
|
291
|
-
} catch (err) {
|
|
292
|
-
return {
|
|
293
|
-
jsonrpc: "2.0",
|
|
294
|
-
id,
|
|
295
|
-
error: {
|
|
296
|
-
code: -32001,
|
|
297
|
-
message: err instanceof Error ? err.message : String(err),
|
|
298
|
-
data: { errorType: "connection_failed", server: entry.serverName, retriable: true }
|
|
299
|
-
}
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/** Connect to all backend servers and discover their tools (direct mode). */
|
|
305
|
-
private async discoverDirectTools(): Promise<void> {
|
|
306
|
-
if (this.directTools.length > 0) return; // Already discovered
|
|
307
|
-
|
|
308
|
-
const globalNames = new Set<string>();
|
|
309
|
-
|
|
310
|
-
for (const [serverName, serverConfig] of Object.entries(this.config.servers)) {
|
|
311
|
-
try {
|
|
312
|
-
const transport = this.createTransport(serverName, serverConfig);
|
|
313
|
-
await transport.connect();
|
|
314
|
-
await initializeProtocol(transport, PACKAGE_VERSION);
|
|
315
|
-
|
|
316
|
-
this.directConnections.set(serverName, { transport, initialized: true });
|
|
317
|
-
|
|
318
|
-
const tools = await fetchToolsList(transport);
|
|
319
|
-
const localNames = new Set<string>();
|
|
320
|
-
|
|
321
|
-
for (const tool of tools) {
|
|
322
|
-
const registeredName = pickRegisteredToolName(
|
|
323
|
-
serverName,
|
|
324
|
-
tool.name,
|
|
325
|
-
this.config.toolPrefix,
|
|
326
|
-
localNames,
|
|
327
|
-
globalNames,
|
|
328
|
-
this.logger
|
|
329
|
-
);
|
|
330
|
-
localNames.add(registeredName);
|
|
331
|
-
globalNames.add(registeredName);
|
|
332
|
-
|
|
333
|
-
this.directTools.push({
|
|
334
|
-
serverName,
|
|
335
|
-
originalName: tool.name,
|
|
336
|
-
registeredName,
|
|
337
|
-
description: tool.description,
|
|
338
|
-
inputSchema: tool.inputSchema
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
this.logger.info(`[mcp-bridge] Discovered ${tools.length} tools from ${serverName}`);
|
|
343
|
-
} catch (err) {
|
|
344
|
-
this.logger.error(`[mcp-bridge] Failed to connect to ${serverName}:`, err);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
private createTransport(serverName: string, serverConfig: McpServerConfig): McpTransport {
|
|
350
|
-
const onReconnected = async () => {
|
|
351
|
-
this.logger.info(`[mcp-bridge] ${serverName} reconnected`);
|
|
352
|
-
};
|
|
353
|
-
|
|
354
|
-
switch (serverConfig.transport) {
|
|
355
|
-
case "sse":
|
|
356
|
-
return new SseTransport(serverConfig, this.config, this.logger, onReconnected);
|
|
357
|
-
case "stdio":
|
|
358
|
-
return new StdioTransport(serverConfig, this.config, this.logger, onReconnected);
|
|
359
|
-
case "streamable-http":
|
|
360
|
-
return new StreamableHttpTransport(serverConfig, this.config, this.logger, onReconnected);
|
|
361
|
-
default:
|
|
362
|
-
throw new Error(`Unsupported transport: ${serverConfig.transport}`);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/** Graceful shutdown: disconnect all backend servers. */
|
|
367
|
-
async shutdown(): Promise<void> {
|
|
368
|
-
this.logger.info("[mcp-bridge] Shutting down...");
|
|
369
|
-
|
|
370
|
-
if (this.router) {
|
|
371
|
-
await this.router.disconnectAll();
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
for (const [name, conn] of this.directConnections) {
|
|
375
|
-
try {
|
|
376
|
-
await conn.transport.disconnect();
|
|
377
|
-
} catch (err) {
|
|
378
|
-
this.logger.error(`[mcp-bridge] Error disconnecting ${name}:`, err);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
this.directConnections.clear();
|
|
382
|
-
|
|
383
|
-
this.logger.info("[mcp-bridge] Shutdown complete");
|
|
384
|
-
}
|
|
385
|
-
}
|
package/src/tool-naming.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
function isNameTaken(name: string, localNames: Set<string>, globalNames: Set<string>): boolean {
|
|
2
|
-
return localNames.has(name) || globalNames.has(name);
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
export function pickRegisteredToolName(
|
|
6
|
-
serverName: string,
|
|
7
|
-
toolName: string,
|
|
8
|
-
toolPrefix: boolean | "auto" | undefined,
|
|
9
|
-
localNames: Set<string>,
|
|
10
|
-
globalNames: Set<string>,
|
|
11
|
-
logger?: { warn: (...args: unknown[]) => void }
|
|
12
|
-
): string {
|
|
13
|
-
// toolPrefix: true = always prefix, false = never prefix, "auto" = prefix only on collision (default)
|
|
14
|
-
const effectivePrefix = toolPrefix === undefined ? "auto" : toolPrefix;
|
|
15
|
-
|
|
16
|
-
let candidate: string;
|
|
17
|
-
if (effectivePrefix === true) {
|
|
18
|
-
// Always prefix with server name
|
|
19
|
-
candidate = `${serverName}_${toolName}`.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
20
|
-
} else if (effectivePrefix === false) {
|
|
21
|
-
// Never prefix — use raw tool name, no collision fallback
|
|
22
|
-
candidate = toolName.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
23
|
-
} else {
|
|
24
|
-
// "auto" — try without prefix, auto-prefix on collision
|
|
25
|
-
const unprefixed = toolName.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
26
|
-
if (isNameTaken(unprefixed, localNames, globalNames)) {
|
|
27
|
-
const prefixedName = `${serverName}_${toolName}`.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
28
|
-
logger?.warn(
|
|
29
|
-
`[mcp-bridge] Global tool name collision detected for "${unprefixed}". Auto-prefixing with server name: "${prefixedName}"`
|
|
30
|
-
);
|
|
31
|
-
candidate = prefixedName;
|
|
32
|
-
} else {
|
|
33
|
-
candidate = unprefixed;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const uniqueBase = candidate;
|
|
38
|
-
let suffix = 2;
|
|
39
|
-
while (isNameTaken(candidate, localNames, globalNames)) {
|
|
40
|
-
candidate = `${uniqueBase}_${suffix}`;
|
|
41
|
-
suffix += 1;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (candidate !== uniqueBase) {
|
|
45
|
-
logger?.warn(
|
|
46
|
-
`[mcp-bridge] Tool name collision after sanitization on server ${serverName}: "${uniqueBase}" -> "${candidate}"`
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return candidate;
|
|
51
|
-
}
|