@mhingston5/conduit 1.1.4 → 1.1.6
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 +13 -2
- package/dist/index.js +500 -131
- package/dist/index.js.map +1 -1
- package/dist/pyodide.worker.js.map +1 -0
- package/package.json +1 -1
- package/src/auth.cmd.ts +161 -14
- package/src/core/config.service.ts +5 -1
- package/src/core/execution.service.ts +5 -0
- package/src/core/policy.service.ts +5 -0
- package/src/core/request.controller.ts +80 -7
- package/src/executors/pyodide.executor.ts +9 -4
- package/src/gateway/auth.service.ts +17 -7
- package/src/gateway/gateway.service.ts +150 -73
- package/src/gateway/host.client.ts +65 -0
- package/src/gateway/upstream.client.ts +21 -14
- package/src/index.ts +33 -7
- package/src/sdk/sdk-generator.ts +90 -30
- package/src/transport/stdio.transport.ts +44 -3
- package/tests/__snapshots__/assets.test.ts.snap +56 -3
- package/tests/code-mode-lite-gateway.test.ts +4 -4
- package/tests/debug.fallback.test.ts +40 -0
- package/tests/debug_upstream.ts +69 -0
- package/tests/dynamic.tool.test.ts +3 -3
- package/tests/gateway.manifest.test.ts +1 -1
- package/tests/gateway.service.test.ts +6 -6
- package/tests/reference_mcp.ts +5 -3
- package/tests/routing.test.ts +8 -1
- package/tests/sdk/sdk-generator.test.ts +10 -9
- package/tsup.config.ts +1 -1
- package/dist/executors/pyodide.worker.js.map +0 -1
- /package/dist/{executors/pyodide.worker.d.ts → pyodide.worker.d.ts} +0 -0
- /package/dist/{executors/pyodide.worker.js → pyodide.worker.js} +0 -0
package/dist/index.js
CHANGED
|
@@ -111,9 +111,13 @@ var ConfigService = class {
|
|
|
111
111
|
}
|
|
112
112
|
loadConfigFile() {
|
|
113
113
|
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)
|
|
114
|
+
if (!configPath) {
|
|
115
|
+
console.warn(`[Conduit] No config file found in ${process.cwd()}. Running with default settings.`);
|
|
116
|
+
return {};
|
|
117
|
+
}
|
|
115
118
|
try {
|
|
116
119
|
const fullPath = path.resolve(process.cwd(), configPath);
|
|
120
|
+
console.error(`[Conduit] Loading config from ${fullPath}`);
|
|
117
121
|
let fileContent = fs.readFileSync(fullPath, "utf-8");
|
|
118
122
|
fileContent = fileContent.replace(/\$\{([a-zA-Z0-9_]+)(?::-([^}]+))?\}/g, (match, varName, defaultValue) => {
|
|
119
123
|
const value = process.env[varName];
|
|
@@ -392,6 +396,7 @@ var StdioTransport = class {
|
|
|
392
396
|
requestController;
|
|
393
397
|
concurrencyService;
|
|
394
398
|
buffer = "";
|
|
399
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
395
400
|
constructor(logger, requestController, concurrencyService) {
|
|
396
401
|
this.logger = logger;
|
|
397
402
|
this.requestController = requestController;
|
|
@@ -405,6 +410,30 @@ var StdioTransport = class {
|
|
|
405
410
|
this.logger.info("Stdin closed");
|
|
406
411
|
});
|
|
407
412
|
}
|
|
413
|
+
async callHost(method, params) {
|
|
414
|
+
const id = Math.random().toString(36).substring(7);
|
|
415
|
+
const request = {
|
|
416
|
+
jsonrpc: "2.0",
|
|
417
|
+
id,
|
|
418
|
+
method,
|
|
419
|
+
params
|
|
420
|
+
};
|
|
421
|
+
return new Promise((resolve, reject) => {
|
|
422
|
+
const timeout = setTimeout(() => {
|
|
423
|
+
this.pendingRequests.delete(id);
|
|
424
|
+
reject(new Error(`Timeout waiting for host response to ${method}`));
|
|
425
|
+
}, 3e4);
|
|
426
|
+
this.pendingRequests.set(id, (response) => {
|
|
427
|
+
clearTimeout(timeout);
|
|
428
|
+
if (response.error) {
|
|
429
|
+
reject(new Error(response.error.message));
|
|
430
|
+
} else {
|
|
431
|
+
resolve(response.result);
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
this.sendResponse(request);
|
|
435
|
+
});
|
|
436
|
+
}
|
|
408
437
|
handleData(chunk) {
|
|
409
438
|
this.buffer += chunk;
|
|
410
439
|
let pos;
|
|
@@ -416,11 +445,11 @@ var StdioTransport = class {
|
|
|
416
445
|
}
|
|
417
446
|
}
|
|
418
447
|
async processLine(line) {
|
|
419
|
-
let
|
|
448
|
+
let message;
|
|
420
449
|
try {
|
|
421
|
-
|
|
450
|
+
message = JSON.parse(line);
|
|
422
451
|
} catch (err) {
|
|
423
|
-
this.logger.error({ err, line }, "Failed to parse JSON-RPC
|
|
452
|
+
this.logger.error({ err, line }, "Failed to parse JSON-RPC message");
|
|
424
453
|
const errorResponse = {
|
|
425
454
|
jsonrpc: "2.0",
|
|
426
455
|
id: null,
|
|
@@ -432,6 +461,15 @@ var StdioTransport = class {
|
|
|
432
461
|
this.sendResponse(errorResponse);
|
|
433
462
|
return;
|
|
434
463
|
}
|
|
464
|
+
if (message.id !== void 0 && (message.result !== void 0 || message.error !== void 0)) {
|
|
465
|
+
const pending = this.pendingRequests.get(message.id);
|
|
466
|
+
if (pending) {
|
|
467
|
+
this.pendingRequests.delete(message.id);
|
|
468
|
+
pending(message);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
const request = message;
|
|
435
473
|
const context = new ExecutionContext({
|
|
436
474
|
logger: this.logger,
|
|
437
475
|
remoteAddress: "stdio"
|
|
@@ -750,6 +788,9 @@ var RequestController = class {
|
|
|
750
788
|
// Standard MCP method name
|
|
751
789
|
case "mcp_discover_tools":
|
|
752
790
|
return this.handleDiscoverTools(params, context, id);
|
|
791
|
+
case "resources/list":
|
|
792
|
+
case "prompts/list":
|
|
793
|
+
return { jsonrpc: "2.0", id, result: { items: [] } };
|
|
753
794
|
case "mcp_list_tool_packages":
|
|
754
795
|
return this.handleListToolPackages(params, context, id);
|
|
755
796
|
case "mcp_list_tool_stubs":
|
|
@@ -772,12 +813,29 @@ var RequestController = class {
|
|
|
772
813
|
case "notifications/initialized":
|
|
773
814
|
return null;
|
|
774
815
|
// Notifications don't get responses per MCP spec
|
|
816
|
+
case "mcp_register_upstream":
|
|
817
|
+
return this.handleRegisterUpstream(params, context, id);
|
|
775
818
|
case "ping":
|
|
776
819
|
return { jsonrpc: "2.0", id, result: {} };
|
|
777
820
|
default:
|
|
778
821
|
return this.errorResponse(id, -32601, `Method not found: ${method}`);
|
|
779
822
|
}
|
|
780
823
|
}
|
|
824
|
+
async handleRegisterUpstream(params, context, id) {
|
|
825
|
+
if (!params || !params.id || !params.type || !params.url && !params.command) {
|
|
826
|
+
return this.errorResponse(id, -32602, "Missing registration parameters (id, type, url/command)");
|
|
827
|
+
}
|
|
828
|
+
try {
|
|
829
|
+
this.gatewayService.registerUpstream(params);
|
|
830
|
+
return {
|
|
831
|
+
jsonrpc: "2.0",
|
|
832
|
+
id,
|
|
833
|
+
result: { success: true }
|
|
834
|
+
};
|
|
835
|
+
} catch (err) {
|
|
836
|
+
return this.errorResponse(id, -32001, err.message);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
781
839
|
async handleDiscoverTools(params, context, id) {
|
|
782
840
|
const tools = await this.gatewayService.discoverTools(context);
|
|
783
841
|
const standardizedTools = tools.map((t) => ({
|
|
@@ -845,17 +903,52 @@ var RequestController = class {
|
|
|
845
903
|
async handleCallTool(params, context, id) {
|
|
846
904
|
if (!params) return this.errorResponse(id, -32602, "Missing parameters");
|
|
847
905
|
const { name, arguments: toolArgs } = params;
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
906
|
+
const toolId = this.gatewayService.policyService.parseToolName(name);
|
|
907
|
+
const baseName = toolId.name;
|
|
908
|
+
const isConduit = toolId.namespace === "conduit" || toolId.namespace === "";
|
|
909
|
+
if (isConduit) {
|
|
910
|
+
switch (baseName) {
|
|
911
|
+
case "mcp_execute_typescript":
|
|
912
|
+
return this.handleExecuteToolCall("typescript", toolArgs, context, id);
|
|
913
|
+
case "mcp_execute_python":
|
|
914
|
+
return this.handleExecuteToolCall("python", toolArgs, context, id);
|
|
915
|
+
case "mcp_execute_isolate":
|
|
916
|
+
return this.handleExecuteToolCall("isolate", toolArgs, context, id);
|
|
917
|
+
}
|
|
855
918
|
}
|
|
856
919
|
const response = await this.gatewayService.callTool(name, toolArgs, context);
|
|
857
920
|
return { ...response, id };
|
|
858
921
|
}
|
|
922
|
+
formatExecutionResult(result) {
|
|
923
|
+
const structured = {
|
|
924
|
+
stdout: result.stdout,
|
|
925
|
+
stderr: result.stderr,
|
|
926
|
+
exitCode: result.exitCode
|
|
927
|
+
};
|
|
928
|
+
return {
|
|
929
|
+
content: [{
|
|
930
|
+
type: "text",
|
|
931
|
+
text: JSON.stringify(structured)
|
|
932
|
+
}],
|
|
933
|
+
structuredContent: structured
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
async handleExecuteToolCall(mode, params, context, id) {
|
|
937
|
+
if (!params) return this.errorResponse(id, -32602, "Missing parameters");
|
|
938
|
+
const { code, limits, allowedTools } = params;
|
|
939
|
+
if (Array.isArray(allowedTools)) {
|
|
940
|
+
context.allowedTools = allowedTools;
|
|
941
|
+
}
|
|
942
|
+
const result = mode === "typescript" ? await this.executionService.executeTypeScript(code, limits, context, allowedTools) : mode === "python" ? await this.executionService.executePython(code, limits, context, allowedTools) : await this.executionService.executeIsolate(code, limits, context, allowedTools);
|
|
943
|
+
if (result.error) {
|
|
944
|
+
return this.errorResponse(id, result.error.code, result.error.message);
|
|
945
|
+
}
|
|
946
|
+
return {
|
|
947
|
+
jsonrpc: "2.0",
|
|
948
|
+
id,
|
|
949
|
+
result: this.formatExecutionResult(result)
|
|
950
|
+
};
|
|
951
|
+
}
|
|
859
952
|
async handleExecuteTypeScript(params, context, id) {
|
|
860
953
|
if (!params) return this.errorResponse(id, -32602, "Missing parameters");
|
|
861
954
|
const { code, limits, allowedTools } = params;
|
|
@@ -976,6 +1069,7 @@ var UpstreamClient = class {
|
|
|
976
1069
|
urlValidator;
|
|
977
1070
|
mcpClient;
|
|
978
1071
|
transport;
|
|
1072
|
+
connected = false;
|
|
979
1073
|
constructor(logger, info, authService, urlValidator) {
|
|
980
1074
|
this.logger = logger.child({ upstreamId: info.id });
|
|
981
1075
|
this.info = info;
|
|
@@ -1002,11 +1096,15 @@ var UpstreamClient = class {
|
|
|
1002
1096
|
}
|
|
1003
1097
|
async ensureConnected() {
|
|
1004
1098
|
if (!this.mcpClient || !this.transport) return;
|
|
1099
|
+
if (this.connected) return;
|
|
1005
1100
|
try {
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1101
|
+
this.logger.debug("Connecting to upstream transport...");
|
|
1102
|
+
await this.mcpClient.connect(this.transport);
|
|
1103
|
+
this.connected = true;
|
|
1104
|
+
this.logger.info("Connected to upstream MCP");
|
|
1009
1105
|
} catch (e) {
|
|
1106
|
+
this.logger.error({ err: e.message }, "Failed to connect to upstream");
|
|
1107
|
+
throw e;
|
|
1010
1108
|
}
|
|
1011
1109
|
}
|
|
1012
1110
|
async call(request, context) {
|
|
@@ -1023,23 +1121,29 @@ var UpstreamClient = class {
|
|
|
1023
1121
|
}
|
|
1024
1122
|
try {
|
|
1025
1123
|
await this.ensureConnected();
|
|
1026
|
-
if (request.method === "list_tools") {
|
|
1124
|
+
if (request.method === "list_tools" || request.method === "tools/list") {
|
|
1027
1125
|
const result = await this.mcpClient.listTools();
|
|
1028
1126
|
return {
|
|
1029
1127
|
jsonrpc: "2.0",
|
|
1030
1128
|
id: request.id,
|
|
1031
1129
|
result
|
|
1032
1130
|
};
|
|
1033
|
-
} else if (request.method === "call_tool") {
|
|
1131
|
+
} else if (request.method === "call_tool" || request.method === "tools/call") {
|
|
1034
1132
|
const params = request.params;
|
|
1035
1133
|
const result = await this.mcpClient.callTool({
|
|
1036
1134
|
name: params.name,
|
|
1037
1135
|
arguments: params.arguments
|
|
1038
1136
|
});
|
|
1137
|
+
const normalizedResult = result && Array.isArray(result.content) ? result : {
|
|
1138
|
+
content: [{
|
|
1139
|
+
type: "text",
|
|
1140
|
+
text: typeof result === "string" ? result : JSON.stringify(result ?? null)
|
|
1141
|
+
}]
|
|
1142
|
+
};
|
|
1039
1143
|
return {
|
|
1040
1144
|
jsonrpc: "2.0",
|
|
1041
1145
|
id: request.id,
|
|
1042
|
-
result
|
|
1146
|
+
result: normalizedResult
|
|
1043
1147
|
};
|
|
1044
1148
|
} else {
|
|
1045
1149
|
const result = await this.mcpClient.request(
|
|
@@ -1193,21 +1297,29 @@ var AuthService = class {
|
|
|
1193
1297
|
}
|
|
1194
1298
|
}
|
|
1195
1299
|
async doRefresh(creds, cacheKey) {
|
|
1196
|
-
if (!creds.tokenUrl || !creds.refreshToken || !creds.clientId
|
|
1300
|
+
if (!creds.tokenUrl || !creds.refreshToken || !creds.clientId) {
|
|
1197
1301
|
throw new Error("OAuth2 credentials missing required fields for refresh");
|
|
1198
1302
|
}
|
|
1199
1303
|
this.logger.info({ tokenUrl: creds.tokenUrl, clientId: creds.clientId }, "Refreshing OAuth2 token");
|
|
1200
1304
|
try {
|
|
1201
|
-
const
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1305
|
+
const body = new URLSearchParams();
|
|
1306
|
+
body.set("grant_type", "refresh_token");
|
|
1307
|
+
body.set("refresh_token", creds.refreshToken);
|
|
1308
|
+
body.set("client_id", creds.clientId);
|
|
1309
|
+
if (creds.clientSecret) {
|
|
1310
|
+
body.set("client_secret", creds.clientSecret);
|
|
1311
|
+
}
|
|
1312
|
+
const response = await axios3.post(creds.tokenUrl, body, {
|
|
1313
|
+
headers: {
|
|
1314
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1315
|
+
"Accept": "application/json"
|
|
1316
|
+
}
|
|
1206
1317
|
});
|
|
1207
1318
|
const { access_token, expires_in } = response.data;
|
|
1319
|
+
const expiresInSeconds = Number(expires_in) || 3600;
|
|
1208
1320
|
this.tokenCache.set(cacheKey, {
|
|
1209
1321
|
accessToken: access_token,
|
|
1210
|
-
expiresAt: Date.now() +
|
|
1322
|
+
expiresAt: Date.now() + expiresInSeconds * 1e3
|
|
1211
1323
|
});
|
|
1212
1324
|
return `Bearer ${access_token}`;
|
|
1213
1325
|
} catch (err) {
|
|
@@ -1299,6 +1411,9 @@ var PolicyService = class {
|
|
|
1299
1411
|
}
|
|
1300
1412
|
return true;
|
|
1301
1413
|
}
|
|
1414
|
+
if (patternParts.length === 1 && toolParts.length > 1) {
|
|
1415
|
+
return patternParts[0] === toolParts[toolParts.length - 1];
|
|
1416
|
+
}
|
|
1302
1417
|
if (patternParts.length !== toolParts.length) return false;
|
|
1303
1418
|
for (let i = 0; i < patternParts.length; i++) {
|
|
1304
1419
|
if (patternParts[i] !== toolParts[i]) return false;
|
|
@@ -1314,7 +1429,7 @@ import addFormats from "ajv-formats";
|
|
|
1314
1429
|
var BUILT_IN_TOOLS = [
|
|
1315
1430
|
{
|
|
1316
1431
|
name: "mcp_execute_typescript",
|
|
1317
|
-
description: "Executes TypeScript code in a secure sandbox
|
|
1432
|
+
description: "Executes TypeScript code in a secure sandbox. Access MCP tools via the global `tools` object (e.g. `filesystem__list_directory` -> `await tools.filesystem.list_directory(...)`).",
|
|
1318
1433
|
inputSchema: {
|
|
1319
1434
|
type: "object",
|
|
1320
1435
|
properties: {
|
|
@@ -1325,7 +1440,7 @@ var BUILT_IN_TOOLS = [
|
|
|
1325
1440
|
allowedTools: {
|
|
1326
1441
|
type: "array",
|
|
1327
1442
|
items: { type: "string" },
|
|
1328
|
-
description: '
|
|
1443
|
+
description: 'List of tool names (e.g. "filesystem.list_directory" or "filesystem.*") that the script is allowed to call.'
|
|
1329
1444
|
}
|
|
1330
1445
|
},
|
|
1331
1446
|
required: ["code"]
|
|
@@ -1333,7 +1448,7 @@ var BUILT_IN_TOOLS = [
|
|
|
1333
1448
|
},
|
|
1334
1449
|
{
|
|
1335
1450
|
name: "mcp_execute_python",
|
|
1336
|
-
description: "Executes Python code in a secure sandbox
|
|
1451
|
+
description: "Executes Python code in a secure sandbox. Access MCP tools via the global `tools` object (e.g. `filesystem__list_directory` -> `await tools.filesystem.list_directory(...)`).",
|
|
1337
1452
|
inputSchema: {
|
|
1338
1453
|
type: "object",
|
|
1339
1454
|
properties: {
|
|
@@ -1344,7 +1459,7 @@ var BUILT_IN_TOOLS = [
|
|
|
1344
1459
|
allowedTools: {
|
|
1345
1460
|
type: "array",
|
|
1346
1461
|
items: { type: "string" },
|
|
1347
|
-
description: '
|
|
1462
|
+
description: 'List of tool names (e.g. "filesystem.list_directory" or "filesystem.*") that the script is allowed to call.'
|
|
1348
1463
|
}
|
|
1349
1464
|
},
|
|
1350
1465
|
required: ["code"]
|
|
@@ -1352,7 +1467,7 @@ var BUILT_IN_TOOLS = [
|
|
|
1352
1467
|
},
|
|
1353
1468
|
{
|
|
1354
1469
|
name: "mcp_execute_isolate",
|
|
1355
|
-
description: "Executes JavaScript code in a high-speed V8 isolate (
|
|
1470
|
+
description: "Executes JavaScript code in a high-speed V8 isolate. Access MCP tools via the global `tools` object (e.g. `await tools.filesystem.list_directory(...)`). No Deno/Node APIs. Use `console.log` for output.",
|
|
1356
1471
|
inputSchema: {
|
|
1357
1472
|
type: "object",
|
|
1358
1473
|
properties: {
|
|
@@ -1363,7 +1478,7 @@ var BUILT_IN_TOOLS = [
|
|
|
1363
1478
|
allowedTools: {
|
|
1364
1479
|
type: "array",
|
|
1365
1480
|
items: { type: "string" },
|
|
1366
|
-
description: "
|
|
1481
|
+
description: 'List of tool names (e.g. "filesystem.list_directory" or "filesystem.*") that the script is allowed to call.'
|
|
1367
1482
|
}
|
|
1368
1483
|
},
|
|
1369
1484
|
required: ["code"]
|
|
@@ -1381,7 +1496,8 @@ var GatewayService = class {
|
|
|
1381
1496
|
// Cache compiled validators to avoid recompilation on every call
|
|
1382
1497
|
validatorCache = /* @__PURE__ */ new Map();
|
|
1383
1498
|
constructor(logger, urlValidator, policyService) {
|
|
1384
|
-
this.logger = logger;
|
|
1499
|
+
this.logger = logger.child({ component: "GatewayService" });
|
|
1500
|
+
this.logger.debug("GatewayService instance created");
|
|
1385
1501
|
this.urlValidator = urlValidator;
|
|
1386
1502
|
this.authService = new AuthService(logger);
|
|
1387
1503
|
this.schemaCache = new SchemaCache(logger);
|
|
@@ -1392,17 +1508,37 @@ var GatewayService = class {
|
|
|
1392
1508
|
registerUpstream(info) {
|
|
1393
1509
|
const client = new UpstreamClient(this.logger, info, this.authService, this.urlValidator);
|
|
1394
1510
|
this.clients.set(info.id, client);
|
|
1395
|
-
this.logger.info({ upstreamId: info.id }, "Registered upstream MCP");
|
|
1511
|
+
this.logger.info({ upstreamId: info.id, totalRegistered: this.clients.size }, "Registered upstream MCP");
|
|
1512
|
+
}
|
|
1513
|
+
registerHost(transport) {
|
|
1514
|
+
this.logger.debug("Host transport available but not registered as tool upstream (protocol limitation)");
|
|
1396
1515
|
}
|
|
1397
1516
|
async listToolPackages() {
|
|
1398
|
-
|
|
1517
|
+
const upstreams = Array.from(this.clients.entries()).map(([id, client]) => ({
|
|
1399
1518
|
id,
|
|
1400
1519
|
description: `Upstream ${id}`,
|
|
1401
|
-
// NOTE: Upstream description fetching deferred to V2
|
|
1402
1520
|
version: "1.0.0"
|
|
1403
1521
|
}));
|
|
1522
|
+
return [
|
|
1523
|
+
{ id: "conduit", description: "Conduit built-in execution tools", version: "1.0.0" },
|
|
1524
|
+
...upstreams
|
|
1525
|
+
];
|
|
1526
|
+
}
|
|
1527
|
+
getBuiltInTools() {
|
|
1528
|
+
return BUILT_IN_TOOLS;
|
|
1404
1529
|
}
|
|
1405
1530
|
async listToolStubs(packageId, context) {
|
|
1531
|
+
if (packageId === "conduit") {
|
|
1532
|
+
const stubs2 = BUILT_IN_TOOLS.map((t) => ({
|
|
1533
|
+
id: `conduit__${t.name}`,
|
|
1534
|
+
name: t.name,
|
|
1535
|
+
description: t.description
|
|
1536
|
+
}));
|
|
1537
|
+
if (context.allowedTools) {
|
|
1538
|
+
return stubs2.filter((t) => this.policyService.isToolAllowed(t.id, context.allowedTools));
|
|
1539
|
+
}
|
|
1540
|
+
return stubs2;
|
|
1541
|
+
}
|
|
1406
1542
|
const client = this.clients.get(packageId);
|
|
1407
1543
|
if (!client) {
|
|
1408
1544
|
throw new Error(`Upstream package not found: ${packageId}`);
|
|
@@ -1411,33 +1547,33 @@ var GatewayService = class {
|
|
|
1411
1547
|
if (!tools) {
|
|
1412
1548
|
try {
|
|
1413
1549
|
const manifest = await client.getManifest(context);
|
|
1414
|
-
if (manifest) {
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
|
|
1550
|
+
if (manifest && manifest.tools) {
|
|
1551
|
+
tools = manifest.tools;
|
|
1552
|
+
} else {
|
|
1553
|
+
if (typeof client.listTools === "function") {
|
|
1554
|
+
tools = await client.listTools();
|
|
1555
|
+
} else {
|
|
1556
|
+
const response = await client.call({
|
|
1557
|
+
jsonrpc: "2.0",
|
|
1558
|
+
id: "discovery",
|
|
1559
|
+
method: "tools/list"
|
|
1560
|
+
}, context);
|
|
1561
|
+
if (response.result?.tools) {
|
|
1562
|
+
tools = response.result.tools;
|
|
1563
|
+
} else {
|
|
1564
|
+
this.logger.warn({ upstreamId: packageId, error: response.error }, "Failed to discover tools via RPC");
|
|
1565
|
+
}
|
|
1422
1566
|
}
|
|
1423
|
-
|
|
1567
|
+
}
|
|
1568
|
+
if (tools && tools.length > 0) {
|
|
1569
|
+
this.schemaCache.set(packageId, tools);
|
|
1570
|
+
this.logger.info({ upstreamId: packageId, toolCount: tools.length }, "Discovered tools from upstream");
|
|
1424
1571
|
}
|
|
1425
1572
|
} catch (e) {
|
|
1426
|
-
this.logger.
|
|
1427
|
-
}
|
|
1428
|
-
const response = await client.call({
|
|
1429
|
-
jsonrpc: "2.0",
|
|
1430
|
-
id: "discovery",
|
|
1431
|
-
method: "list_tools"
|
|
1432
|
-
}, context);
|
|
1433
|
-
if (response.result?.tools) {
|
|
1434
|
-
tools = response.result.tools;
|
|
1435
|
-
this.schemaCache.set(packageId, tools);
|
|
1436
|
-
} else {
|
|
1437
|
-
this.logger.warn({ upstreamId: packageId, error: response.error }, "Failed to discover tools from upstream");
|
|
1438
|
-
tools = [];
|
|
1573
|
+
this.logger.error({ upstreamId: packageId, err: e.message }, "Error during tool discovery");
|
|
1439
1574
|
}
|
|
1440
1575
|
}
|
|
1576
|
+
if (!tools) tools = [];
|
|
1441
1577
|
const stubs = tools.map((t) => ({
|
|
1442
1578
|
id: `${packageId}__${t.name}`,
|
|
1443
1579
|
name: t.name,
|
|
@@ -1453,10 +1589,22 @@ var GatewayService = class {
|
|
|
1453
1589
|
throw new Error(`Access to tool ${toolId} is forbidden by allowlist`);
|
|
1454
1590
|
}
|
|
1455
1591
|
const parsed = this.policyService.parseToolName(toolId);
|
|
1592
|
+
const namespace = parsed.namespace;
|
|
1456
1593
|
const toolName = parsed.name;
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1594
|
+
if (namespace === "conduit" || namespace === "") {
|
|
1595
|
+
const builtIn = BUILT_IN_TOOLS.find((t) => t.name === toolName);
|
|
1596
|
+
if (builtIn) {
|
|
1597
|
+
return { ...builtIn, name: `conduit__${builtIn.name}` };
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
const upstreamId = namespace;
|
|
1601
|
+
if (!upstreamId) {
|
|
1602
|
+
for (const id of this.clients.keys()) {
|
|
1603
|
+
const schema = await this.getToolSchema(`${id}__${toolName}`, context);
|
|
1604
|
+
if (schema) return schema;
|
|
1605
|
+
}
|
|
1606
|
+
return null;
|
|
1607
|
+
}
|
|
1460
1608
|
if (!this.schemaCache.get(upstreamId)) {
|
|
1461
1609
|
await this.listToolStubs(upstreamId, context);
|
|
1462
1610
|
}
|
|
@@ -1469,34 +1617,37 @@ var GatewayService = class {
|
|
|
1469
1617
|
};
|
|
1470
1618
|
}
|
|
1471
1619
|
async discoverTools(context) {
|
|
1472
|
-
const allTools =
|
|
1620
|
+
const allTools = BUILT_IN_TOOLS.map((t) => ({
|
|
1621
|
+
...t,
|
|
1622
|
+
name: `conduit__${t.name}`
|
|
1623
|
+
}));
|
|
1624
|
+
this.logger.debug({ clientCount: this.clients.size, clientIds: Array.from(this.clients.keys()) }, "Starting tool discovery");
|
|
1473
1625
|
for (const [id, client] of this.clients.entries()) {
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
},
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1626
|
+
if (id === "host") {
|
|
1627
|
+
continue;
|
|
1628
|
+
}
|
|
1629
|
+
this.logger.debug({ upstreamId: id }, "Discovering tools from upstream");
|
|
1630
|
+
try {
|
|
1631
|
+
await this.listToolStubs(id, context);
|
|
1632
|
+
} catch (e) {
|
|
1633
|
+
this.logger.error({ upstreamId: id, err: e.message }, "Failed to list tool stubs");
|
|
1634
|
+
}
|
|
1635
|
+
const tools = this.schemaCache.get(id) || [];
|
|
1636
|
+
this.logger.debug({ upstreamId: id, toolCount: tools.length }, "Discovery result");
|
|
1637
|
+
if (tools && tools.length > 0) {
|
|
1638
|
+
const prefixedTools = tools.map((t) => ({ ...t, name: `${id}__${t.name}` }));
|
|
1639
|
+
if (context.allowedTools) {
|
|
1640
|
+
allTools.push(...prefixedTools.filter((t) => this.policyService.isToolAllowed(t.name, context.allowedTools)));
|
|
1485
1641
|
} else {
|
|
1486
|
-
|
|
1487
|
-
tools = [];
|
|
1642
|
+
allTools.push(...prefixedTools);
|
|
1488
1643
|
}
|
|
1489
1644
|
}
|
|
1490
|
-
const prefixedTools = tools.map((t) => ({ ...t, name: `${id}__${t.name}` }));
|
|
1491
|
-
if (context.allowedTools) {
|
|
1492
|
-
allTools.push(...prefixedTools.filter((t) => this.policyService.isToolAllowed(t.name, context.allowedTools)));
|
|
1493
|
-
} else {
|
|
1494
|
-
allTools.push(...prefixedTools);
|
|
1495
|
-
}
|
|
1496
1645
|
}
|
|
1646
|
+
this.logger.info({ totalTools: allTools.length }, "Tool discovery complete");
|
|
1497
1647
|
return allTools;
|
|
1498
1648
|
}
|
|
1499
1649
|
async callTool(name, params, context) {
|
|
1650
|
+
this.logger.debug({ name, upstreamCount: this.clients.size }, "GatewayService.callTool called");
|
|
1500
1651
|
if (context.allowedTools && !this.policyService.isToolAllowed(name, context.allowedTools)) {
|
|
1501
1652
|
this.logger.warn({ name, allowedTools: context.allowedTools }, "Tool call blocked by allowlist");
|
|
1502
1653
|
return {
|
|
@@ -1511,14 +1662,37 @@ var GatewayService = class {
|
|
|
1511
1662
|
const toolId = this.policyService.parseToolName(name);
|
|
1512
1663
|
const upstreamId = toolId.namespace;
|
|
1513
1664
|
const toolName = toolId.name;
|
|
1665
|
+
this.logger.debug({ name, upstreamId, toolName }, "Parsed tool name");
|
|
1666
|
+
if (!upstreamId) {
|
|
1667
|
+
this.logger.debug({ toolName }, "Namespaceless call, attempting discovery across upstreams");
|
|
1668
|
+
const allStubs = await this.discoverTools(context);
|
|
1669
|
+
const found = allStubs.find((t) => {
|
|
1670
|
+
const parts = t.name.split("__");
|
|
1671
|
+
return parts[parts.length - 1] === toolName;
|
|
1672
|
+
});
|
|
1673
|
+
if (found) {
|
|
1674
|
+
this.logger.debug({ original: name, resolved: found.name }, "Resolved namespaceless tool");
|
|
1675
|
+
return this.callTool(found.name, params, context);
|
|
1676
|
+
}
|
|
1677
|
+
const upstreamList = Array.from(this.clients.keys()).filter((k) => k !== "host");
|
|
1678
|
+
return {
|
|
1679
|
+
jsonrpc: "2.0",
|
|
1680
|
+
id: 0,
|
|
1681
|
+
error: {
|
|
1682
|
+
code: -32601,
|
|
1683
|
+
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 ? "..." : ""}`
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1514
1687
|
const client = this.clients.get(upstreamId);
|
|
1515
1688
|
if (!client) {
|
|
1689
|
+
this.logger.error({ upstreamId, availableUpstreams: Array.from(this.clients.keys()) }, "Upstream not found");
|
|
1516
1690
|
return {
|
|
1517
1691
|
jsonrpc: "2.0",
|
|
1518
1692
|
id: 0,
|
|
1519
1693
|
error: {
|
|
1520
1694
|
code: -32003,
|
|
1521
|
-
message: `Upstream not found: ${upstreamId}`
|
|
1695
|
+
message: `Upstream not found: '${upstreamId}'. Available: ${Array.from(this.clients.keys()).join(", ") || "none"}`
|
|
1522
1696
|
}
|
|
1523
1697
|
};
|
|
1524
1698
|
}
|
|
@@ -1578,7 +1752,7 @@ var GatewayService = class {
|
|
|
1578
1752
|
response = await client.call({
|
|
1579
1753
|
jsonrpc: "2.0",
|
|
1580
1754
|
id: context.correlationId,
|
|
1581
|
-
method: "
|
|
1755
|
+
method: "tools/call",
|
|
1582
1756
|
params: {
|
|
1583
1757
|
name: toolName,
|
|
1584
1758
|
arguments: params
|
|
@@ -1606,7 +1780,7 @@ var GatewayService = class {
|
|
|
1606
1780
|
const response = await client.call({
|
|
1607
1781
|
jsonrpc: "2.0",
|
|
1608
1782
|
id: "health",
|
|
1609
|
-
method: "
|
|
1783
|
+
method: "tools/list"
|
|
1610
1784
|
}, context);
|
|
1611
1785
|
upstreamStatus[id] = response.error ? "degraded" : "active";
|
|
1612
1786
|
} catch (err) {
|
|
@@ -2213,9 +2387,15 @@ var PyodideExecutor = class {
|
|
|
2213
2387
|
});
|
|
2214
2388
|
}
|
|
2215
2389
|
createWorker(limits) {
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2390
|
+
const candidates = [
|
|
2391
|
+
path5.resolve(__dirname3, "./pyodide.worker.js"),
|
|
2392
|
+
path5.resolve(__dirname3, "./pyodide.worker.ts"),
|
|
2393
|
+
path5.resolve(__dirname3, "./executors/pyodide.worker.js"),
|
|
2394
|
+
path5.resolve(__dirname3, "./executors/pyodide.worker.ts")
|
|
2395
|
+
];
|
|
2396
|
+
const workerPath = candidates.find((p) => fs5.existsSync(p));
|
|
2397
|
+
if (!workerPath) {
|
|
2398
|
+
throw new Error(`Pyodide worker not found. Tried: ${candidates.join(", ")}`);
|
|
2219
2399
|
}
|
|
2220
2400
|
return new Worker(workerPath, {
|
|
2221
2401
|
execArgv: process.execArgv.includes("--loader") ? process.execArgv : [],
|
|
@@ -2666,7 +2846,8 @@ var SDKGenerator = class {
|
|
|
2666
2846
|
* Convert camelCase to snake_case for Python
|
|
2667
2847
|
*/
|
|
2668
2848
|
toSnakeCase(str) {
|
|
2669
|
-
|
|
2849
|
+
const snake = str.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "").replace(/[^a-z0-9_]/g, "_");
|
|
2850
|
+
return /^[0-9]/.test(snake) ? `_${snake}` : snake;
|
|
2670
2851
|
}
|
|
2671
2852
|
/**
|
|
2672
2853
|
* Escape a string for use in generated code
|
|
@@ -2691,7 +2872,7 @@ var SDKGenerator = class {
|
|
|
2691
2872
|
} else {
|
|
2692
2873
|
lines.push("const __allowedTools = null;");
|
|
2693
2874
|
}
|
|
2694
|
-
lines.push("const
|
|
2875
|
+
lines.push("const _tools = {");
|
|
2695
2876
|
for (const [namespace, tools] of grouped.entries()) {
|
|
2696
2877
|
const safeNamespace = this.isValidIdentifier(namespace) ? namespace : `["${this.escapeString(namespace)}"]`;
|
|
2697
2878
|
if (this.isValidIdentifier(namespace)) {
|
|
@@ -2729,6 +2910,29 @@ var SDKGenerator = class {
|
|
|
2729
2910
|
lines.push(` },`);
|
|
2730
2911
|
}
|
|
2731
2912
|
lines.push("};");
|
|
2913
|
+
lines.push(`
|
|
2914
|
+
const tools = new Proxy(_tools, {
|
|
2915
|
+
get: (target, prop) => {
|
|
2916
|
+
if (prop in target) return target[prop];
|
|
2917
|
+
if (prop === 'then') return undefined;
|
|
2918
|
+
if (typeof prop === 'string') {
|
|
2919
|
+
// Flat tool access fallback: search all namespaces for a matching tool
|
|
2920
|
+
for (const nsName of Object.keys(target)) {
|
|
2921
|
+
if (nsName === '$raw') continue;
|
|
2922
|
+
const ns = target[nsName];
|
|
2923
|
+
if (ns && typeof ns === 'object' && ns[prop]) {
|
|
2924
|
+
return ns[prop];
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
const forbidden = ['$raw'];
|
|
2929
|
+
const namespaces = Object.keys(target).filter(k => !forbidden.includes(k));
|
|
2930
|
+
throw new Error(\`Namespace or Tool '\${prop}' not found. Available namespaces: \${namespaces.join(', ') || 'none'}. Use tools.$raw(name, args) for dynamic calls.\`);
|
|
2931
|
+
}
|
|
2932
|
+
return undefined;
|
|
2933
|
+
}
|
|
2934
|
+
});
|
|
2935
|
+
`);
|
|
2732
2936
|
lines.push("(globalThis as any).tools = tools;");
|
|
2733
2937
|
return lines.join("\n");
|
|
2734
2938
|
}
|
|
@@ -2750,28 +2954,39 @@ var SDKGenerator = class {
|
|
|
2750
2954
|
lines.push("_allowed_tools = None");
|
|
2751
2955
|
}
|
|
2752
2956
|
lines.push("");
|
|
2753
|
-
lines.push("class _ToolNamespace:");
|
|
2754
|
-
lines.push(" def __init__(self, methods):");
|
|
2755
|
-
lines.push(" for name, fn in methods.items():");
|
|
2756
|
-
lines.push(" setattr(self, name, fn)");
|
|
2757
|
-
lines.push("");
|
|
2758
|
-
lines.push("class _Tools:");
|
|
2759
|
-
lines.push(" def __init__(self):");
|
|
2760
2957
|
for (const [namespace, tools] of grouped.entries()) {
|
|
2761
2958
|
const safeNamespace = this.toSnakeCase(namespace);
|
|
2762
|
-
|
|
2959
|
+
lines.push(`class _${safeNamespace}_Namespace:`);
|
|
2763
2960
|
for (const tool of tools) {
|
|
2764
2961
|
const methodName = this.toSnakeCase(tool.methodName);
|
|
2765
2962
|
const fullName = tool.name;
|
|
2766
|
-
|
|
2963
|
+
lines.push(` async def ${methodName}(self, args=None, **kwargs):`);
|
|
2964
|
+
lines.push(` params = args if args is not None else kwargs`);
|
|
2965
|
+
lines.push(` return await _internal_call_tool("${this.escapeString(fullName)}", params)`);
|
|
2767
2966
|
}
|
|
2768
|
-
lines.push(
|
|
2769
|
-
lines.push(methodsDict.join(",\n"));
|
|
2770
|
-
lines.push(` })`);
|
|
2967
|
+
lines.push("");
|
|
2771
2968
|
}
|
|
2969
|
+
lines.push("class _Tools:");
|
|
2970
|
+
lines.push(" def __init__(self):");
|
|
2971
|
+
if (grouped.size === 0) {
|
|
2972
|
+
lines.push(" pass");
|
|
2973
|
+
} else {
|
|
2974
|
+
for (const [namespace] of grouped.entries()) {
|
|
2975
|
+
const safeNamespace = this.toSnakeCase(namespace);
|
|
2976
|
+
lines.push(` self.${safeNamespace} = _${safeNamespace}_Namespace()`);
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
lines.push("");
|
|
2980
|
+
lines.push(" def __getattr__(self, name):");
|
|
2981
|
+
lines.push(" # Flat access fallback: search all namespaces");
|
|
2982
|
+
lines.push(" for attr_name in dir(self):");
|
|
2983
|
+
lines.push(" attr = getattr(self, attr_name, None)");
|
|
2984
|
+
lines.push(" if attr and hasattr(attr, name):");
|
|
2985
|
+
lines.push(" return getattr(attr, name)");
|
|
2986
|
+
lines.push(` raise AttributeError(f"Namespace or Tool '{name}' not found")`);
|
|
2772
2987
|
if (enableRawFallback) {
|
|
2773
2988
|
lines.push("");
|
|
2774
|
-
lines.push(" async def raw(self, name, args):");
|
|
2989
|
+
lines.push(" async def raw(self, name, args=None):");
|
|
2775
2990
|
lines.push(' """Call a tool by its full name (escape hatch for dynamic/unknown tools)"""');
|
|
2776
2991
|
lines.push(' normalized = name.replace(".", "__")');
|
|
2777
2992
|
lines.push(" if _allowed_tools is not None:");
|
|
@@ -2781,7 +2996,7 @@ var SDKGenerator = class {
|
|
|
2781
2996
|
lines.push(" )");
|
|
2782
2997
|
lines.push(" if not allowed:");
|
|
2783
2998
|
lines.push(' raise PermissionError(f"Tool {name} is not in the allowlist")');
|
|
2784
|
-
lines.push(" return await _internal_call_tool(normalized, args)");
|
|
2999
|
+
lines.push(" return await _internal_call_tool(normalized, args or {})");
|
|
2785
3000
|
}
|
|
2786
3001
|
lines.push("");
|
|
2787
3002
|
lines.push("tools = _Tools()");
|
|
@@ -2804,7 +3019,7 @@ var SDKGenerator = class {
|
|
|
2804
3019
|
} else {
|
|
2805
3020
|
lines.push("const __allowedTools = null;");
|
|
2806
3021
|
}
|
|
2807
|
-
lines.push("const
|
|
3022
|
+
lines.push("const _tools = {");
|
|
2808
3023
|
for (const [namespace, tools] of grouped.entries()) {
|
|
2809
3024
|
const safeNamespace = this.isValidIdentifier(namespace) ? namespace : `["${this.escapeString(namespace)}"]`;
|
|
2810
3025
|
if (this.isValidIdentifier(namespace)) {
|
|
@@ -2823,23 +3038,46 @@ var SDKGenerator = class {
|
|
|
2823
3038
|
lines.push(` return JSON.parse(resStr);`);
|
|
2824
3039
|
lines.push(` },`);
|
|
2825
3040
|
}
|
|
2826
|
-
lines.push(
|
|
3041
|
+
lines.push(" },");
|
|
2827
3042
|
}
|
|
2828
3043
|
if (enableRawFallback) {
|
|
2829
|
-
lines.push(
|
|
3044
|
+
lines.push(" async $raw(name, args) {");
|
|
2830
3045
|
lines.push(` const normalized = name.replace(/\\./g, '__');`);
|
|
2831
|
-
lines.push(
|
|
3046
|
+
lines.push(" if (__allowedTools) {");
|
|
2832
3047
|
lines.push(` const allowed = __allowedTools.some(p => {`);
|
|
2833
3048
|
lines.push(` if (p.endsWith('__*')) return normalized.startsWith(p.slice(0, -1));`);
|
|
2834
|
-
lines.push(
|
|
2835
|
-
lines.push(
|
|
2836
|
-
lines.push(
|
|
2837
|
-
lines.push(
|
|
2838
|
-
lines.push(
|
|
2839
|
-
lines.push(
|
|
2840
|
-
lines.push(
|
|
3049
|
+
lines.push(" return normalized === p;");
|
|
3050
|
+
lines.push(" });");
|
|
3051
|
+
lines.push(" if (!allowed) throw new Error(`Tool ${name} is not in the allowlist`);");
|
|
3052
|
+
lines.push(" }");
|
|
3053
|
+
lines.push(" const resStr = await __callTool(normalized, JSON.stringify(args || {}));");
|
|
3054
|
+
lines.push(" return JSON.parse(resStr);");
|
|
3055
|
+
lines.push(" },");
|
|
2841
3056
|
}
|
|
2842
3057
|
lines.push("};");
|
|
3058
|
+
lines.push(`
|
|
3059
|
+
const tools = new Proxy(_tools, {
|
|
3060
|
+
get: (target, prop) => {
|
|
3061
|
+
if (prop in target) return target[prop];
|
|
3062
|
+
if (prop === 'then') return undefined;
|
|
3063
|
+
if (typeof prop === 'string') {
|
|
3064
|
+
// Flat tool access fallback: search all namespaces for a matching tool
|
|
3065
|
+
for (const nsName of Object.keys(target)) {
|
|
3066
|
+
if (nsName === '$raw') continue;
|
|
3067
|
+
const ns = target[nsName];
|
|
3068
|
+
if (ns && typeof ns === 'object' && ns[prop]) {
|
|
3069
|
+
return ns[prop];
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
const forbidden = ['$raw'];
|
|
3074
|
+
const namespaces = Object.keys(target).filter(k => !forbidden.includes(k));
|
|
3075
|
+
throw new Error(\`Namespace or Tool '\${prop}' not found. Available namespaces: \${namespaces.join(', ') || 'none'}. Use tools.$raw(name, args) for dynamic calls.\`);
|
|
3076
|
+
}
|
|
3077
|
+
return undefined;
|
|
3078
|
+
}
|
|
3079
|
+
});
|
|
3080
|
+
`);
|
|
2843
3081
|
return lines.join("\n");
|
|
2844
3082
|
}
|
|
2845
3083
|
/**
|
|
@@ -2929,14 +3167,17 @@ var ExecutionService = class {
|
|
|
2929
3167
|
async getToolBindings(context) {
|
|
2930
3168
|
const packages = await this.gatewayService.listToolPackages();
|
|
2931
3169
|
const allBindings = [];
|
|
3170
|
+
this.logger.debug({ packageCount: packages.length, packages: packages.map((p) => p.id) }, "Fetching tool bindings");
|
|
2932
3171
|
for (const pkg of packages) {
|
|
2933
3172
|
try {
|
|
2934
3173
|
const stubs = await this.gatewayService.listToolStubs(pkg.id, context);
|
|
3174
|
+
this.logger.debug({ packageId: pkg.id, stubCount: stubs.length }, "Got stubs from package");
|
|
2935
3175
|
allBindings.push(...stubs.map((s) => toToolBinding(s.id, void 0, s.description)));
|
|
2936
3176
|
} catch (err) {
|
|
2937
3177
|
this.logger.warn({ packageId: pkg.id, err: err.message }, "Failed to list stubs for package");
|
|
2938
3178
|
}
|
|
2939
3179
|
}
|
|
3180
|
+
this.logger.info({ totalBindings: allBindings.length }, "Tool bindings ready for SDK generation");
|
|
2940
3181
|
return allBindings;
|
|
2941
3182
|
}
|
|
2942
3183
|
async executeIsolate(code, limits, context, allowedTools) {
|
|
@@ -3100,11 +3341,97 @@ import Fastify2 from "fastify";
|
|
|
3100
3341
|
import axios4 from "axios";
|
|
3101
3342
|
import open from "open";
|
|
3102
3343
|
import { v4 as uuidv43 } from "uuid";
|
|
3344
|
+
import crypto3 from "crypto";
|
|
3345
|
+
var AUTH_REQUEST_PAYLOAD = {
|
|
3346
|
+
jsonrpc: "2.0",
|
|
3347
|
+
id: "conduit-auth",
|
|
3348
|
+
method: "initialize",
|
|
3349
|
+
params: {
|
|
3350
|
+
clientInfo: {
|
|
3351
|
+
name: "conduit-auth",
|
|
3352
|
+
version: "1.0.0"
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
};
|
|
3356
|
+
function base64UrlEncode(buffer) {
|
|
3357
|
+
return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
3358
|
+
}
|
|
3359
|
+
function createCodeVerifier() {
|
|
3360
|
+
return base64UrlEncode(crypto3.randomBytes(32));
|
|
3361
|
+
}
|
|
3362
|
+
function createCodeChallenge(verifier) {
|
|
3363
|
+
return base64UrlEncode(crypto3.createHash("sha256").update(verifier).digest());
|
|
3364
|
+
}
|
|
3365
|
+
function parseResourceMetadataHeader(headerValue) {
|
|
3366
|
+
if (!headerValue) return null;
|
|
3367
|
+
const header = Array.isArray(headerValue) ? headerValue.join(",") : headerValue;
|
|
3368
|
+
const match = header.match(/resource_metadata="([^"]+)"/i) || header.match(/resource_metadata=([^, ]+)/i);
|
|
3369
|
+
return match ? match[1] : null;
|
|
3370
|
+
}
|
|
3371
|
+
async function discoverOAuthFromMcp(mcpUrl) {
|
|
3372
|
+
const attempts = [
|
|
3373
|
+
() => axios4.get(mcpUrl, { validateStatus: () => true }),
|
|
3374
|
+
() => axios4.post(mcpUrl, AUTH_REQUEST_PAYLOAD, { validateStatus: () => true })
|
|
3375
|
+
];
|
|
3376
|
+
let resourceMetadataUrl = null;
|
|
3377
|
+
for (const attempt of attempts) {
|
|
3378
|
+
const response = await attempt();
|
|
3379
|
+
resourceMetadataUrl = parseResourceMetadataHeader(response.headers["www-authenticate"]);
|
|
3380
|
+
if (resourceMetadataUrl) break;
|
|
3381
|
+
}
|
|
3382
|
+
if (!resourceMetadataUrl) {
|
|
3383
|
+
throw new Error("Unable to discover OAuth metadata (missing WWW-Authenticate resource_metadata)");
|
|
3384
|
+
}
|
|
3385
|
+
const metadataResponse = await axios4.get(resourceMetadataUrl);
|
|
3386
|
+
const metadata = metadataResponse.data;
|
|
3387
|
+
let authUrl = metadata.authorization_endpoint;
|
|
3388
|
+
let tokenUrl = metadata.token_endpoint;
|
|
3389
|
+
let scopes = Array.isArray(metadata.scopes_supported) ? metadata.scopes_supported : void 0;
|
|
3390
|
+
const resource = typeof metadata.resource === "string" ? metadata.resource : void 0;
|
|
3391
|
+
if (!authUrl || !tokenUrl) {
|
|
3392
|
+
const authServer = Array.isArray(metadata.authorization_servers) && metadata.authorization_servers[0] || metadata.issuer;
|
|
3393
|
+
if (!authServer) {
|
|
3394
|
+
throw new Error("OAuth metadata did not include authorization server info");
|
|
3395
|
+
}
|
|
3396
|
+
const asMetadataUrl = new URL("/.well-known/oauth-authorization-server", authServer).toString();
|
|
3397
|
+
const asMetadataResponse = await axios4.get(asMetadataUrl);
|
|
3398
|
+
const asMetadata = asMetadataResponse.data;
|
|
3399
|
+
authUrl = authUrl || asMetadata.authorization_endpoint;
|
|
3400
|
+
tokenUrl = tokenUrl || asMetadata.token_endpoint;
|
|
3401
|
+
scopes = scopes || (Array.isArray(asMetadata.scopes_supported) ? asMetadata.scopes_supported : void 0);
|
|
3402
|
+
}
|
|
3403
|
+
if (!authUrl || !tokenUrl) {
|
|
3404
|
+
throw new Error("OAuth discovery failed: missing authorization or token endpoint");
|
|
3405
|
+
}
|
|
3406
|
+
return { authUrl, tokenUrl, scopes, resource };
|
|
3407
|
+
}
|
|
3408
|
+
function normalizeScopes(rawScopes) {
|
|
3409
|
+
if (!rawScopes) return void 0;
|
|
3410
|
+
return rawScopes.split(",").map((scope) => scope.trim()).filter(Boolean).join(" ");
|
|
3411
|
+
}
|
|
3103
3412
|
async function handleAuth(options) {
|
|
3104
3413
|
const port = options.port || 3333;
|
|
3105
3414
|
const redirectUri = `http://localhost:${port}/callback`;
|
|
3106
3415
|
const state = uuidv43();
|
|
3416
|
+
const codeVerifier = options.usePkce ? createCodeVerifier() : void 0;
|
|
3417
|
+
const codeChallenge = codeVerifier ? createCodeChallenge(codeVerifier) : void 0;
|
|
3107
3418
|
const fastify = Fastify2();
|
|
3419
|
+
let resolvedScopes = normalizeScopes(options.scopes);
|
|
3420
|
+
let resolvedAuthUrl = options.authUrl;
|
|
3421
|
+
let resolvedTokenUrl = options.tokenUrl;
|
|
3422
|
+
let resolvedResource;
|
|
3423
|
+
if (options.mcpUrl) {
|
|
3424
|
+
const discovered = await discoverOAuthFromMcp(options.mcpUrl);
|
|
3425
|
+
resolvedAuthUrl = discovered.authUrl;
|
|
3426
|
+
resolvedTokenUrl = discovered.tokenUrl;
|
|
3427
|
+
resolvedResource = discovered.resource;
|
|
3428
|
+
if (!resolvedScopes && discovered.scopes && discovered.scopes.length > 0) {
|
|
3429
|
+
resolvedScopes = discovered.scopes.join(" ");
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
if (!resolvedAuthUrl || !resolvedTokenUrl) {
|
|
3433
|
+
throw new Error("OAuth configuration missing authUrl or tokenUrl (set --mcp-url or provide both)");
|
|
3434
|
+
}
|
|
3108
3435
|
return new Promise((resolve, reject) => {
|
|
3109
3436
|
fastify.get("/callback", async (request, reply) => {
|
|
3110
3437
|
const { code, state: returnedState, error, error_description } = request.query;
|
|
@@ -3119,12 +3446,25 @@ async function handleAuth(options) {
|
|
|
3119
3446
|
return;
|
|
3120
3447
|
}
|
|
3121
3448
|
try {
|
|
3122
|
-
const
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3449
|
+
const body = new URLSearchParams();
|
|
3450
|
+
body.set("grant_type", "authorization_code");
|
|
3451
|
+
body.set("code", code);
|
|
3452
|
+
body.set("redirect_uri", redirectUri);
|
|
3453
|
+
body.set("client_id", options.clientId);
|
|
3454
|
+
if (options.clientSecret) {
|
|
3455
|
+
body.set("client_secret", options.clientSecret);
|
|
3456
|
+
}
|
|
3457
|
+
if (codeVerifier) {
|
|
3458
|
+
body.set("code_verifier", codeVerifier);
|
|
3459
|
+
}
|
|
3460
|
+
if (resolvedResource) {
|
|
3461
|
+
body.set("resource", resolvedResource);
|
|
3462
|
+
}
|
|
3463
|
+
const response = await axios4.post(resolvedTokenUrl, body, {
|
|
3464
|
+
headers: {
|
|
3465
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
3466
|
+
"Accept": "application/json"
|
|
3467
|
+
}
|
|
3128
3468
|
});
|
|
3129
3469
|
const { refresh_token, access_token } = response.data;
|
|
3130
3470
|
console.log("\n--- Authentication Successful ---\n");
|
|
@@ -3132,9 +3472,14 @@ async function handleAuth(options) {
|
|
|
3132
3472
|
console.log("credentials:");
|
|
3133
3473
|
console.log(" type: oauth2");
|
|
3134
3474
|
console.log(` clientId: ${options.clientId}`);
|
|
3135
|
-
|
|
3136
|
-
|
|
3475
|
+
if (options.clientSecret) {
|
|
3476
|
+
console.log(` clientSecret: ${options.clientSecret}`);
|
|
3477
|
+
}
|
|
3478
|
+
console.log(` tokenUrl: "${resolvedTokenUrl}"`);
|
|
3137
3479
|
console.log(` refreshToken: "${refresh_token || "N/A (No refresh token returned)"}"`);
|
|
3480
|
+
if (resolvedScopes) {
|
|
3481
|
+
console.log(` scopes: ["${resolvedScopes.split(" ").join('", "')}"]`);
|
|
3482
|
+
}
|
|
3138
3483
|
if (!refresh_token) {
|
|
3139
3484
|
console.log('\nWarning: No refresh token was returned. Ensure your app has "offline_access" scope or similar.');
|
|
3140
3485
|
}
|
|
@@ -3154,13 +3499,20 @@ async function handleAuth(options) {
|
|
|
3154
3499
|
reject(err);
|
|
3155
3500
|
return;
|
|
3156
3501
|
}
|
|
3157
|
-
const authUrl = new URL(
|
|
3502
|
+
const authUrl = new URL(resolvedAuthUrl);
|
|
3158
3503
|
authUrl.searchParams.append("client_id", options.clientId);
|
|
3159
3504
|
authUrl.searchParams.append("redirect_uri", redirectUri);
|
|
3160
3505
|
authUrl.searchParams.append("response_type", "code");
|
|
3161
3506
|
authUrl.searchParams.append("state", state);
|
|
3162
|
-
if (
|
|
3163
|
-
authUrl.searchParams.append("scope",
|
|
3507
|
+
if (resolvedScopes) {
|
|
3508
|
+
authUrl.searchParams.append("scope", resolvedScopes);
|
|
3509
|
+
}
|
|
3510
|
+
if (codeChallenge) {
|
|
3511
|
+
authUrl.searchParams.append("code_challenge", codeChallenge);
|
|
3512
|
+
authUrl.searchParams.append("code_challenge_method", "S256");
|
|
3513
|
+
}
|
|
3514
|
+
if (resolvedResource) {
|
|
3515
|
+
authUrl.searchParams.append("resource", resolvedResource);
|
|
3164
3516
|
}
|
|
3165
3517
|
console.log(`Opening browser to: ${authUrl.toString()}`);
|
|
3166
3518
|
console.log("Waiting for callback...");
|
|
@@ -3172,23 +3524,25 @@ async function handleAuth(options) {
|
|
|
3172
3524
|
// src/index.ts
|
|
3173
3525
|
var program = new Command();
|
|
3174
3526
|
program.name("conduit").description("A secure Code Mode execution substrate for MCP agents").version("1.0.0");
|
|
3175
|
-
program.command("serve", { isDefault: true }).description("Start the Conduit server").option("--stdio", "Use stdio transport").action(async (options) => {
|
|
3527
|
+
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) => {
|
|
3176
3528
|
try {
|
|
3177
|
-
await startServer();
|
|
3529
|
+
await startServer(options);
|
|
3178
3530
|
} catch (err) {
|
|
3179
3531
|
console.error("Failed to start Conduit:", err);
|
|
3180
3532
|
process.exit(1);
|
|
3181
3533
|
}
|
|
3182
3534
|
});
|
|
3183
|
-
program.command("auth").description("Help set up OAuth for an upstream MCP server").requiredOption("--client-id <id>", "OAuth Client ID").requiredOption("--client-secret <secret>", "OAuth Client Secret").
|
|
3535
|
+
program.command("auth").description("Help set up OAuth for an upstream MCP server").requiredOption("--client-id <id>", "OAuth Client ID").requiredOption("--client-secret <secret>", "OAuth Client Secret").option("--auth-url <url>", "OAuth Authorization URL").option("--token-url <url>", "OAuth Token URL").option("--mcp-url <url>", "MCP base URL (auto-discover OAuth metadata)").option("--scopes <scopes>", "OAuth Scopes (comma separated)").option("--port <port>", "Port for the local callback server", "3333").option("--pkce", "Use PKCE for the authorization code flow").action(async (options) => {
|
|
3184
3536
|
try {
|
|
3185
3537
|
await handleAuth({
|
|
3186
3538
|
clientId: options.clientId,
|
|
3187
3539
|
clientSecret: options.clientSecret,
|
|
3188
3540
|
authUrl: options.authUrl,
|
|
3189
3541
|
tokenUrl: options.tokenUrl,
|
|
3542
|
+
mcpUrl: options.mcpUrl,
|
|
3190
3543
|
scopes: options.scopes,
|
|
3191
|
-
port: parseInt(options.port, 10)
|
|
3544
|
+
port: parseInt(options.port, 10),
|
|
3545
|
+
usePkce: options.pkce || Boolean(options.mcpUrl)
|
|
3192
3546
|
});
|
|
3193
3547
|
console.log("\nSuccess! Configuration generated.");
|
|
3194
3548
|
} catch (err) {
|
|
@@ -3196,8 +3550,11 @@ program.command("auth").description("Help set up OAuth for an upstream MCP serve
|
|
|
3196
3550
|
process.exit(1);
|
|
3197
3551
|
}
|
|
3198
3552
|
});
|
|
3199
|
-
async function startServer() {
|
|
3200
|
-
const
|
|
3553
|
+
async function startServer(options = {}) {
|
|
3554
|
+
const overrides = {};
|
|
3555
|
+
if (options.stdio) overrides.transport = "stdio";
|
|
3556
|
+
if (options.config) process.env.CONFIG_FILE = options.config;
|
|
3557
|
+
const configService = new ConfigService(overrides);
|
|
3201
3558
|
const logger = createLogger(configService);
|
|
3202
3559
|
const otelService = new OtelService(logger);
|
|
3203
3560
|
await otelService.start();
|
|
@@ -3207,6 +3564,7 @@ async function startServer() {
|
|
|
3207
3564
|
const securityService = new SecurityService(logger, ipcToken);
|
|
3208
3565
|
const gatewayService = new GatewayService(logger, securityService);
|
|
3209
3566
|
const upstreams = configService.get("upstreams") || [];
|
|
3567
|
+
logger.info({ upstreamCount: upstreams.length, upstreamIds: upstreams.map((u) => u.id) }, "Registering upstreams from config");
|
|
3210
3568
|
for (const upstream of upstreams) {
|
|
3211
3569
|
gatewayService.registerUpstream(upstream);
|
|
3212
3570
|
}
|
|
@@ -3236,15 +3594,26 @@ async function startServer() {
|
|
|
3236
3594
|
let transport;
|
|
3237
3595
|
let address;
|
|
3238
3596
|
if (configService.get("transport") === "stdio") {
|
|
3239
|
-
|
|
3597
|
+
const stdioTransport = new StdioTransport(logger, requestController, concurrencyService);
|
|
3598
|
+
transport = stdioTransport;
|
|
3240
3599
|
await transport.start();
|
|
3600
|
+
gatewayService.registerHost(stdioTransport);
|
|
3241
3601
|
address = "stdio";
|
|
3602
|
+
const internalTransport = new SocketTransport(logger, requestController, concurrencyService);
|
|
3603
|
+
const internalPort = 0;
|
|
3604
|
+
const internalAddress = await internalTransport.listen({ port: internalPort });
|
|
3605
|
+
executionService.ipcAddress = internalAddress;
|
|
3606
|
+
const originalShutdown = transport.close.bind(transport);
|
|
3607
|
+
transport.close = async () => {
|
|
3608
|
+
await originalShutdown();
|
|
3609
|
+
await internalTransport.close();
|
|
3610
|
+
};
|
|
3242
3611
|
} else {
|
|
3243
3612
|
transport = new SocketTransport(logger, requestController, concurrencyService);
|
|
3244
3613
|
const port = configService.get("port");
|
|
3245
3614
|
address = await transport.listen({ port });
|
|
3615
|
+
executionService.ipcAddress = address;
|
|
3246
3616
|
}
|
|
3247
|
-
executionService.ipcAddress = address;
|
|
3248
3617
|
await requestController.warmup();
|
|
3249
3618
|
logger.info("Conduit server started");
|
|
3250
3619
|
const shutdown = async () => {
|