@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/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"
@@ -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
- switch (name) {
849
- case "mcp_execute_typescript":
850
- return this.handleExecuteTypeScript(toolArgs, context, id);
851
- case "mcp_execute_python":
852
- return this.handleExecutePython(toolArgs, context, id);
853
- case "mcp_execute_isolate":
854
- return this.handleExecuteIsolate(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
+ }
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
- if (!this.transport.connection) {
1007
- await this.mcpClient.connect(this.transport);
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 || !creds.clientSecret) {
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 response = await axios3.post(creds.tokenUrl, {
1202
- grant_type: "refresh_token",
1203
- refresh_token: creds.refreshToken,
1204
- client_id: creds.clientId,
1205
- client_secret: creds.clientSecret
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() + expires_in * 1e3
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 with access to `tools.*` SDK.",
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: 'Optional list of tools the script is allowed to call (e.g. ["github.*"]).'
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 with access to `tools.*` SDK.",
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: 'Optional list of tools the script is allowed to call (e.g. ["github.*"]).'
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 (no Deno/Node APIs).",
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: "Optional list of tools the script is allowed to call."
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
- return Array.from(this.clients.entries()).map(([id, client]) => ({
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
- const stubs2 = manifest.tools.map((t) => ({
1416
- id: `${packageId}__${t.name}`,
1417
- name: t.name,
1418
- description: t.description
1419
- }));
1420
- if (context.allowedTools) {
1421
- 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
+ }
1422
1566
  }
1423
- 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");
1424
1571
  }
1425
1572
  } catch (e) {
1426
- this.logger.debug({ packageId, err: e }, "Manifest fetch failed, falling back to RPC");
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
- const builtIn = BUILT_IN_TOOLS.find((t) => t.name === toolId);
1458
- if (builtIn) return builtIn;
1459
- 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
+ }
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 = [...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");
1473
1625
  for (const [id, client] of this.clients.entries()) {
1474
- let tools = this.schemaCache.get(id);
1475
- if (!tools) {
1476
- const response = await client.call({
1477
- jsonrpc: "2.0",
1478
- id: "discovery",
1479
- method: "list_tools"
1480
- // Standard MCP method
1481
- }, context);
1482
- if (response.result?.tools) {
1483
- tools = response.result.tools;
1484
- 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)));
1485
1641
  } else {
1486
- this.logger.warn({ upstreamId: id, error: response.error }, "Failed to discover tools from upstream");
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: "call_tool",
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: "list_tools"
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
- let workerPath = path5.resolve(__dirname3, "./pyodide.worker.js");
2217
- if (!fs5.existsSync(workerPath)) {
2218
- workerPath = path5.resolve(__dirname3, "./pyodide.worker.ts");
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
- 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;
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 tools = {");
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
- const methodsDict = [];
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
- 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)`);
2767
2966
  }
2768
- lines.push(` self.${safeNamespace} = _ToolNamespace({`);
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 tools = {");
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(` async $raw(name, args) {`);
3044
+ lines.push(" async $raw(name, args) {");
2830
3045
  lines.push(` const normalized = name.replace(/\\./g, '__');`);
2831
- lines.push(` if (__allowedTools) {`);
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(` return normalized === p;`);
2835
- lines.push(` });`);
2836
- lines.push(` if (!allowed) throw new Error(\`Tool \${name} is not in the allowlist\`);`);
2837
- lines.push(` }`);
2838
- lines.push(` const resStr = await __callTool(normalized, JSON.stringify(args || {}));`);
2839
- lines.push(` return JSON.parse(resStr);`);
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 response = await axios4.post(options.tokenUrl, {
3123
- grant_type: "authorization_code",
3124
- code,
3125
- redirect_uri: redirectUri,
3126
- client_id: options.clientId,
3127
- client_secret: options.clientSecret
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
- console.log(` clientSecret: ${options.clientSecret}`);
3136
- console.log(` tokenUrl: "${options.tokenUrl}"`);
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(options.authUrl);
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 (options.scopes) {
3163
- authUrl.searchParams.append("scope", options.scopes);
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").requiredOption("--auth-url <url>", "OAuth Authorization URL").requiredOption("--token-url <url>", "OAuth Token URL").option("--scopes <scopes>", "OAuth Scopes (comma separated)").option("--port <port>", "Port for the local callback server", "3333").action(async (options) => {
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 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);
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
- transport = new StdioTransport(logger, requestController, concurrencyService);
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 () => {