@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 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) return {};
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 request;
448
+ let message;
420
449
  try {
421
- request = JSON.parse(line);
450
+ message = JSON.parse(line);
422
451
  } catch (err) {
423
- this.logger.error({ err, line }, "Failed to parse JSON-RPC request");
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
- switch (name) {
852
- case "mcp_execute_typescript":
853
- return this.handleExecuteToolCall("typescript", toolArgs, context, id);
854
- case "mcp_execute_python":
855
- return this.handleExecuteToolCall("python", toolArgs, context, id);
856
- case "mcp_execute_isolate":
857
- return this.handleExecuteToolCall("isolate", toolArgs, context, id);
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
- if (!this.transport.connection) {
1040
- await this.mcpClient.connect(this.transport);
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
- return Array.from(this.clients.entries()).map(([id, client]) => ({
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
- const stubs2 = manifest.tools.map((t) => ({
1463
- id: `${packageId}__${t.name}`,
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));
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
- return stubs2;
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.debug({ packageId, err: e }, "Manifest fetch failed, falling back to RPC");
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
- const builtIn = BUILT_IN_TOOLS.find((t) => t.name === toolId);
1505
- if (builtIn) return builtIn;
1506
- const upstreamId = parsed.namespace;
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 = [...BUILT_IN_TOOLS];
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
- let tools = this.schemaCache.get(id);
1522
- if (!tools) {
1523
- const response = await client.call({
1524
- jsonrpc: "2.0",
1525
- id: "discovery",
1526
- method: "tools/list"
1527
- // Standard MCP method
1528
- }, context);
1529
- if (response.result?.tools) {
1530
- tools = response.result.tools;
1531
- this.schemaCache.set(id, tools);
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
- this.logger.warn({ upstreamId: id, error: response.error }, "Failed to discover tools from upstream");
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
- return str.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
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
- throw new Error(\`Namespace '\${prop}' not found. It might be invalid, or all tools in it were disallowed.\`);
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
- const methodsDict = [];
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
- methodsDict.push(` "${methodName}": lambda args, n="${this.escapeString(fullName)}": _internal_call_tool(n, args)`);
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(` async $raw(name, args) {`);
3044
+ lines.push(" async $raw(name, args) {");
2895
3045
  lines.push(` const normalized = name.replace(/\\./g, '__');`);
2896
- lines.push(` if (__allowedTools) {`);
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(` return normalized === p;`);
2900
- lines.push(` });`);
2901
- lines.push(` if (!allowed) throw new Error(\`Tool \${name} is not in the allowlist\`);`);
2902
- lines.push(` }`);
2903
- lines.push(` const resStr = await __callTool(normalized, JSON.stringify(args || {}));`);
2904
- lines.push(` return JSON.parse(resStr);`);
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
- throw new Error(\`Namespace '\${prop}' not found. It might be invalid, or all tools in it were disallowed.\`);
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 configService = new ConfigService();
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
- transport = new StdioTransport(logger, requestController, concurrencyService);
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;