@mhingston5/conduit 1.1.5 → 1.1.7
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/README.md +29 -1
- package/dist/index.js +413 -127
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/auth.cmd.ts +26 -14
- package/src/core/config.service.ts +27 -2
- package/src/core/execution.service.ts +5 -0
- package/src/core/policy.service.ts +5 -0
- package/src/core/request.controller.ts +32 -7
- package/src/gateway/auth.service.ts +55 -13
- package/src/gateway/gateway.service.ts +150 -65
- package/src/gateway/host.client.ts +65 -0
- package/src/gateway/upstream.client.ts +94 -26
- package/src/index.ts +13 -4
- package/src/sdk/sdk-generator.ts +66 -30
- package/src/transport/stdio.transport.ts +44 -3
- package/tests/__snapshots__/assets.test.ts.snap +45 -14
- package/tests/auth.service.test.ts +57 -0
- package/tests/code-mode-lite-gateway.test.ts +4 -4
- package/tests/config.service.test.ts +29 -1
- package/tests/gateway.service.test.ts +5 -5
- package/tests/routing.test.ts +7 -0
- package/tests/sdk/sdk-generator.test.ts +7 -7
- package/tests/upstream.transports.test.ts +117 -0
package/dist/index.js
CHANGED
|
@@ -29,6 +29,8 @@ var UpstreamCredentialsSchema = z.object({
|
|
|
29
29
|
tokenUrl: z.string().optional(),
|
|
30
30
|
refreshToken: z.string().optional(),
|
|
31
31
|
scopes: z.array(z.string()).optional(),
|
|
32
|
+
tokenRequestFormat: z.enum(["form", "json"]).optional(),
|
|
33
|
+
tokenParams: z.record(z.string(), z.string()).optional(),
|
|
32
34
|
apiKey: z.string().optional(),
|
|
33
35
|
bearerToken: z.string().optional(),
|
|
34
36
|
headerName: z.string().optional()
|
|
@@ -39,6 +41,18 @@ var HttpUpstreamSchema = z.object({
|
|
|
39
41
|
url: z.string(),
|
|
40
42
|
credentials: UpstreamCredentialsSchema.optional()
|
|
41
43
|
});
|
|
44
|
+
var StreamableHttpUpstreamSchema = z.object({
|
|
45
|
+
id: z.string(),
|
|
46
|
+
type: z.literal("streamableHttp"),
|
|
47
|
+
url: z.string(),
|
|
48
|
+
credentials: UpstreamCredentialsSchema.optional()
|
|
49
|
+
});
|
|
50
|
+
var SseUpstreamSchema = z.object({
|
|
51
|
+
id: z.string(),
|
|
52
|
+
type: z.literal("sse"),
|
|
53
|
+
url: z.string(),
|
|
54
|
+
credentials: UpstreamCredentialsSchema.optional()
|
|
55
|
+
});
|
|
42
56
|
var StdioUpstreamSchema = z.object({
|
|
43
57
|
id: z.string(),
|
|
44
58
|
type: z.literal("stdio"),
|
|
@@ -46,7 +60,12 @@ var StdioUpstreamSchema = z.object({
|
|
|
46
60
|
args: z.array(z.string()).optional(),
|
|
47
61
|
env: z.record(z.string(), z.string()).optional()
|
|
48
62
|
});
|
|
49
|
-
var UpstreamInfoSchema = z.union([
|
|
63
|
+
var UpstreamInfoSchema = z.union([
|
|
64
|
+
HttpUpstreamSchema,
|
|
65
|
+
StreamableHttpUpstreamSchema,
|
|
66
|
+
SseUpstreamSchema,
|
|
67
|
+
StdioUpstreamSchema
|
|
68
|
+
]);
|
|
50
69
|
var ConfigSchema = z.object({
|
|
51
70
|
port: z.union([z.string(), z.number()]).default("3000").transform((v) => Number(v)),
|
|
52
71
|
nodeEnv: z.enum(["development", "production", "test"]).default("development"),
|
|
@@ -111,9 +130,13 @@ var ConfigService = class {
|
|
|
111
130
|
}
|
|
112
131
|
loadConfigFile() {
|
|
113
132
|
const configPath = process.env.CONFIG_FILE || (fs.existsSync(path.resolve(process.cwd(), "conduit.yaml")) ? "conduit.yaml" : fs.existsSync(path.resolve(process.cwd(), "conduit.json")) ? "conduit.json" : null);
|
|
114
|
-
if (!configPath)
|
|
133
|
+
if (!configPath) {
|
|
134
|
+
console.warn(`[Conduit] No config file found in ${process.cwd()}. Running with default settings.`);
|
|
135
|
+
return {};
|
|
136
|
+
}
|
|
115
137
|
try {
|
|
116
138
|
const fullPath = path.resolve(process.cwd(), configPath);
|
|
139
|
+
console.error(`[Conduit] Loading config from ${fullPath}`);
|
|
117
140
|
let fileContent = fs.readFileSync(fullPath, "utf-8");
|
|
118
141
|
fileContent = fileContent.replace(/\$\{([a-zA-Z0-9_]+)(?::-([^}]+))?\}/g, (match, varName, defaultValue) => {
|
|
119
142
|
const value = process.env[varName];
|
|
@@ -392,6 +415,7 @@ var StdioTransport = class {
|
|
|
392
415
|
requestController;
|
|
393
416
|
concurrencyService;
|
|
394
417
|
buffer = "";
|
|
418
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
395
419
|
constructor(logger, requestController, concurrencyService) {
|
|
396
420
|
this.logger = logger;
|
|
397
421
|
this.requestController = requestController;
|
|
@@ -405,6 +429,30 @@ var StdioTransport = class {
|
|
|
405
429
|
this.logger.info("Stdin closed");
|
|
406
430
|
});
|
|
407
431
|
}
|
|
432
|
+
async callHost(method, params) {
|
|
433
|
+
const id = Math.random().toString(36).substring(7);
|
|
434
|
+
const request = {
|
|
435
|
+
jsonrpc: "2.0",
|
|
436
|
+
id,
|
|
437
|
+
method,
|
|
438
|
+
params
|
|
439
|
+
};
|
|
440
|
+
return new Promise((resolve, reject) => {
|
|
441
|
+
const timeout = setTimeout(() => {
|
|
442
|
+
this.pendingRequests.delete(id);
|
|
443
|
+
reject(new Error(`Timeout waiting for host response to ${method}`));
|
|
444
|
+
}, 3e4);
|
|
445
|
+
this.pendingRequests.set(id, (response) => {
|
|
446
|
+
clearTimeout(timeout);
|
|
447
|
+
if (response.error) {
|
|
448
|
+
reject(new Error(response.error.message));
|
|
449
|
+
} else {
|
|
450
|
+
resolve(response.result);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
this.sendResponse(request);
|
|
454
|
+
});
|
|
455
|
+
}
|
|
408
456
|
handleData(chunk) {
|
|
409
457
|
this.buffer += chunk;
|
|
410
458
|
let pos;
|
|
@@ -416,11 +464,11 @@ var StdioTransport = class {
|
|
|
416
464
|
}
|
|
417
465
|
}
|
|
418
466
|
async processLine(line) {
|
|
419
|
-
let
|
|
467
|
+
let message;
|
|
420
468
|
try {
|
|
421
|
-
|
|
469
|
+
message = JSON.parse(line);
|
|
422
470
|
} catch (err) {
|
|
423
|
-
this.logger.error({ err, line }, "Failed to parse JSON-RPC
|
|
471
|
+
this.logger.error({ err, line }, "Failed to parse JSON-RPC message");
|
|
424
472
|
const errorResponse = {
|
|
425
473
|
jsonrpc: "2.0",
|
|
426
474
|
id: null,
|
|
@@ -432,6 +480,15 @@ var StdioTransport = class {
|
|
|
432
480
|
this.sendResponse(errorResponse);
|
|
433
481
|
return;
|
|
434
482
|
}
|
|
483
|
+
if (message.id !== void 0 && (message.result !== void 0 || message.error !== void 0)) {
|
|
484
|
+
const pending = this.pendingRequests.get(message.id);
|
|
485
|
+
if (pending) {
|
|
486
|
+
this.pendingRequests.delete(message.id);
|
|
487
|
+
pending(message);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const request = message;
|
|
435
492
|
const context = new ExecutionContext({
|
|
436
493
|
logger: this.logger,
|
|
437
494
|
remoteAddress: "stdio"
|
|
@@ -775,12 +832,29 @@ var RequestController = class {
|
|
|
775
832
|
case "notifications/initialized":
|
|
776
833
|
return null;
|
|
777
834
|
// Notifications don't get responses per MCP spec
|
|
835
|
+
case "mcp_register_upstream":
|
|
836
|
+
return this.handleRegisterUpstream(params, context, id);
|
|
778
837
|
case "ping":
|
|
779
838
|
return { jsonrpc: "2.0", id, result: {} };
|
|
780
839
|
default:
|
|
781
840
|
return this.errorResponse(id, -32601, `Method not found: ${method}`);
|
|
782
841
|
}
|
|
783
842
|
}
|
|
843
|
+
async handleRegisterUpstream(params, context, id) {
|
|
844
|
+
if (!params || !params.id || !params.type || !params.url && !params.command) {
|
|
845
|
+
return this.errorResponse(id, -32602, "Missing registration parameters (id, type, url/command)");
|
|
846
|
+
}
|
|
847
|
+
try {
|
|
848
|
+
this.gatewayService.registerUpstream(params);
|
|
849
|
+
return {
|
|
850
|
+
jsonrpc: "2.0",
|
|
851
|
+
id,
|
|
852
|
+
result: { success: true }
|
|
853
|
+
};
|
|
854
|
+
} catch (err) {
|
|
855
|
+
return this.errorResponse(id, -32001, err.message);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
784
858
|
async handleDiscoverTools(params, context, id) {
|
|
785
859
|
const tools = await this.gatewayService.discoverTools(context);
|
|
786
860
|
const standardizedTools = tools.map((t) => ({
|
|
@@ -848,13 +922,18 @@ var RequestController = class {
|
|
|
848
922
|
async handleCallTool(params, context, id) {
|
|
849
923
|
if (!params) return this.errorResponse(id, -32602, "Missing parameters");
|
|
850
924
|
const { name, arguments: toolArgs } = params;
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
925
|
+
const toolId = this.gatewayService.policyService.parseToolName(name);
|
|
926
|
+
const baseName = toolId.name;
|
|
927
|
+
const isConduit = toolId.namespace === "conduit" || toolId.namespace === "";
|
|
928
|
+
if (isConduit) {
|
|
929
|
+
switch (baseName) {
|
|
930
|
+
case "mcp_execute_typescript":
|
|
931
|
+
return this.handleExecuteToolCall("typescript", toolArgs, context, id);
|
|
932
|
+
case "mcp_execute_python":
|
|
933
|
+
return this.handleExecuteToolCall("python", toolArgs, context, id);
|
|
934
|
+
case "mcp_execute_isolate":
|
|
935
|
+
return this.handleExecuteToolCall("isolate", toolArgs, context, id);
|
|
936
|
+
}
|
|
858
937
|
}
|
|
859
938
|
const response = await this.gatewayService.callTool(name, toolArgs, context);
|
|
860
939
|
return { ...response, id };
|
|
@@ -1001,6 +1080,8 @@ var RequestController = class {
|
|
|
1001
1080
|
import axios2 from "axios";
|
|
1002
1081
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
1003
1082
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
1083
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
1084
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
1004
1085
|
import { z as z2 } from "zod";
|
|
1005
1086
|
var UpstreamClient = class {
|
|
1006
1087
|
logger;
|
|
@@ -1009,6 +1090,7 @@ var UpstreamClient = class {
|
|
|
1009
1090
|
urlValidator;
|
|
1010
1091
|
mcpClient;
|
|
1011
1092
|
transport;
|
|
1093
|
+
connected = false;
|
|
1012
1094
|
constructor(logger, info, authService, urlValidator) {
|
|
1013
1095
|
this.logger = logger.child({ upstreamId: info.id });
|
|
1014
1096
|
this.info = info;
|
|
@@ -1031,28 +1113,80 @@ var UpstreamClient = class {
|
|
|
1031
1113
|
}, {
|
|
1032
1114
|
capabilities: {}
|
|
1033
1115
|
});
|
|
1116
|
+
return;
|
|
1034
1117
|
}
|
|
1118
|
+
if (this.info.type === "streamableHttp") {
|
|
1119
|
+
this.transport = new StreamableHTTPClientTransport(new URL(this.info.url), {
|
|
1120
|
+
fetch: this.createAuthedFetch()
|
|
1121
|
+
});
|
|
1122
|
+
this.mcpClient = new Client({
|
|
1123
|
+
name: "conduit-gateway",
|
|
1124
|
+
version: "1.0.0"
|
|
1125
|
+
}, {
|
|
1126
|
+
capabilities: {}
|
|
1127
|
+
});
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
if (this.info.type === "sse") {
|
|
1131
|
+
this.mcpClient = new Client({
|
|
1132
|
+
name: "conduit-gateway",
|
|
1133
|
+
version: "1.0.0"
|
|
1134
|
+
}, {
|
|
1135
|
+
capabilities: {}
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
createAuthedFetch() {
|
|
1140
|
+
const creds = this.info.credentials;
|
|
1141
|
+
if (!creds) return fetch;
|
|
1142
|
+
return async (input, init = {}) => {
|
|
1143
|
+
const headers = new Headers(init.headers || {});
|
|
1144
|
+
const authHeaders = await this.authService.getAuthHeaders(creds);
|
|
1145
|
+
for (const [k, v] of Object.entries(authHeaders)) {
|
|
1146
|
+
headers.set(k, v);
|
|
1147
|
+
}
|
|
1148
|
+
return fetch(input, { ...init, headers });
|
|
1149
|
+
};
|
|
1035
1150
|
}
|
|
1036
1151
|
async ensureConnected() {
|
|
1037
|
-
if (!this.mcpClient
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1152
|
+
if (!this.mcpClient) return;
|
|
1153
|
+
if (!this.transport && this.info.type === "sse") {
|
|
1154
|
+
const authHeaders = this.info.credentials ? await this.authService.getAuthHeaders(this.info.credentials) : {};
|
|
1155
|
+
this.transport = new SSEClientTransport(new URL(this.info.url), {
|
|
1156
|
+
fetch: this.createAuthedFetch(),
|
|
1157
|
+
eventSourceInit: { headers: authHeaders },
|
|
1158
|
+
requestInit: { headers: authHeaders }
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
if (!this.transport) return;
|
|
1162
|
+
if (this.connected) return;
|
|
1163
|
+
if (this.info.type === "streamableHttp" || this.info.type === "sse") {
|
|
1164
|
+
const securityResult = await this.urlValidator.validateUrl(this.info.url);
|
|
1165
|
+
if (!securityResult.valid) {
|
|
1166
|
+
this.logger.error({ url: this.info.url }, "Blocked upstream URL (SSRF)");
|
|
1167
|
+
throw new Error(securityResult.message || "Forbidden URL");
|
|
1041
1168
|
}
|
|
1169
|
+
}
|
|
1170
|
+
try {
|
|
1171
|
+
this.logger.debug("Connecting to upstream transport...");
|
|
1172
|
+
await this.mcpClient.connect(this.transport);
|
|
1173
|
+
this.connected = true;
|
|
1174
|
+
this.logger.info("Connected to upstream MCP");
|
|
1042
1175
|
} catch (e) {
|
|
1176
|
+
this.logger.error({ err: e.message }, "Failed to connect to upstream");
|
|
1177
|
+
throw e;
|
|
1043
1178
|
}
|
|
1044
1179
|
}
|
|
1045
1180
|
async call(request, context) {
|
|
1046
|
-
const
|
|
1047
|
-
if (
|
|
1048
|
-
return this.
|
|
1049
|
-
} else {
|
|
1050
|
-
return this.callHttp(request, context);
|
|
1181
|
+
const usesMcpClientTransport = (info) => info.type === "stdio" || info.type === "streamableHttp" || info.type === "sse";
|
|
1182
|
+
if (usesMcpClientTransport(this.info)) {
|
|
1183
|
+
return this.callMcpClient(request);
|
|
1051
1184
|
}
|
|
1185
|
+
return this.callHttp(request, context);
|
|
1052
1186
|
}
|
|
1053
|
-
async
|
|
1187
|
+
async callMcpClient(request) {
|
|
1054
1188
|
if (!this.mcpClient) {
|
|
1055
|
-
return { jsonrpc: "2.0", id: request.id, error: { code: -32603, message: "
|
|
1189
|
+
return { jsonrpc: "2.0", id: request.id, error: { code: -32603, message: "MCP client not initialized" } };
|
|
1056
1190
|
}
|
|
1057
1191
|
try {
|
|
1058
1192
|
await this.ensureConnected();
|
|
@@ -1092,19 +1226,21 @@ var UpstreamClient = class {
|
|
|
1092
1226
|
};
|
|
1093
1227
|
}
|
|
1094
1228
|
} catch (error) {
|
|
1095
|
-
this.logger.error({ err: error }, "
|
|
1229
|
+
this.logger.error({ err: error }, "MCP call failed");
|
|
1096
1230
|
return {
|
|
1097
1231
|
jsonrpc: "2.0",
|
|
1098
1232
|
id: request.id,
|
|
1099
1233
|
error: {
|
|
1100
1234
|
code: error.code || -32603,
|
|
1101
|
-
message: error.message || "Internal error in
|
|
1235
|
+
message: error.message || "Internal error in MCP transport"
|
|
1102
1236
|
}
|
|
1103
1237
|
};
|
|
1104
1238
|
}
|
|
1105
1239
|
}
|
|
1106
1240
|
async callHttp(request, context) {
|
|
1107
|
-
if (this.info.type === "stdio"
|
|
1241
|
+
if (this.info.type === "stdio" || this.info.type === "streamableHttp" || this.info.type === "sse") {
|
|
1242
|
+
throw new Error("Unreachable");
|
|
1243
|
+
}
|
|
1108
1244
|
const url = this.info.url;
|
|
1109
1245
|
const headers = {
|
|
1110
1246
|
"Content-Type": "application/json",
|
|
@@ -1153,7 +1289,7 @@ var UpstreamClient = class {
|
|
|
1153
1289
|
}
|
|
1154
1290
|
}
|
|
1155
1291
|
async getManifest(context) {
|
|
1156
|
-
if (this.info.type !== "http") return null;
|
|
1292
|
+
if (this.info.type && this.info.type !== "http") return null;
|
|
1157
1293
|
try {
|
|
1158
1294
|
const baseUrl = this.info.url.replace(/\/$/, "");
|
|
1159
1295
|
const manifestUrl = `${baseUrl}/conduit.manifest.json`;
|
|
@@ -1193,6 +1329,8 @@ var AuthService = class {
|
|
|
1193
1329
|
logger;
|
|
1194
1330
|
// Cache tokens separately from credentials to avoid mutation
|
|
1195
1331
|
tokenCache = /* @__PURE__ */ new Map();
|
|
1332
|
+
// Keep the latest refresh token in-memory (rotating tokens)
|
|
1333
|
+
refreshTokenCache = /* @__PURE__ */ new Map();
|
|
1196
1334
|
// Prevent concurrent refresh requests for the same client
|
|
1197
1335
|
refreshLocks = /* @__PURE__ */ new Map();
|
|
1198
1336
|
constructor(logger) {
|
|
@@ -1237,25 +1375,53 @@ var AuthService = class {
|
|
|
1237
1375
|
}
|
|
1238
1376
|
this.logger.info({ tokenUrl: creds.tokenUrl, clientId: creds.clientId }, "Refreshing OAuth2 token");
|
|
1239
1377
|
try {
|
|
1240
|
-
const
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1378
|
+
const tokenUrl = creds.tokenUrl;
|
|
1379
|
+
const cachedRefreshToken = this.refreshTokenCache.get(cacheKey);
|
|
1380
|
+
const refreshToken = cachedRefreshToken || creds.refreshToken;
|
|
1381
|
+
if (!refreshToken) {
|
|
1382
|
+
throw new Error("OAuth2 credentials missing required fields for refresh");
|
|
1383
|
+
}
|
|
1384
|
+
const payload = {
|
|
1385
|
+
grant_type: "refresh_token",
|
|
1386
|
+
refresh_token: refreshToken,
|
|
1387
|
+
client_id: creds.clientId
|
|
1388
|
+
};
|
|
1244
1389
|
if (creds.clientSecret) {
|
|
1245
|
-
|
|
1390
|
+
payload.client_secret = creds.clientSecret;
|
|
1391
|
+
}
|
|
1392
|
+
if (creds.tokenParams) {
|
|
1393
|
+
Object.assign(payload, creds.tokenParams);
|
|
1246
1394
|
}
|
|
1247
|
-
const
|
|
1395
|
+
const requestFormat = (() => {
|
|
1396
|
+
if (creds.tokenRequestFormat) return creds.tokenRequestFormat;
|
|
1397
|
+
try {
|
|
1398
|
+
const hostname = new URL(tokenUrl).hostname;
|
|
1399
|
+
if (hostname === "auth.atlassian.com") return "json";
|
|
1400
|
+
} catch {
|
|
1401
|
+
}
|
|
1402
|
+
return "form";
|
|
1403
|
+
})();
|
|
1404
|
+
const response = requestFormat === "json" ? await axios3.post(tokenUrl, payload, {
|
|
1405
|
+
headers: {
|
|
1406
|
+
"Content-Type": "application/json",
|
|
1407
|
+
"Accept": "application/json"
|
|
1408
|
+
}
|
|
1409
|
+
}) : await axios3.post(tokenUrl, new URLSearchParams(payload), {
|
|
1248
1410
|
headers: {
|
|
1249
1411
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
1250
1412
|
"Accept": "application/json"
|
|
1251
1413
|
}
|
|
1252
1414
|
});
|
|
1253
|
-
const { access_token, expires_in } = response.data;
|
|
1254
|
-
const
|
|
1415
|
+
const { access_token, expires_in, refresh_token } = response.data;
|
|
1416
|
+
const expiresInRaw = Number(expires_in);
|
|
1417
|
+
const expiresInSeconds = Number.isFinite(expiresInRaw) ? expiresInRaw : 3600;
|
|
1255
1418
|
this.tokenCache.set(cacheKey, {
|
|
1256
1419
|
accessToken: access_token,
|
|
1257
1420
|
expiresAt: Date.now() + expiresInSeconds * 1e3
|
|
1258
1421
|
});
|
|
1422
|
+
if (typeof refresh_token === "string" && refresh_token.length > 0) {
|
|
1423
|
+
this.refreshTokenCache.set(cacheKey, refresh_token);
|
|
1424
|
+
}
|
|
1259
1425
|
return `Bearer ${access_token}`;
|
|
1260
1426
|
} catch (err) {
|
|
1261
1427
|
const errorMsg = err.response?.data?.error_description || err.response?.data?.error || err.message;
|
|
@@ -1346,6 +1512,9 @@ var PolicyService = class {
|
|
|
1346
1512
|
}
|
|
1347
1513
|
return true;
|
|
1348
1514
|
}
|
|
1515
|
+
if (patternParts.length === 1 && toolParts.length > 1) {
|
|
1516
|
+
return patternParts[0] === toolParts[toolParts.length - 1];
|
|
1517
|
+
}
|
|
1349
1518
|
if (patternParts.length !== toolParts.length) return false;
|
|
1350
1519
|
for (let i = 0; i < patternParts.length; i++) {
|
|
1351
1520
|
if (patternParts[i] !== toolParts[i]) return false;
|
|
@@ -1428,7 +1597,8 @@ var GatewayService = class {
|
|
|
1428
1597
|
// Cache compiled validators to avoid recompilation on every call
|
|
1429
1598
|
validatorCache = /* @__PURE__ */ new Map();
|
|
1430
1599
|
constructor(logger, urlValidator, policyService) {
|
|
1431
|
-
this.logger = logger;
|
|
1600
|
+
this.logger = logger.child({ component: "GatewayService" });
|
|
1601
|
+
this.logger.debug("GatewayService instance created");
|
|
1432
1602
|
this.urlValidator = urlValidator;
|
|
1433
1603
|
this.authService = new AuthService(logger);
|
|
1434
1604
|
this.schemaCache = new SchemaCache(logger);
|
|
@@ -1439,52 +1609,79 @@ var GatewayService = class {
|
|
|
1439
1609
|
registerUpstream(info) {
|
|
1440
1610
|
const client = new UpstreamClient(this.logger, info, this.authService, this.urlValidator);
|
|
1441
1611
|
this.clients.set(info.id, client);
|
|
1442
|
-
this.logger.info({ upstreamId: info.id }, "Registered upstream MCP");
|
|
1612
|
+
this.logger.info({ upstreamId: info.id, totalRegistered: this.clients.size }, "Registered upstream MCP");
|
|
1613
|
+
}
|
|
1614
|
+
registerHost(transport) {
|
|
1615
|
+
this.logger.debug("Host transport available but not registered as tool upstream (protocol limitation)");
|
|
1443
1616
|
}
|
|
1444
1617
|
async listToolPackages() {
|
|
1445
|
-
|
|
1618
|
+
const upstreams = Array.from(this.clients.entries()).map(([id, client]) => ({
|
|
1446
1619
|
id,
|
|
1447
1620
|
description: `Upstream ${id}`,
|
|
1448
|
-
// NOTE: Upstream description fetching deferred to V2
|
|
1449
1621
|
version: "1.0.0"
|
|
1450
1622
|
}));
|
|
1623
|
+
return [
|
|
1624
|
+
{ id: "conduit", description: "Conduit built-in execution tools", version: "1.0.0" },
|
|
1625
|
+
...upstreams
|
|
1626
|
+
];
|
|
1627
|
+
}
|
|
1628
|
+
getBuiltInTools() {
|
|
1629
|
+
return BUILT_IN_TOOLS;
|
|
1451
1630
|
}
|
|
1452
1631
|
async listToolStubs(packageId, context) {
|
|
1632
|
+
if (packageId === "conduit") {
|
|
1633
|
+
const stubs2 = BUILT_IN_TOOLS.map((t) => ({
|
|
1634
|
+
id: `conduit__${t.name}`,
|
|
1635
|
+
name: t.name,
|
|
1636
|
+
description: t.description
|
|
1637
|
+
}));
|
|
1638
|
+
if (context.allowedTools) {
|
|
1639
|
+
return stubs2.filter((t) => this.policyService.isToolAllowed(t.id, context.allowedTools));
|
|
1640
|
+
}
|
|
1641
|
+
return stubs2;
|
|
1642
|
+
}
|
|
1453
1643
|
const client = this.clients.get(packageId);
|
|
1454
1644
|
if (!client) {
|
|
1455
1645
|
throw new Error(`Upstream package not found: ${packageId}`);
|
|
1456
1646
|
}
|
|
1457
1647
|
let tools = this.schemaCache.get(packageId);
|
|
1458
1648
|
if (!tools) {
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
name: t.name,
|
|
1465
|
-
description: t.description
|
|
1466
|
-
}));
|
|
1467
|
-
if (context.allowedTools) {
|
|
1468
|
-
return stubs2.filter((t) => this.policyService.isToolAllowed(t.id, context.allowedTools));
|
|
1649
|
+
if (typeof client.getManifest === "function") {
|
|
1650
|
+
try {
|
|
1651
|
+
const manifest = await client.getManifest(context);
|
|
1652
|
+
if (manifest && manifest.tools) {
|
|
1653
|
+
tools = manifest.tools;
|
|
1469
1654
|
}
|
|
1470
|
-
|
|
1655
|
+
} catch (e) {
|
|
1656
|
+
this.logger.debug({ upstreamId: packageId, err: e.message }, "Manifest fetch failed (will fallback)");
|
|
1471
1657
|
}
|
|
1472
|
-
} catch (e) {
|
|
1473
|
-
this.logger.debug({ packageId, err: e }, "Manifest fetch failed, falling back to RPC");
|
|
1474
1658
|
}
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1659
|
+
if (!tools) {
|
|
1660
|
+
try {
|
|
1661
|
+
if (typeof client.listTools === "function") {
|
|
1662
|
+
tools = await client.listTools();
|
|
1663
|
+
} else {
|
|
1664
|
+
const response = await client.call({
|
|
1665
|
+
jsonrpc: "2.0",
|
|
1666
|
+
id: "discovery",
|
|
1667
|
+
method: "tools/list"
|
|
1668
|
+
}, context);
|
|
1669
|
+
if (response.result?.tools) {
|
|
1670
|
+
tools = response.result.tools;
|
|
1671
|
+
} else {
|
|
1672
|
+
this.logger.warn({ upstreamId: packageId, error: response.error }, "Failed to discover tools via RPC");
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
} catch (e) {
|
|
1676
|
+
this.logger.error({ upstreamId: packageId, err: e.message }, "Error during tool discovery");
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
if (tools && tools.length > 0) {
|
|
1482
1680
|
this.schemaCache.set(packageId, tools);
|
|
1483
|
-
|
|
1484
|
-
this.logger.warn({ upstreamId: packageId, error: response.error }, "Failed to discover tools from upstream");
|
|
1485
|
-
tools = [];
|
|
1681
|
+
this.logger.info({ upstreamId: packageId, toolCount: tools.length }, "Discovered tools from upstream");
|
|
1486
1682
|
}
|
|
1487
1683
|
}
|
|
1684
|
+
if (!tools) tools = [];
|
|
1488
1685
|
const stubs = tools.map((t) => ({
|
|
1489
1686
|
id: `${packageId}__${t.name}`,
|
|
1490
1687
|
name: t.name,
|
|
@@ -1500,10 +1697,22 @@ var GatewayService = class {
|
|
|
1500
1697
|
throw new Error(`Access to tool ${toolId} is forbidden by allowlist`);
|
|
1501
1698
|
}
|
|
1502
1699
|
const parsed = this.policyService.parseToolName(toolId);
|
|
1700
|
+
const namespace = parsed.namespace;
|
|
1503
1701
|
const toolName = parsed.name;
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1702
|
+
if (namespace === "conduit" || namespace === "") {
|
|
1703
|
+
const builtIn = BUILT_IN_TOOLS.find((t) => t.name === toolName);
|
|
1704
|
+
if (builtIn) {
|
|
1705
|
+
return { ...builtIn, name: `conduit__${builtIn.name}` };
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
const upstreamId = namespace;
|
|
1709
|
+
if (!upstreamId) {
|
|
1710
|
+
for (const id of this.clients.keys()) {
|
|
1711
|
+
const schema = await this.getToolSchema(`${id}__${toolName}`, context);
|
|
1712
|
+
if (schema) return schema;
|
|
1713
|
+
}
|
|
1714
|
+
return null;
|
|
1715
|
+
}
|
|
1507
1716
|
if (!this.schemaCache.get(upstreamId)) {
|
|
1508
1717
|
await this.listToolStubs(upstreamId, context);
|
|
1509
1718
|
}
|
|
@@ -1516,34 +1725,37 @@ var GatewayService = class {
|
|
|
1516
1725
|
};
|
|
1517
1726
|
}
|
|
1518
1727
|
async discoverTools(context) {
|
|
1519
|
-
const allTools =
|
|
1728
|
+
const allTools = BUILT_IN_TOOLS.map((t) => ({
|
|
1729
|
+
...t,
|
|
1730
|
+
name: `conduit__${t.name}`
|
|
1731
|
+
}));
|
|
1732
|
+
this.logger.debug({ clientCount: this.clients.size, clientIds: Array.from(this.clients.keys()) }, "Starting tool discovery");
|
|
1520
1733
|
for (const [id, client] of this.clients.entries()) {
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
},
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1734
|
+
if (id === "host") {
|
|
1735
|
+
continue;
|
|
1736
|
+
}
|
|
1737
|
+
this.logger.debug({ upstreamId: id }, "Discovering tools from upstream");
|
|
1738
|
+
try {
|
|
1739
|
+
await this.listToolStubs(id, context);
|
|
1740
|
+
} catch (e) {
|
|
1741
|
+
this.logger.error({ upstreamId: id, err: e.message }, "Failed to list tool stubs");
|
|
1742
|
+
}
|
|
1743
|
+
const tools = this.schemaCache.get(id) || [];
|
|
1744
|
+
this.logger.debug({ upstreamId: id, toolCount: tools.length }, "Discovery result");
|
|
1745
|
+
if (tools && tools.length > 0) {
|
|
1746
|
+
const prefixedTools = tools.map((t) => ({ ...t, name: `${id}__${t.name}` }));
|
|
1747
|
+
if (context.allowedTools) {
|
|
1748
|
+
allTools.push(...prefixedTools.filter((t) => this.policyService.isToolAllowed(t.name, context.allowedTools)));
|
|
1532
1749
|
} else {
|
|
1533
|
-
|
|
1534
|
-
tools = [];
|
|
1750
|
+
allTools.push(...prefixedTools);
|
|
1535
1751
|
}
|
|
1536
1752
|
}
|
|
1537
|
-
const prefixedTools = tools.map((t) => ({ ...t, name: `${id}__${t.name}` }));
|
|
1538
|
-
if (context.allowedTools) {
|
|
1539
|
-
allTools.push(...prefixedTools.filter((t) => this.policyService.isToolAllowed(t.name, context.allowedTools)));
|
|
1540
|
-
} else {
|
|
1541
|
-
allTools.push(...prefixedTools);
|
|
1542
|
-
}
|
|
1543
1753
|
}
|
|
1754
|
+
this.logger.info({ totalTools: allTools.length }, "Tool discovery complete");
|
|
1544
1755
|
return allTools;
|
|
1545
1756
|
}
|
|
1546
1757
|
async callTool(name, params, context) {
|
|
1758
|
+
this.logger.debug({ name, upstreamCount: this.clients.size }, "GatewayService.callTool called");
|
|
1547
1759
|
if (context.allowedTools && !this.policyService.isToolAllowed(name, context.allowedTools)) {
|
|
1548
1760
|
this.logger.warn({ name, allowedTools: context.allowedTools }, "Tool call blocked by allowlist");
|
|
1549
1761
|
return {
|
|
@@ -1558,14 +1770,37 @@ var GatewayService = class {
|
|
|
1558
1770
|
const toolId = this.policyService.parseToolName(name);
|
|
1559
1771
|
const upstreamId = toolId.namespace;
|
|
1560
1772
|
const toolName = toolId.name;
|
|
1773
|
+
this.logger.debug({ name, upstreamId, toolName }, "Parsed tool name");
|
|
1774
|
+
if (!upstreamId) {
|
|
1775
|
+
this.logger.debug({ toolName }, "Namespaceless call, attempting discovery across upstreams");
|
|
1776
|
+
const allStubs = await this.discoverTools(context);
|
|
1777
|
+
const found = allStubs.find((t) => {
|
|
1778
|
+
const parts = t.name.split("__");
|
|
1779
|
+
return parts[parts.length - 1] === toolName;
|
|
1780
|
+
});
|
|
1781
|
+
if (found) {
|
|
1782
|
+
this.logger.debug({ original: name, resolved: found.name }, "Resolved namespaceless tool");
|
|
1783
|
+
return this.callTool(found.name, params, context);
|
|
1784
|
+
}
|
|
1785
|
+
const upstreamList = Array.from(this.clients.keys()).filter((k) => k !== "host");
|
|
1786
|
+
return {
|
|
1787
|
+
jsonrpc: "2.0",
|
|
1788
|
+
id: 0,
|
|
1789
|
+
error: {
|
|
1790
|
+
code: -32601,
|
|
1791
|
+
message: `Tool '${toolName}' not found. Discovered ${allStubs.length} tools from upstreams: [${upstreamList.join(", ") || "none"}]. Available tools: ${allStubs.map((t) => t.name).slice(0, 10).join(", ")}${allStubs.length > 10 ? "..." : ""}`
|
|
1792
|
+
}
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1561
1795
|
const client = this.clients.get(upstreamId);
|
|
1562
1796
|
if (!client) {
|
|
1797
|
+
this.logger.error({ upstreamId, availableUpstreams: Array.from(this.clients.keys()) }, "Upstream not found");
|
|
1563
1798
|
return {
|
|
1564
1799
|
jsonrpc: "2.0",
|
|
1565
1800
|
id: 0,
|
|
1566
1801
|
error: {
|
|
1567
1802
|
code: -32003,
|
|
1568
|
-
message: `Upstream not found: ${upstreamId}`
|
|
1803
|
+
message: `Upstream not found: '${upstreamId}'. Available: ${Array.from(this.clients.keys()).join(", ") || "none"}`
|
|
1569
1804
|
}
|
|
1570
1805
|
};
|
|
1571
1806
|
}
|
|
@@ -2719,7 +2954,8 @@ var SDKGenerator = class {
|
|
|
2719
2954
|
* Convert camelCase to snake_case for Python
|
|
2720
2955
|
*/
|
|
2721
2956
|
toSnakeCase(str) {
|
|
2722
|
-
|
|
2957
|
+
const snake = str.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "").replace(/[^a-z0-9_]/g, "_");
|
|
2958
|
+
return /^[0-9]/.test(snake) ? `_${snake}` : snake;
|
|
2723
2959
|
}
|
|
2724
2960
|
/**
|
|
2725
2961
|
* Escape a string for use in generated code
|
|
@@ -2788,7 +3024,18 @@ const tools = new Proxy(_tools, {
|
|
|
2788
3024
|
if (prop in target) return target[prop];
|
|
2789
3025
|
if (prop === 'then') return undefined;
|
|
2790
3026
|
if (typeof prop === 'string') {
|
|
2791
|
-
|
|
3027
|
+
// Flat tool access fallback: search all namespaces for a matching tool
|
|
3028
|
+
for (const nsName of Object.keys(target)) {
|
|
3029
|
+
if (nsName === '$raw') continue;
|
|
3030
|
+
const ns = target[nsName];
|
|
3031
|
+
if (ns && typeof ns === 'object' && ns[prop]) {
|
|
3032
|
+
return ns[prop];
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
const forbidden = ['$raw'];
|
|
3037
|
+
const namespaces = Object.keys(target).filter(k => !forbidden.includes(k));
|
|
3038
|
+
throw new Error(\`Namespace or Tool '\${prop}' not found. Available namespaces: \${namespaces.join(', ') || 'none'}. Use tools.$raw(name, args) for dynamic calls.\`);
|
|
2792
3039
|
}
|
|
2793
3040
|
return undefined;
|
|
2794
3041
|
}
|
|
@@ -2815,28 +3062,39 @@ const tools = new Proxy(_tools, {
|
|
|
2815
3062
|
lines.push("_allowed_tools = None");
|
|
2816
3063
|
}
|
|
2817
3064
|
lines.push("");
|
|
2818
|
-
lines.push("class _ToolNamespace:");
|
|
2819
|
-
lines.push(" def __init__(self, methods):");
|
|
2820
|
-
lines.push(" for name, fn in methods.items():");
|
|
2821
|
-
lines.push(" setattr(self, name, fn)");
|
|
2822
|
-
lines.push("");
|
|
2823
|
-
lines.push("class _Tools:");
|
|
2824
|
-
lines.push(" def __init__(self):");
|
|
2825
3065
|
for (const [namespace, tools] of grouped.entries()) {
|
|
2826
3066
|
const safeNamespace = this.toSnakeCase(namespace);
|
|
2827
|
-
|
|
3067
|
+
lines.push(`class _${safeNamespace}_Namespace:`);
|
|
2828
3068
|
for (const tool of tools) {
|
|
2829
3069
|
const methodName = this.toSnakeCase(tool.methodName);
|
|
2830
3070
|
const fullName = tool.name;
|
|
2831
|
-
|
|
3071
|
+
lines.push(` async def ${methodName}(self, args=None, **kwargs):`);
|
|
3072
|
+
lines.push(` params = args if args is not None else kwargs`);
|
|
3073
|
+
lines.push(` return await _internal_call_tool("${this.escapeString(fullName)}", params)`);
|
|
2832
3074
|
}
|
|
2833
|
-
lines.push(
|
|
2834
|
-
lines.push(methodsDict.join(",\n"));
|
|
2835
|
-
lines.push(` })`);
|
|
3075
|
+
lines.push("");
|
|
2836
3076
|
}
|
|
3077
|
+
lines.push("class _Tools:");
|
|
3078
|
+
lines.push(" def __init__(self):");
|
|
3079
|
+
if (grouped.size === 0) {
|
|
3080
|
+
lines.push(" pass");
|
|
3081
|
+
} else {
|
|
3082
|
+
for (const [namespace] of grouped.entries()) {
|
|
3083
|
+
const safeNamespace = this.toSnakeCase(namespace);
|
|
3084
|
+
lines.push(` self.${safeNamespace} = _${safeNamespace}_Namespace()`);
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
lines.push("");
|
|
3088
|
+
lines.push(" def __getattr__(self, name):");
|
|
3089
|
+
lines.push(" # Flat access fallback: search all namespaces");
|
|
3090
|
+
lines.push(" for attr_name in dir(self):");
|
|
3091
|
+
lines.push(" attr = getattr(self, attr_name, None)");
|
|
3092
|
+
lines.push(" if attr and hasattr(attr, name):");
|
|
3093
|
+
lines.push(" return getattr(attr, name)");
|
|
3094
|
+
lines.push(` raise AttributeError(f"Namespace or Tool '{name}' not found")`);
|
|
2837
3095
|
if (enableRawFallback) {
|
|
2838
3096
|
lines.push("");
|
|
2839
|
-
lines.push(" async def raw(self, name, args):");
|
|
3097
|
+
lines.push(" async def raw(self, name, args=None):");
|
|
2840
3098
|
lines.push(' """Call a tool by its full name (escape hatch for dynamic/unknown tools)"""');
|
|
2841
3099
|
lines.push(' normalized = name.replace(".", "__")');
|
|
2842
3100
|
lines.push(" if _allowed_tools is not None:");
|
|
@@ -2846,7 +3104,7 @@ const tools = new Proxy(_tools, {
|
|
|
2846
3104
|
lines.push(" )");
|
|
2847
3105
|
lines.push(" if not allowed:");
|
|
2848
3106
|
lines.push(' raise PermissionError(f"Tool {name} is not in the allowlist")');
|
|
2849
|
-
lines.push(" return await _internal_call_tool(normalized, args)");
|
|
3107
|
+
lines.push(" return await _internal_call_tool(normalized, args or {})");
|
|
2850
3108
|
}
|
|
2851
3109
|
lines.push("");
|
|
2852
3110
|
lines.push("tools = _Tools()");
|
|
@@ -2888,21 +3146,21 @@ const tools = new Proxy(_tools, {
|
|
|
2888
3146
|
lines.push(` return JSON.parse(resStr);`);
|
|
2889
3147
|
lines.push(` },`);
|
|
2890
3148
|
}
|
|
2891
|
-
lines.push(
|
|
3149
|
+
lines.push(" },");
|
|
2892
3150
|
}
|
|
2893
3151
|
if (enableRawFallback) {
|
|
2894
|
-
lines.push(
|
|
3152
|
+
lines.push(" async $raw(name, args) {");
|
|
2895
3153
|
lines.push(` const normalized = name.replace(/\\./g, '__');`);
|
|
2896
|
-
lines.push(
|
|
3154
|
+
lines.push(" if (__allowedTools) {");
|
|
2897
3155
|
lines.push(` const allowed = __allowedTools.some(p => {`);
|
|
2898
3156
|
lines.push(` if (p.endsWith('__*')) return normalized.startsWith(p.slice(0, -1));`);
|
|
2899
|
-
lines.push(
|
|
2900
|
-
lines.push(
|
|
2901
|
-
lines.push(
|
|
2902
|
-
lines.push(
|
|
2903
|
-
lines.push(
|
|
2904
|
-
lines.push(
|
|
2905
|
-
lines.push(
|
|
3157
|
+
lines.push(" return normalized === p;");
|
|
3158
|
+
lines.push(" });");
|
|
3159
|
+
lines.push(" if (!allowed) throw new Error(`Tool ${name} is not in the allowlist`);");
|
|
3160
|
+
lines.push(" }");
|
|
3161
|
+
lines.push(" const resStr = await __callTool(normalized, JSON.stringify(args || {}));");
|
|
3162
|
+
lines.push(" return JSON.parse(resStr);");
|
|
3163
|
+
lines.push(" },");
|
|
2906
3164
|
}
|
|
2907
3165
|
lines.push("};");
|
|
2908
3166
|
lines.push(`
|
|
@@ -2911,7 +3169,18 @@ const tools = new Proxy(_tools, {
|
|
|
2911
3169
|
if (prop in target) return target[prop];
|
|
2912
3170
|
if (prop === 'then') return undefined;
|
|
2913
3171
|
if (typeof prop === 'string') {
|
|
2914
|
-
|
|
3172
|
+
// Flat tool access fallback: search all namespaces for a matching tool
|
|
3173
|
+
for (const nsName of Object.keys(target)) {
|
|
3174
|
+
if (nsName === '$raw') continue;
|
|
3175
|
+
const ns = target[nsName];
|
|
3176
|
+
if (ns && typeof ns === 'object' && ns[prop]) {
|
|
3177
|
+
return ns[prop];
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
const forbidden = ['$raw'];
|
|
3182
|
+
const namespaces = Object.keys(target).filter(k => !forbidden.includes(k));
|
|
3183
|
+
throw new Error(\`Namespace or Tool '\${prop}' not found. Available namespaces: \${namespaces.join(', ') || 'none'}. Use tools.$raw(name, args) for dynamic calls.\`);
|
|
2915
3184
|
}
|
|
2916
3185
|
return undefined;
|
|
2917
3186
|
}
|
|
@@ -3006,14 +3275,17 @@ var ExecutionService = class {
|
|
|
3006
3275
|
async getToolBindings(context) {
|
|
3007
3276
|
const packages = await this.gatewayService.listToolPackages();
|
|
3008
3277
|
const allBindings = [];
|
|
3278
|
+
this.logger.debug({ packageCount: packages.length, packages: packages.map((p) => p.id) }, "Fetching tool bindings");
|
|
3009
3279
|
for (const pkg of packages) {
|
|
3010
3280
|
try {
|
|
3011
3281
|
const stubs = await this.gatewayService.listToolStubs(pkg.id, context);
|
|
3282
|
+
this.logger.debug({ packageId: pkg.id, stubCount: stubs.length }, "Got stubs from package");
|
|
3012
3283
|
allBindings.push(...stubs.map((s) => toToolBinding(s.id, void 0, s.description)));
|
|
3013
3284
|
} catch (err) {
|
|
3014
3285
|
this.logger.warn({ packageId: pkg.id, err: err.message }, "Failed to list stubs for package");
|
|
3015
3286
|
}
|
|
3016
3287
|
}
|
|
3288
|
+
this.logger.info({ totalBindings: allBindings.length }, "Tool bindings ready for SDK generation");
|
|
3017
3289
|
return allBindings;
|
|
3018
3290
|
}
|
|
3019
3291
|
async executeIsolate(code, limits, context, allowedTools) {
|
|
@@ -3282,21 +3554,29 @@ async function handleAuth(options) {
|
|
|
3282
3554
|
return;
|
|
3283
3555
|
}
|
|
3284
3556
|
try {
|
|
3285
|
-
const
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3557
|
+
const payload = {
|
|
3558
|
+
grant_type: "authorization_code",
|
|
3559
|
+
code,
|
|
3560
|
+
redirect_uri: redirectUri,
|
|
3561
|
+
client_id: options.clientId
|
|
3562
|
+
};
|
|
3290
3563
|
if (options.clientSecret) {
|
|
3291
|
-
|
|
3564
|
+
payload.client_secret = options.clientSecret;
|
|
3292
3565
|
}
|
|
3293
3566
|
if (codeVerifier) {
|
|
3294
|
-
|
|
3567
|
+
payload.code_verifier = codeVerifier;
|
|
3295
3568
|
}
|
|
3296
3569
|
if (resolvedResource) {
|
|
3297
|
-
|
|
3570
|
+
payload.resource = resolvedResource;
|
|
3298
3571
|
}
|
|
3299
|
-
const
|
|
3572
|
+
const tokenHostname = new URL(resolvedTokenUrl).hostname;
|
|
3573
|
+
const useJson = tokenHostname === "auth.atlassian.com";
|
|
3574
|
+
const response = useJson ? await axios4.post(resolvedTokenUrl, payload, {
|
|
3575
|
+
headers: {
|
|
3576
|
+
"Content-Type": "application/json",
|
|
3577
|
+
"Accept": "application/json"
|
|
3578
|
+
}
|
|
3579
|
+
}) : await axios4.post(resolvedTokenUrl, new URLSearchParams(payload), {
|
|
3300
3580
|
headers: {
|
|
3301
3581
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
3302
3582
|
"Accept": "application/json"
|
|
@@ -3360,9 +3640,9 @@ async function handleAuth(options) {
|
|
|
3360
3640
|
// src/index.ts
|
|
3361
3641
|
var program = new Command();
|
|
3362
3642
|
program.name("conduit").description("A secure Code Mode execution substrate for MCP agents").version("1.0.0");
|
|
3363
|
-
program.command("serve", { isDefault: true }).description("Start the Conduit server").option("--stdio", "Use stdio transport").action(async (options) => {
|
|
3643
|
+
program.command("serve", { isDefault: true }).description("Start the Conduit server").option("--stdio", "Use stdio transport").option("--config <path>", "Path to config file").action(async (options) => {
|
|
3364
3644
|
try {
|
|
3365
|
-
await startServer();
|
|
3645
|
+
await startServer(options);
|
|
3366
3646
|
} catch (err) {
|
|
3367
3647
|
console.error("Failed to start Conduit:", err);
|
|
3368
3648
|
process.exit(1);
|
|
@@ -3386,8 +3666,11 @@ program.command("auth").description("Help set up OAuth for an upstream MCP serve
|
|
|
3386
3666
|
process.exit(1);
|
|
3387
3667
|
}
|
|
3388
3668
|
});
|
|
3389
|
-
async function startServer() {
|
|
3390
|
-
const
|
|
3669
|
+
async function startServer(options = {}) {
|
|
3670
|
+
const overrides = {};
|
|
3671
|
+
if (options.stdio) overrides.transport = "stdio";
|
|
3672
|
+
if (options.config) process.env.CONFIG_FILE = options.config;
|
|
3673
|
+
const configService = new ConfigService(overrides);
|
|
3391
3674
|
const logger = createLogger(configService);
|
|
3392
3675
|
const otelService = new OtelService(logger);
|
|
3393
3676
|
await otelService.start();
|
|
@@ -3397,6 +3680,7 @@ async function startServer() {
|
|
|
3397
3680
|
const securityService = new SecurityService(logger, ipcToken);
|
|
3398
3681
|
const gatewayService = new GatewayService(logger, securityService);
|
|
3399
3682
|
const upstreams = configService.get("upstreams") || [];
|
|
3683
|
+
logger.info({ upstreamCount: upstreams.length, upstreamIds: upstreams.map((u) => u.id) }, "Registering upstreams from config");
|
|
3400
3684
|
for (const upstream of upstreams) {
|
|
3401
3685
|
gatewayService.registerUpstream(upstream);
|
|
3402
3686
|
}
|
|
@@ -3426,8 +3710,10 @@ async function startServer() {
|
|
|
3426
3710
|
let transport;
|
|
3427
3711
|
let address;
|
|
3428
3712
|
if (configService.get("transport") === "stdio") {
|
|
3429
|
-
|
|
3713
|
+
const stdioTransport = new StdioTransport(logger, requestController, concurrencyService);
|
|
3714
|
+
transport = stdioTransport;
|
|
3430
3715
|
await transport.start();
|
|
3716
|
+
gatewayService.registerHost(stdioTransport);
|
|
3431
3717
|
address = "stdio";
|
|
3432
3718
|
const internalTransport = new SocketTransport(logger, requestController, concurrencyService);
|
|
3433
3719
|
const internalPort = 0;
|