@mhingston5/conduit 1.1.5 → 1.1.7

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