@mhingston5/conduit 1.1.5 → 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/dist/index.js +266 -96
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- 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 +32 -7
- package/src/gateway/gateway.service.ts +142 -65
- package/src/gateway/host.client.ts +65 -0
- package/src/gateway/upstream.client.ts +10 -11
- 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 +31 -2
- 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/gateway.service.test.ts +5 -5
- package/tests/routing.test.ts +7 -0
- package/tests/sdk/sdk-generator.test.ts +7 -7
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"
|
|
@@ -775,12 +813,29 @@ var RequestController = class {
|
|
|
775
813
|
case "notifications/initialized":
|
|
776
814
|
return null;
|
|
777
815
|
// Notifications don't get responses per MCP spec
|
|
816
|
+
case "mcp_register_upstream":
|
|
817
|
+
return this.handleRegisterUpstream(params, context, id);
|
|
778
818
|
case "ping":
|
|
779
819
|
return { jsonrpc: "2.0", id, result: {} };
|
|
780
820
|
default:
|
|
781
821
|
return this.errorResponse(id, -32601, `Method not found: ${method}`);
|
|
782
822
|
}
|
|
783
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
|
+
}
|
|
784
839
|
async handleDiscoverTools(params, context, id) {
|
|
785
840
|
const tools = await this.gatewayService.discoverTools(context);
|
|
786
841
|
const standardizedTools = tools.map((t) => ({
|
|
@@ -848,13 +903,18 @@ var RequestController = class {
|
|
|
848
903
|
async handleCallTool(params, context, id) {
|
|
849
904
|
if (!params) return this.errorResponse(id, -32602, "Missing parameters");
|
|
850
905
|
const { name, arguments: toolArgs } = params;
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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
|
+
}
|
|
858
918
|
}
|
|
859
919
|
const response = await this.gatewayService.callTool(name, toolArgs, context);
|
|
860
920
|
return { ...response, id };
|
|
@@ -1009,6 +1069,7 @@ var UpstreamClient = class {
|
|
|
1009
1069
|
urlValidator;
|
|
1010
1070
|
mcpClient;
|
|
1011
1071
|
transport;
|
|
1072
|
+
connected = false;
|
|
1012
1073
|
constructor(logger, info, authService, urlValidator) {
|
|
1013
1074
|
this.logger = logger.child({ upstreamId: info.id });
|
|
1014
1075
|
this.info = info;
|
|
@@ -1035,11 +1096,15 @@ var UpstreamClient = class {
|
|
|
1035
1096
|
}
|
|
1036
1097
|
async ensureConnected() {
|
|
1037
1098
|
if (!this.mcpClient || !this.transport) return;
|
|
1099
|
+
if (this.connected) return;
|
|
1038
1100
|
try {
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
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");
|
|
1042
1105
|
} catch (e) {
|
|
1106
|
+
this.logger.error({ err: e.message }, "Failed to connect to upstream");
|
|
1107
|
+
throw e;
|
|
1043
1108
|
}
|
|
1044
1109
|
}
|
|
1045
1110
|
async call(request, context) {
|
|
@@ -1346,6 +1411,9 @@ var PolicyService = class {
|
|
|
1346
1411
|
}
|
|
1347
1412
|
return true;
|
|
1348
1413
|
}
|
|
1414
|
+
if (patternParts.length === 1 && toolParts.length > 1) {
|
|
1415
|
+
return patternParts[0] === toolParts[toolParts.length - 1];
|
|
1416
|
+
}
|
|
1349
1417
|
if (patternParts.length !== toolParts.length) return false;
|
|
1350
1418
|
for (let i = 0; i < patternParts.length; i++) {
|
|
1351
1419
|
if (patternParts[i] !== toolParts[i]) return false;
|
|
@@ -1428,7 +1496,8 @@ var GatewayService = class {
|
|
|
1428
1496
|
// Cache compiled validators to avoid recompilation on every call
|
|
1429
1497
|
validatorCache = /* @__PURE__ */ new Map();
|
|
1430
1498
|
constructor(logger, urlValidator, policyService) {
|
|
1431
|
-
this.logger = logger;
|
|
1499
|
+
this.logger = logger.child({ component: "GatewayService" });
|
|
1500
|
+
this.logger.debug("GatewayService instance created");
|
|
1432
1501
|
this.urlValidator = urlValidator;
|
|
1433
1502
|
this.authService = new AuthService(logger);
|
|
1434
1503
|
this.schemaCache = new SchemaCache(logger);
|
|
@@ -1439,17 +1508,37 @@ var GatewayService = class {
|
|
|
1439
1508
|
registerUpstream(info) {
|
|
1440
1509
|
const client = new UpstreamClient(this.logger, info, this.authService, this.urlValidator);
|
|
1441
1510
|
this.clients.set(info.id, client);
|
|
1442
|
-
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)");
|
|
1443
1515
|
}
|
|
1444
1516
|
async listToolPackages() {
|
|
1445
|
-
|
|
1517
|
+
const upstreams = Array.from(this.clients.entries()).map(([id, client]) => ({
|
|
1446
1518
|
id,
|
|
1447
1519
|
description: `Upstream ${id}`,
|
|
1448
|
-
// NOTE: Upstream description fetching deferred to V2
|
|
1449
1520
|
version: "1.0.0"
|
|
1450
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;
|
|
1451
1529
|
}
|
|
1452
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
|
+
}
|
|
1453
1542
|
const client = this.clients.get(packageId);
|
|
1454
1543
|
if (!client) {
|
|
1455
1544
|
throw new Error(`Upstream package not found: ${packageId}`);
|
|
@@ -1458,33 +1547,33 @@ var GatewayService = class {
|
|
|
1458
1547
|
if (!tools) {
|
|
1459
1548
|
try {
|
|
1460
1549
|
const manifest = await client.getManifest(context);
|
|
1461
|
-
if (manifest) {
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
|
|
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
|
+
}
|
|
1469
1566
|
}
|
|
1470
|
-
|
|
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");
|
|
1471
1571
|
}
|
|
1472
1572
|
} catch (e) {
|
|
1473
|
-
this.logger.
|
|
1474
|
-
}
|
|
1475
|
-
const response = await client.call({
|
|
1476
|
-
jsonrpc: "2.0",
|
|
1477
|
-
id: "discovery",
|
|
1478
|
-
method: "tools/list"
|
|
1479
|
-
}, context);
|
|
1480
|
-
if (response.result?.tools) {
|
|
1481
|
-
tools = response.result.tools;
|
|
1482
|
-
this.schemaCache.set(packageId, tools);
|
|
1483
|
-
} else {
|
|
1484
|
-
this.logger.warn({ upstreamId: packageId, error: response.error }, "Failed to discover tools from upstream");
|
|
1485
|
-
tools = [];
|
|
1573
|
+
this.logger.error({ upstreamId: packageId, err: e.message }, "Error during tool discovery");
|
|
1486
1574
|
}
|
|
1487
1575
|
}
|
|
1576
|
+
if (!tools) tools = [];
|
|
1488
1577
|
const stubs = tools.map((t) => ({
|
|
1489
1578
|
id: `${packageId}__${t.name}`,
|
|
1490
1579
|
name: t.name,
|
|
@@ -1500,10 +1589,22 @@ var GatewayService = class {
|
|
|
1500
1589
|
throw new Error(`Access to tool ${toolId} is forbidden by allowlist`);
|
|
1501
1590
|
}
|
|
1502
1591
|
const parsed = this.policyService.parseToolName(toolId);
|
|
1592
|
+
const namespace = parsed.namespace;
|
|
1503
1593
|
const toolName = parsed.name;
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
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
|
+
}
|
|
1507
1608
|
if (!this.schemaCache.get(upstreamId)) {
|
|
1508
1609
|
await this.listToolStubs(upstreamId, context);
|
|
1509
1610
|
}
|
|
@@ -1516,34 +1617,37 @@ var GatewayService = class {
|
|
|
1516
1617
|
};
|
|
1517
1618
|
}
|
|
1518
1619
|
async discoverTools(context) {
|
|
1519
|
-
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");
|
|
1520
1625
|
for (const [id, client] of this.clients.entries()) {
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
},
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
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)));
|
|
1532
1641
|
} else {
|
|
1533
|
-
|
|
1534
|
-
tools = [];
|
|
1642
|
+
allTools.push(...prefixedTools);
|
|
1535
1643
|
}
|
|
1536
1644
|
}
|
|
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
1645
|
}
|
|
1646
|
+
this.logger.info({ totalTools: allTools.length }, "Tool discovery complete");
|
|
1544
1647
|
return allTools;
|
|
1545
1648
|
}
|
|
1546
1649
|
async callTool(name, params, context) {
|
|
1650
|
+
this.logger.debug({ name, upstreamCount: this.clients.size }, "GatewayService.callTool called");
|
|
1547
1651
|
if (context.allowedTools && !this.policyService.isToolAllowed(name, context.allowedTools)) {
|
|
1548
1652
|
this.logger.warn({ name, allowedTools: context.allowedTools }, "Tool call blocked by allowlist");
|
|
1549
1653
|
return {
|
|
@@ -1558,14 +1662,37 @@ var GatewayService = class {
|
|
|
1558
1662
|
const toolId = this.policyService.parseToolName(name);
|
|
1559
1663
|
const upstreamId = toolId.namespace;
|
|
1560
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
|
+
}
|
|
1561
1687
|
const client = this.clients.get(upstreamId);
|
|
1562
1688
|
if (!client) {
|
|
1689
|
+
this.logger.error({ upstreamId, availableUpstreams: Array.from(this.clients.keys()) }, "Upstream not found");
|
|
1563
1690
|
return {
|
|
1564
1691
|
jsonrpc: "2.0",
|
|
1565
1692
|
id: 0,
|
|
1566
1693
|
error: {
|
|
1567
1694
|
code: -32003,
|
|
1568
|
-
message: `Upstream not found: ${upstreamId}`
|
|
1695
|
+
message: `Upstream not found: '${upstreamId}'. Available: ${Array.from(this.clients.keys()).join(", ") || "none"}`
|
|
1569
1696
|
}
|
|
1570
1697
|
};
|
|
1571
1698
|
}
|
|
@@ -2719,7 +2846,8 @@ var SDKGenerator = class {
|
|
|
2719
2846
|
* Convert camelCase to snake_case for Python
|
|
2720
2847
|
*/
|
|
2721
2848
|
toSnakeCase(str) {
|
|
2722
|
-
|
|
2849
|
+
const snake = str.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "").replace(/[^a-z0-9_]/g, "_");
|
|
2850
|
+
return /^[0-9]/.test(snake) ? `_${snake}` : snake;
|
|
2723
2851
|
}
|
|
2724
2852
|
/**
|
|
2725
2853
|
* Escape a string for use in generated code
|
|
@@ -2788,7 +2916,18 @@ const tools = new Proxy(_tools, {
|
|
|
2788
2916
|
if (prop in target) return target[prop];
|
|
2789
2917
|
if (prop === 'then') return undefined;
|
|
2790
2918
|
if (typeof prop === 'string') {
|
|
2791
|
-
|
|
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.\`);
|
|
2792
2931
|
}
|
|
2793
2932
|
return undefined;
|
|
2794
2933
|
}
|
|
@@ -2815,28 +2954,39 @@ const tools = new Proxy(_tools, {
|
|
|
2815
2954
|
lines.push("_allowed_tools = None");
|
|
2816
2955
|
}
|
|
2817
2956
|
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
2957
|
for (const [namespace, tools] of grouped.entries()) {
|
|
2826
2958
|
const safeNamespace = this.toSnakeCase(namespace);
|
|
2827
|
-
|
|
2959
|
+
lines.push(`class _${safeNamespace}_Namespace:`);
|
|
2828
2960
|
for (const tool of tools) {
|
|
2829
2961
|
const methodName = this.toSnakeCase(tool.methodName);
|
|
2830
2962
|
const fullName = tool.name;
|
|
2831
|
-
|
|
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)`);
|
|
2966
|
+
}
|
|
2967
|
+
lines.push("");
|
|
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()`);
|
|
2832
2977
|
}
|
|
2833
|
-
lines.push(` self.${safeNamespace} = _ToolNamespace({`);
|
|
2834
|
-
lines.push(methodsDict.join(",\n"));
|
|
2835
|
-
lines.push(` })`);
|
|
2836
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")`);
|
|
2837
2987
|
if (enableRawFallback) {
|
|
2838
2988
|
lines.push("");
|
|
2839
|
-
lines.push(" async def raw(self, name, args):");
|
|
2989
|
+
lines.push(" async def raw(self, name, args=None):");
|
|
2840
2990
|
lines.push(' """Call a tool by its full name (escape hatch for dynamic/unknown tools)"""');
|
|
2841
2991
|
lines.push(' normalized = name.replace(".", "__")');
|
|
2842
2992
|
lines.push(" if _allowed_tools is not None:");
|
|
@@ -2846,7 +2996,7 @@ const tools = new Proxy(_tools, {
|
|
|
2846
2996
|
lines.push(" )");
|
|
2847
2997
|
lines.push(" if not allowed:");
|
|
2848
2998
|
lines.push(' raise PermissionError(f"Tool {name} is not in the allowlist")');
|
|
2849
|
-
lines.push(" return await _internal_call_tool(normalized, args)");
|
|
2999
|
+
lines.push(" return await _internal_call_tool(normalized, args or {})");
|
|
2850
3000
|
}
|
|
2851
3001
|
lines.push("");
|
|
2852
3002
|
lines.push("tools = _Tools()");
|
|
@@ -2888,21 +3038,21 @@ const tools = new Proxy(_tools, {
|
|
|
2888
3038
|
lines.push(` return JSON.parse(resStr);`);
|
|
2889
3039
|
lines.push(` },`);
|
|
2890
3040
|
}
|
|
2891
|
-
lines.push(
|
|
3041
|
+
lines.push(" },");
|
|
2892
3042
|
}
|
|
2893
3043
|
if (enableRawFallback) {
|
|
2894
|
-
lines.push(
|
|
3044
|
+
lines.push(" async $raw(name, args) {");
|
|
2895
3045
|
lines.push(` const normalized = name.replace(/\\./g, '__');`);
|
|
2896
|
-
lines.push(
|
|
3046
|
+
lines.push(" if (__allowedTools) {");
|
|
2897
3047
|
lines.push(` const allowed = __allowedTools.some(p => {`);
|
|
2898
3048
|
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(
|
|
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(" },");
|
|
2906
3056
|
}
|
|
2907
3057
|
lines.push("};");
|
|
2908
3058
|
lines.push(`
|
|
@@ -2911,7 +3061,18 @@ const tools = new Proxy(_tools, {
|
|
|
2911
3061
|
if (prop in target) return target[prop];
|
|
2912
3062
|
if (prop === 'then') return undefined;
|
|
2913
3063
|
if (typeof prop === 'string') {
|
|
2914
|
-
|
|
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.\`);
|
|
2915
3076
|
}
|
|
2916
3077
|
return undefined;
|
|
2917
3078
|
}
|
|
@@ -3006,14 +3167,17 @@ var ExecutionService = class {
|
|
|
3006
3167
|
async getToolBindings(context) {
|
|
3007
3168
|
const packages = await this.gatewayService.listToolPackages();
|
|
3008
3169
|
const allBindings = [];
|
|
3170
|
+
this.logger.debug({ packageCount: packages.length, packages: packages.map((p) => p.id) }, "Fetching tool bindings");
|
|
3009
3171
|
for (const pkg of packages) {
|
|
3010
3172
|
try {
|
|
3011
3173
|
const stubs = await this.gatewayService.listToolStubs(pkg.id, context);
|
|
3174
|
+
this.logger.debug({ packageId: pkg.id, stubCount: stubs.length }, "Got stubs from package");
|
|
3012
3175
|
allBindings.push(...stubs.map((s) => toToolBinding(s.id, void 0, s.description)));
|
|
3013
3176
|
} catch (err) {
|
|
3014
3177
|
this.logger.warn({ packageId: pkg.id, err: err.message }, "Failed to list stubs for package");
|
|
3015
3178
|
}
|
|
3016
3179
|
}
|
|
3180
|
+
this.logger.info({ totalBindings: allBindings.length }, "Tool bindings ready for SDK generation");
|
|
3017
3181
|
return allBindings;
|
|
3018
3182
|
}
|
|
3019
3183
|
async executeIsolate(code, limits, context, allowedTools) {
|
|
@@ -3360,9 +3524,9 @@ async function handleAuth(options) {
|
|
|
3360
3524
|
// src/index.ts
|
|
3361
3525
|
var program = new Command();
|
|
3362
3526
|
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) => {
|
|
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) => {
|
|
3364
3528
|
try {
|
|
3365
|
-
await startServer();
|
|
3529
|
+
await startServer(options);
|
|
3366
3530
|
} catch (err) {
|
|
3367
3531
|
console.error("Failed to start Conduit:", err);
|
|
3368
3532
|
process.exit(1);
|
|
@@ -3386,8 +3550,11 @@ program.command("auth").description("Help set up OAuth for an upstream MCP serve
|
|
|
3386
3550
|
process.exit(1);
|
|
3387
3551
|
}
|
|
3388
3552
|
});
|
|
3389
|
-
async function startServer() {
|
|
3390
|
-
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);
|
|
3391
3558
|
const logger = createLogger(configService);
|
|
3392
3559
|
const otelService = new OtelService(logger);
|
|
3393
3560
|
await otelService.start();
|
|
@@ -3397,6 +3564,7 @@ async function startServer() {
|
|
|
3397
3564
|
const securityService = new SecurityService(logger, ipcToken);
|
|
3398
3565
|
const gatewayService = new GatewayService(logger, securityService);
|
|
3399
3566
|
const upstreams = configService.get("upstreams") || [];
|
|
3567
|
+
logger.info({ upstreamCount: upstreams.length, upstreamIds: upstreams.map((u) => u.id) }, "Registering upstreams from config");
|
|
3400
3568
|
for (const upstream of upstreams) {
|
|
3401
3569
|
gatewayService.registerUpstream(upstream);
|
|
3402
3570
|
}
|
|
@@ -3426,8 +3594,10 @@ async function startServer() {
|
|
|
3426
3594
|
let transport;
|
|
3427
3595
|
let address;
|
|
3428
3596
|
if (configService.get("transport") === "stdio") {
|
|
3429
|
-
|
|
3597
|
+
const stdioTransport = new StdioTransport(logger, requestController, concurrencyService);
|
|
3598
|
+
transport = stdioTransport;
|
|
3430
3599
|
await transport.start();
|
|
3600
|
+
gatewayService.registerHost(stdioTransport);
|
|
3431
3601
|
address = "stdio";
|
|
3432
3602
|
const internalTransport = new SocketTransport(logger, requestController, concurrencyService);
|
|
3433
3603
|
const internalPort = 0;
|