@mhingston5/conduit 1.1.3 → 1.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -97,6 +97,17 @@ npx conduit auth \
97
97
 
98
98
  This will start a temporary local server, open your browser for authorization, and print the generated `credentials` block for your `conduit.yaml`.
99
99
 
100
+ For GitHub MCP (remote server OAuth), you can auto-discover endpoints and use PKCE:
101
+
102
+ ```bash
103
+ npx conduit auth \
104
+ --client-id <id> \
105
+ --client-secret <secret> \
106
+ --mcp-url https://api.githubcopilot.com/mcp/ \
107
+ --scopes repo,read:org \
108
+ --pkce
109
+ ```
110
+
100
111
  ### 4. Execute TypeScript
101
112
 
102
113
  Using any [MCP Client](https://modelcontextprotocol.io/clients) (Claude Desktop, etc.), call `mcp_execute_typescript`:
@@ -104,7 +115,7 @@ Using any [MCP Client](https://modelcontextprotocol.io/clients) (Claude Desktop,
104
115
  ```ts
105
116
  // The agent writes this code:
106
117
  const result = await tools.filesystem.list_allowed_directories();
107
- console.log("Files:", result);
118
+ console.log("Directories:", JSON.stringify(result.content));
108
119
  ```
109
120
 
110
121
  ### 5. Result
@@ -113,7 +124,7 @@ Conduit runs the code, handles the tool call securely, and returns:
113
124
 
114
125
  ```json
115
126
  {
116
- "stdout": "Files: ['/tmp']\n",
127
+ "stdout": "Directories: [{\"type\":\"text\",\"text\":\"/tmp\"}]\n",
117
128
  "stderr": "",
118
129
  "exitCode": 0
119
130
  }
package/dist/index.d.ts CHANGED
@@ -1,2 +1 @@
1
-
2
- export { }
1
+ #!/usr/bin/env node
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ #!/usr/bin/env node
2
+
1
3
  // src/index.ts
2
4
  import { Command } from "commander";
3
5
 
@@ -6,6 +8,7 @@ import { z } from "zod";
6
8
  import dotenv from "dotenv";
7
9
  import fs from "fs";
8
10
  import path from "path";
11
+ import crypto from "crypto";
9
12
  import yaml from "js-yaml";
10
13
  var originalWrite = process.stdout.write;
11
14
  process.stdout.write = () => true;
@@ -58,7 +61,7 @@ var ConfigSchema = z.object({
58
61
  "[A-Za-z0-9-_]{20,}"
59
62
  // Default pattern from spec
60
63
  ]),
61
- ipcBearerToken: z.string().optional().default(() => Math.random().toString(36).substring(7)),
64
+ ipcBearerToken: z.string().optional().default(() => crypto.randomUUID()),
62
65
  maxConcurrent: z.number().default(10),
63
66
  denoMaxPoolSize: z.number().default(10),
64
67
  pyodideMaxPoolSize: z.number().default(3),
@@ -192,6 +195,7 @@ function createLogger(configService) {
192
195
 
193
196
  // src/transport/socket.transport.ts
194
197
  import net from "net";
198
+ import fs2 from "fs";
195
199
  import os from "os";
196
200
  import path2 from "path";
197
201
 
@@ -245,6 +249,13 @@ var SocketTransport = class {
245
249
  const socketPath = this.formatSocketPath(options.path);
246
250
  this.logger.info({ socketPath }, "Binding to IPC socket");
247
251
  if (os.platform() !== "win32" && path2.isAbsolute(socketPath)) {
252
+ try {
253
+ fs2.unlinkSync(socketPath);
254
+ } catch (error) {
255
+ if (error.code !== "ENOENT") {
256
+ this.logger.warn({ err: error, socketPath }, "Failed to unlink socket before binding");
257
+ }
258
+ }
248
259
  }
249
260
  this.server.listen(socketPath, () => {
250
261
  this.resolveAddress(resolve);
@@ -739,6 +750,9 @@ var RequestController = class {
739
750
  // Standard MCP method name
740
751
  case "mcp_discover_tools":
741
752
  return this.handleDiscoverTools(params, context, id);
753
+ case "resources/list":
754
+ case "prompts/list":
755
+ return { jsonrpc: "2.0", id, result: { items: [] } };
742
756
  case "mcp_list_tool_packages":
743
757
  return this.handleListToolPackages(params, context, id);
744
758
  case "mcp_list_tool_stubs":
@@ -832,19 +846,51 @@ var RequestController = class {
832
846
  }
833
847
  }
834
848
  async handleCallTool(params, context, id) {
849
+ if (!params) return this.errorResponse(id, -32602, "Missing parameters");
835
850
  const { name, arguments: toolArgs } = params;
836
851
  switch (name) {
837
852
  case "mcp_execute_typescript":
838
- return this.handleExecuteTypeScript(toolArgs, context, id);
853
+ return this.handleExecuteToolCall("typescript", toolArgs, context, id);
839
854
  case "mcp_execute_python":
840
- return this.handleExecutePython(toolArgs, context, id);
855
+ return this.handleExecuteToolCall("python", toolArgs, context, id);
841
856
  case "mcp_execute_isolate":
842
- return this.handleExecuteIsolate(toolArgs, context, id);
857
+ return this.handleExecuteToolCall("isolate", toolArgs, context, id);
843
858
  }
844
859
  const response = await this.gatewayService.callTool(name, toolArgs, context);
845
860
  return { ...response, id };
846
861
  }
862
+ formatExecutionResult(result) {
863
+ const structured = {
864
+ stdout: result.stdout,
865
+ stderr: result.stderr,
866
+ exitCode: result.exitCode
867
+ };
868
+ return {
869
+ content: [{
870
+ type: "text",
871
+ text: JSON.stringify(structured)
872
+ }],
873
+ structuredContent: structured
874
+ };
875
+ }
876
+ async handleExecuteToolCall(mode, params, context, id) {
877
+ if (!params) return this.errorResponse(id, -32602, "Missing parameters");
878
+ const { code, limits, allowedTools } = params;
879
+ if (Array.isArray(allowedTools)) {
880
+ context.allowedTools = allowedTools;
881
+ }
882
+ 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);
883
+ if (result.error) {
884
+ return this.errorResponse(id, result.error.code, result.error.message);
885
+ }
886
+ return {
887
+ jsonrpc: "2.0",
888
+ id,
889
+ result: this.formatExecutionResult(result)
890
+ };
891
+ }
847
892
  async handleExecuteTypeScript(params, context, id) {
893
+ if (!params) return this.errorResponse(id, -32602, "Missing parameters");
848
894
  const { code, limits, allowedTools } = params;
849
895
  if (Array.isArray(allowedTools)) {
850
896
  context.allowedTools = allowedTools;
@@ -864,6 +910,7 @@ var RequestController = class {
864
910
  };
865
911
  }
866
912
  async handleExecutePython(params, context, id) {
913
+ if (!params) return this.errorResponse(id, -32602, "Missing parameters");
867
914
  const { code, limits, allowedTools } = params;
868
915
  if (Array.isArray(allowedTools)) {
869
916
  context.allowedTools = allowedTools;
@@ -906,6 +953,7 @@ var RequestController = class {
906
953
  };
907
954
  }
908
955
  async handleExecuteIsolate(params, context, id) {
956
+ if (!params) return this.errorResponse(id, -32602, "Missing parameters");
909
957
  const { code, limits, allowedTools } = params;
910
958
  if (Array.isArray(allowedTools)) {
911
959
  context.allowedTools = allowedTools;
@@ -1008,23 +1056,29 @@ var UpstreamClient = class {
1008
1056
  }
1009
1057
  try {
1010
1058
  await this.ensureConnected();
1011
- if (request.method === "list_tools") {
1059
+ if (request.method === "list_tools" || request.method === "tools/list") {
1012
1060
  const result = await this.mcpClient.listTools();
1013
1061
  return {
1014
1062
  jsonrpc: "2.0",
1015
1063
  id: request.id,
1016
1064
  result
1017
1065
  };
1018
- } else if (request.method === "call_tool") {
1066
+ } else if (request.method === "call_tool" || request.method === "tools/call") {
1019
1067
  const params = request.params;
1020
1068
  const result = await this.mcpClient.callTool({
1021
1069
  name: params.name,
1022
1070
  arguments: params.arguments
1023
1071
  });
1072
+ const normalizedResult = result && Array.isArray(result.content) ? result : {
1073
+ content: [{
1074
+ type: "text",
1075
+ text: typeof result === "string" ? result : JSON.stringify(result ?? null)
1076
+ }]
1077
+ };
1024
1078
  return {
1025
1079
  jsonrpc: "2.0",
1026
1080
  id: request.id,
1027
- result
1081
+ result: normalizedResult
1028
1082
  };
1029
1083
  } else {
1030
1084
  const result = await this.mcpClient.request(
@@ -1178,21 +1232,29 @@ var AuthService = class {
1178
1232
  }
1179
1233
  }
1180
1234
  async doRefresh(creds, cacheKey) {
1181
- if (!creds.tokenUrl || !creds.refreshToken || !creds.clientId || !creds.clientSecret) {
1235
+ if (!creds.tokenUrl || !creds.refreshToken || !creds.clientId) {
1182
1236
  throw new Error("OAuth2 credentials missing required fields for refresh");
1183
1237
  }
1184
1238
  this.logger.info({ tokenUrl: creds.tokenUrl, clientId: creds.clientId }, "Refreshing OAuth2 token");
1185
1239
  try {
1186
- const response = await axios3.post(creds.tokenUrl, {
1187
- grant_type: "refresh_token",
1188
- refresh_token: creds.refreshToken,
1189
- client_id: creds.clientId,
1190
- client_secret: creds.clientSecret
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);
1244
+ if (creds.clientSecret) {
1245
+ body.set("client_secret", creds.clientSecret);
1246
+ }
1247
+ const response = await axios3.post(creds.tokenUrl, body, {
1248
+ headers: {
1249
+ "Content-Type": "application/x-www-form-urlencoded",
1250
+ "Accept": "application/json"
1251
+ }
1191
1252
  });
1192
1253
  const { access_token, expires_in } = response.data;
1254
+ const expiresInSeconds = Number(expires_in) || 3600;
1193
1255
  this.tokenCache.set(cacheKey, {
1194
1256
  accessToken: access_token,
1195
- expiresAt: Date.now() + expires_in * 1e3
1257
+ expiresAt: Date.now() + expiresInSeconds * 1e3
1196
1258
  });
1197
1259
  return `Bearer ${access_token}`;
1198
1260
  } catch (err) {
@@ -1299,7 +1361,7 @@ import addFormats from "ajv-formats";
1299
1361
  var BUILT_IN_TOOLS = [
1300
1362
  {
1301
1363
  name: "mcp_execute_typescript",
1302
- description: "Executes TypeScript code in a secure sandbox with access to `tools.*` SDK.",
1364
+ 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(...)`).",
1303
1365
  inputSchema: {
1304
1366
  type: "object",
1305
1367
  properties: {
@@ -1310,7 +1372,7 @@ var BUILT_IN_TOOLS = [
1310
1372
  allowedTools: {
1311
1373
  type: "array",
1312
1374
  items: { type: "string" },
1313
- description: 'Optional list of tools the script is allowed to call (e.g. ["github.*"]).'
1375
+ description: 'List of tool names (e.g. "filesystem.list_directory" or "filesystem.*") that the script is allowed to call.'
1314
1376
  }
1315
1377
  },
1316
1378
  required: ["code"]
@@ -1318,7 +1380,7 @@ var BUILT_IN_TOOLS = [
1318
1380
  },
1319
1381
  {
1320
1382
  name: "mcp_execute_python",
1321
- description: "Executes Python code in a secure sandbox with access to `tools.*` SDK.",
1383
+ 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(...)`).",
1322
1384
  inputSchema: {
1323
1385
  type: "object",
1324
1386
  properties: {
@@ -1329,7 +1391,7 @@ var BUILT_IN_TOOLS = [
1329
1391
  allowedTools: {
1330
1392
  type: "array",
1331
1393
  items: { type: "string" },
1332
- description: 'Optional list of tools the script is allowed to call (e.g. ["github.*"]).'
1394
+ description: 'List of tool names (e.g. "filesystem.list_directory" or "filesystem.*") that the script is allowed to call.'
1333
1395
  }
1334
1396
  },
1335
1397
  required: ["code"]
@@ -1337,7 +1399,7 @@ var BUILT_IN_TOOLS = [
1337
1399
  },
1338
1400
  {
1339
1401
  name: "mcp_execute_isolate",
1340
- description: "Executes JavaScript code in a high-speed V8 isolate (no Deno/Node APIs).",
1402
+ 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.",
1341
1403
  inputSchema: {
1342
1404
  type: "object",
1343
1405
  properties: {
@@ -1348,7 +1410,7 @@ var BUILT_IN_TOOLS = [
1348
1410
  allowedTools: {
1349
1411
  type: "array",
1350
1412
  items: { type: "string" },
1351
- description: "Optional list of tools the script is allowed to call."
1413
+ description: 'List of tool names (e.g. "filesystem.list_directory" or "filesystem.*") that the script is allowed to call.'
1352
1414
  }
1353
1415
  },
1354
1416
  required: ["code"]
@@ -1413,7 +1475,7 @@ var GatewayService = class {
1413
1475
  const response = await client.call({
1414
1476
  jsonrpc: "2.0",
1415
1477
  id: "discovery",
1416
- method: "list_tools"
1478
+ method: "tools/list"
1417
1479
  }, context);
1418
1480
  if (response.result?.tools) {
1419
1481
  tools = response.result.tools;
@@ -1461,7 +1523,7 @@ var GatewayService = class {
1461
1523
  const response = await client.call({
1462
1524
  jsonrpc: "2.0",
1463
1525
  id: "discovery",
1464
- method: "list_tools"
1526
+ method: "tools/list"
1465
1527
  // Standard MCP method
1466
1528
  }, context);
1467
1529
  if (response.result?.tools) {
@@ -1563,7 +1625,7 @@ var GatewayService = class {
1563
1625
  response = await client.call({
1564
1626
  jsonrpc: "2.0",
1565
1627
  id: context.correlationId,
1566
- method: "call_tool",
1628
+ method: "tools/call",
1567
1629
  params: {
1568
1630
  name: toolName,
1569
1631
  arguments: params
@@ -1591,7 +1653,7 @@ var GatewayService = class {
1591
1653
  const response = await client.call({
1592
1654
  jsonrpc: "2.0",
1593
1655
  id: "health",
1594
- method: "list_tools"
1656
+ method: "tools/list"
1595
1657
  }, context);
1596
1658
  upstreamStatus[id] = response.error ? "degraded" : "active";
1597
1659
  } catch (err) {
@@ -1759,7 +1821,7 @@ var SessionManager = class {
1759
1821
  };
1760
1822
 
1761
1823
  // src/core/security.service.ts
1762
- import crypto from "crypto";
1824
+ import crypto2 from "crypto";
1763
1825
  var SecurityService = class {
1764
1826
  logger;
1765
1827
  ipcToken;
@@ -1789,7 +1851,7 @@ var SecurityService = class {
1789
1851
  }
1790
1852
  const expected = Buffer.from(this.ipcToken);
1791
1853
  const actual = Buffer.from(token);
1792
- if (expected.length === actual.length && crypto.timingSafeEqual(expected, actual)) {
1854
+ if (expected.length === actual.length && crypto2.timingSafeEqual(expected, actual)) {
1793
1855
  return true;
1794
1856
  }
1795
1857
  return !!this.sessionManager.getSession(token);
@@ -1852,14 +1914,14 @@ var OtelService = class {
1852
1914
  // src/executors/deno.executor.ts
1853
1915
  import { spawn, exec } from "child_process";
1854
1916
  import { promisify } from "util";
1855
- import fs3 from "fs";
1917
+ import fs4 from "fs";
1856
1918
  import path4 from "path";
1857
1919
  import { platform } from "os";
1858
1920
  import { fileURLToPath as fileURLToPath2 } from "url";
1859
1921
 
1860
1922
  // src/core/asset.utils.ts
1861
1923
  import path3 from "path";
1862
- import fs2 from "fs";
1924
+ import fs3 from "fs";
1863
1925
  import { fileURLToPath } from "url";
1864
1926
  var __dirname = path3.dirname(fileURLToPath(import.meta.url));
1865
1927
  function resolveAssetPath(filename) {
@@ -1876,7 +1938,7 @@ function resolveAssetPath(filename) {
1876
1938
  path3.resolve(process.cwd(), "dist/assets", filename)
1877
1939
  ];
1878
1940
  for (const candidate of candidates) {
1879
- if (fs2.existsSync(candidate)) {
1941
+ if (fs3.existsSync(candidate)) {
1880
1942
  return candidate;
1881
1943
  }
1882
1944
  }
@@ -1900,7 +1962,7 @@ var DenoExecutor = class {
1900
1962
  if (this.shimContent) return this.shimContent;
1901
1963
  try {
1902
1964
  const assetPath = resolveAssetPath("deno-shim.ts");
1903
- this.shimContent = fs3.readFileSync(assetPath, "utf-8");
1965
+ this.shimContent = fs4.readFileSync(assetPath, "utf-8");
1904
1966
  return this.shimContent;
1905
1967
  } catch (err) {
1906
1968
  throw new Error(`Failed to load Deno shim: ${err.message}`);
@@ -2142,7 +2204,7 @@ var DenoExecutor = class {
2142
2204
 
2143
2205
  // src/executors/pyodide.executor.ts
2144
2206
  import { Worker } from "worker_threads";
2145
- import fs4 from "fs";
2207
+ import fs5 from "fs";
2146
2208
  import path5 from "path";
2147
2209
  import { fileURLToPath as fileURLToPath3 } from "url";
2148
2210
  var __dirname3 = path5.dirname(fileURLToPath3(import.meta.url));
@@ -2158,7 +2220,7 @@ var PyodideExecutor = class {
2158
2220
  if (this.shimContent) return this.shimContent;
2159
2221
  try {
2160
2222
  const assetPath = resolveAssetPath("python-shim.py");
2161
- this.shimContent = fs4.readFileSync(assetPath, "utf-8");
2223
+ this.shimContent = fs5.readFileSync(assetPath, "utf-8");
2162
2224
  return this.shimContent;
2163
2225
  } catch (err) {
2164
2226
  throw new Error(`Failed to load Python shim: ${err.message}`);
@@ -2198,9 +2260,15 @@ var PyodideExecutor = class {
2198
2260
  });
2199
2261
  }
2200
2262
  createWorker(limits) {
2201
- let workerPath = path5.resolve(__dirname3, "./pyodide.worker.js");
2202
- if (!fs4.existsSync(workerPath)) {
2203
- workerPath = path5.resolve(__dirname3, "./pyodide.worker.ts");
2263
+ const candidates = [
2264
+ path5.resolve(__dirname3, "./pyodide.worker.js"),
2265
+ path5.resolve(__dirname3, "./pyodide.worker.ts"),
2266
+ path5.resolve(__dirname3, "./executors/pyodide.worker.js"),
2267
+ path5.resolve(__dirname3, "./executors/pyodide.worker.ts")
2268
+ ];
2269
+ const workerPath = candidates.find((p) => fs5.existsSync(p));
2270
+ if (!workerPath) {
2271
+ throw new Error(`Pyodide worker not found. Tried: ${candidates.join(", ")}`);
2204
2272
  }
2205
2273
  return new Worker(workerPath, {
2206
2274
  execArgv: process.execArgv.includes("--loader") ? process.execArgv : [],
@@ -2676,7 +2744,7 @@ var SDKGenerator = class {
2676
2744
  } else {
2677
2745
  lines.push("const __allowedTools = null;");
2678
2746
  }
2679
- lines.push("const tools = {");
2747
+ lines.push("const _tools = {");
2680
2748
  for (const [namespace, tools] of grouped.entries()) {
2681
2749
  const safeNamespace = this.isValidIdentifier(namespace) ? namespace : `["${this.escapeString(namespace)}"]`;
2682
2750
  if (this.isValidIdentifier(namespace)) {
@@ -2714,6 +2782,18 @@ var SDKGenerator = class {
2714
2782
  lines.push(` },`);
2715
2783
  }
2716
2784
  lines.push("};");
2785
+ lines.push(`
2786
+ const tools = new Proxy(_tools, {
2787
+ get: (target, prop) => {
2788
+ if (prop in target) return target[prop];
2789
+ if (prop === 'then') return undefined;
2790
+ if (typeof prop === 'string') {
2791
+ throw new Error(\`Namespace '\${prop}' not found. It might be invalid, or all tools in it were disallowed.\`);
2792
+ }
2793
+ return undefined;
2794
+ }
2795
+ });
2796
+ `);
2717
2797
  lines.push("(globalThis as any).tools = tools;");
2718
2798
  return lines.join("\n");
2719
2799
  }
@@ -2789,7 +2869,7 @@ var SDKGenerator = class {
2789
2869
  } else {
2790
2870
  lines.push("const __allowedTools = null;");
2791
2871
  }
2792
- lines.push("const tools = {");
2872
+ lines.push("const _tools = {");
2793
2873
  for (const [namespace, tools] of grouped.entries()) {
2794
2874
  const safeNamespace = this.isValidIdentifier(namespace) ? namespace : `["${this.escapeString(namespace)}"]`;
2795
2875
  if (this.isValidIdentifier(namespace)) {
@@ -2825,6 +2905,18 @@ var SDKGenerator = class {
2825
2905
  lines.push(` },`);
2826
2906
  }
2827
2907
  lines.push("};");
2908
+ lines.push(`
2909
+ const tools = new Proxy(_tools, {
2910
+ get: (target, prop) => {
2911
+ if (prop in target) return target[prop];
2912
+ if (prop === 'then') return undefined;
2913
+ if (typeof prop === 'string') {
2914
+ throw new Error(\`Namespace '\${prop}' not found. It might be invalid, or all tools in it were disallowed.\`);
2915
+ }
2916
+ return undefined;
2917
+ }
2918
+ });
2919
+ `);
2828
2920
  return lines.join("\n");
2829
2921
  }
2830
2922
  /**
@@ -3085,11 +3177,97 @@ import Fastify2 from "fastify";
3085
3177
  import axios4 from "axios";
3086
3178
  import open from "open";
3087
3179
  import { v4 as uuidv43 } from "uuid";
3180
+ import crypto3 from "crypto";
3181
+ var AUTH_REQUEST_PAYLOAD = {
3182
+ jsonrpc: "2.0",
3183
+ id: "conduit-auth",
3184
+ method: "initialize",
3185
+ params: {
3186
+ clientInfo: {
3187
+ name: "conduit-auth",
3188
+ version: "1.0.0"
3189
+ }
3190
+ }
3191
+ };
3192
+ function base64UrlEncode(buffer) {
3193
+ return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
3194
+ }
3195
+ function createCodeVerifier() {
3196
+ return base64UrlEncode(crypto3.randomBytes(32));
3197
+ }
3198
+ function createCodeChallenge(verifier) {
3199
+ return base64UrlEncode(crypto3.createHash("sha256").update(verifier).digest());
3200
+ }
3201
+ function parseResourceMetadataHeader(headerValue) {
3202
+ if (!headerValue) return null;
3203
+ const header = Array.isArray(headerValue) ? headerValue.join(",") : headerValue;
3204
+ const match = header.match(/resource_metadata="([^"]+)"/i) || header.match(/resource_metadata=([^, ]+)/i);
3205
+ return match ? match[1] : null;
3206
+ }
3207
+ async function discoverOAuthFromMcp(mcpUrl) {
3208
+ const attempts = [
3209
+ () => axios4.get(mcpUrl, { validateStatus: () => true }),
3210
+ () => axios4.post(mcpUrl, AUTH_REQUEST_PAYLOAD, { validateStatus: () => true })
3211
+ ];
3212
+ let resourceMetadataUrl = null;
3213
+ for (const attempt of attempts) {
3214
+ const response = await attempt();
3215
+ resourceMetadataUrl = parseResourceMetadataHeader(response.headers["www-authenticate"]);
3216
+ if (resourceMetadataUrl) break;
3217
+ }
3218
+ if (!resourceMetadataUrl) {
3219
+ throw new Error("Unable to discover OAuth metadata (missing WWW-Authenticate resource_metadata)");
3220
+ }
3221
+ const metadataResponse = await axios4.get(resourceMetadataUrl);
3222
+ const metadata = metadataResponse.data;
3223
+ let authUrl = metadata.authorization_endpoint;
3224
+ let tokenUrl = metadata.token_endpoint;
3225
+ let scopes = Array.isArray(metadata.scopes_supported) ? metadata.scopes_supported : void 0;
3226
+ const resource = typeof metadata.resource === "string" ? metadata.resource : void 0;
3227
+ if (!authUrl || !tokenUrl) {
3228
+ const authServer = Array.isArray(metadata.authorization_servers) && metadata.authorization_servers[0] || metadata.issuer;
3229
+ if (!authServer) {
3230
+ throw new Error("OAuth metadata did not include authorization server info");
3231
+ }
3232
+ const asMetadataUrl = new URL("/.well-known/oauth-authorization-server", authServer).toString();
3233
+ const asMetadataResponse = await axios4.get(asMetadataUrl);
3234
+ const asMetadata = asMetadataResponse.data;
3235
+ authUrl = authUrl || asMetadata.authorization_endpoint;
3236
+ tokenUrl = tokenUrl || asMetadata.token_endpoint;
3237
+ scopes = scopes || (Array.isArray(asMetadata.scopes_supported) ? asMetadata.scopes_supported : void 0);
3238
+ }
3239
+ if (!authUrl || !tokenUrl) {
3240
+ throw new Error("OAuth discovery failed: missing authorization or token endpoint");
3241
+ }
3242
+ return { authUrl, tokenUrl, scopes, resource };
3243
+ }
3244
+ function normalizeScopes(rawScopes) {
3245
+ if (!rawScopes) return void 0;
3246
+ return rawScopes.split(",").map((scope) => scope.trim()).filter(Boolean).join(" ");
3247
+ }
3088
3248
  async function handleAuth(options) {
3089
3249
  const port = options.port || 3333;
3090
3250
  const redirectUri = `http://localhost:${port}/callback`;
3091
3251
  const state = uuidv43();
3252
+ const codeVerifier = options.usePkce ? createCodeVerifier() : void 0;
3253
+ const codeChallenge = codeVerifier ? createCodeChallenge(codeVerifier) : void 0;
3092
3254
  const fastify = Fastify2();
3255
+ let resolvedScopes = normalizeScopes(options.scopes);
3256
+ let resolvedAuthUrl = options.authUrl;
3257
+ let resolvedTokenUrl = options.tokenUrl;
3258
+ let resolvedResource;
3259
+ if (options.mcpUrl) {
3260
+ const discovered = await discoverOAuthFromMcp(options.mcpUrl);
3261
+ resolvedAuthUrl = discovered.authUrl;
3262
+ resolvedTokenUrl = discovered.tokenUrl;
3263
+ resolvedResource = discovered.resource;
3264
+ if (!resolvedScopes && discovered.scopes && discovered.scopes.length > 0) {
3265
+ resolvedScopes = discovered.scopes.join(" ");
3266
+ }
3267
+ }
3268
+ if (!resolvedAuthUrl || !resolvedTokenUrl) {
3269
+ throw new Error("OAuth configuration missing authUrl or tokenUrl (set --mcp-url or provide both)");
3270
+ }
3093
3271
  return new Promise((resolve, reject) => {
3094
3272
  fastify.get("/callback", async (request, reply) => {
3095
3273
  const { code, state: returnedState, error, error_description } = request.query;
@@ -3104,12 +3282,25 @@ async function handleAuth(options) {
3104
3282
  return;
3105
3283
  }
3106
3284
  try {
3107
- const response = await axios4.post(options.tokenUrl, {
3108
- grant_type: "authorization_code",
3109
- code,
3110
- redirect_uri: redirectUri,
3111
- client_id: options.clientId,
3112
- client_secret: options.clientSecret
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);
3290
+ if (options.clientSecret) {
3291
+ body.set("client_secret", options.clientSecret);
3292
+ }
3293
+ if (codeVerifier) {
3294
+ body.set("code_verifier", codeVerifier);
3295
+ }
3296
+ if (resolvedResource) {
3297
+ body.set("resource", resolvedResource);
3298
+ }
3299
+ const response = await axios4.post(resolvedTokenUrl, body, {
3300
+ headers: {
3301
+ "Content-Type": "application/x-www-form-urlencoded",
3302
+ "Accept": "application/json"
3303
+ }
3113
3304
  });
3114
3305
  const { refresh_token, access_token } = response.data;
3115
3306
  console.log("\n--- Authentication Successful ---\n");
@@ -3117,9 +3308,14 @@ async function handleAuth(options) {
3117
3308
  console.log("credentials:");
3118
3309
  console.log(" type: oauth2");
3119
3310
  console.log(` clientId: ${options.clientId}`);
3120
- console.log(` clientSecret: ${options.clientSecret}`);
3121
- console.log(` tokenUrl: "${options.tokenUrl}"`);
3311
+ if (options.clientSecret) {
3312
+ console.log(` clientSecret: ${options.clientSecret}`);
3313
+ }
3314
+ console.log(` tokenUrl: "${resolvedTokenUrl}"`);
3122
3315
  console.log(` refreshToken: "${refresh_token || "N/A (No refresh token returned)"}"`);
3316
+ if (resolvedScopes) {
3317
+ console.log(` scopes: ["${resolvedScopes.split(" ").join('", "')}"]`);
3318
+ }
3123
3319
  if (!refresh_token) {
3124
3320
  console.log('\nWarning: No refresh token was returned. Ensure your app has "offline_access" scope or similar.');
3125
3321
  }
@@ -3139,13 +3335,20 @@ async function handleAuth(options) {
3139
3335
  reject(err);
3140
3336
  return;
3141
3337
  }
3142
- const authUrl = new URL(options.authUrl);
3338
+ const authUrl = new URL(resolvedAuthUrl);
3143
3339
  authUrl.searchParams.append("client_id", options.clientId);
3144
3340
  authUrl.searchParams.append("redirect_uri", redirectUri);
3145
3341
  authUrl.searchParams.append("response_type", "code");
3146
3342
  authUrl.searchParams.append("state", state);
3147
- if (options.scopes) {
3148
- authUrl.searchParams.append("scope", options.scopes);
3343
+ if (resolvedScopes) {
3344
+ authUrl.searchParams.append("scope", resolvedScopes);
3345
+ }
3346
+ if (codeChallenge) {
3347
+ authUrl.searchParams.append("code_challenge", codeChallenge);
3348
+ authUrl.searchParams.append("code_challenge_method", "S256");
3349
+ }
3350
+ if (resolvedResource) {
3351
+ authUrl.searchParams.append("resource", resolvedResource);
3149
3352
  }
3150
3353
  console.log(`Opening browser to: ${authUrl.toString()}`);
3151
3354
  console.log("Waiting for callback...");
@@ -3165,15 +3368,17 @@ program.command("serve", { isDefault: true }).description("Start the Conduit ser
3165
3368
  process.exit(1);
3166
3369
  }
3167
3370
  });
3168
- 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) => {
3371
+ 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) => {
3169
3372
  try {
3170
3373
  await handleAuth({
3171
3374
  clientId: options.clientId,
3172
3375
  clientSecret: options.clientSecret,
3173
3376
  authUrl: options.authUrl,
3174
3377
  tokenUrl: options.tokenUrl,
3378
+ mcpUrl: options.mcpUrl,
3175
3379
  scopes: options.scopes,
3176
- port: parseInt(options.port, 10)
3380
+ port: parseInt(options.port, 10),
3381
+ usePkce: options.pkce || Boolean(options.mcpUrl)
3177
3382
  });
3178
3383
  console.log("\nSuccess! Configuration generated.");
3179
3384
  } catch (err) {
@@ -3224,12 +3429,21 @@ async function startServer() {
3224
3429
  transport = new StdioTransport(logger, requestController, concurrencyService);
3225
3430
  await transport.start();
3226
3431
  address = "stdio";
3432
+ const internalTransport = new SocketTransport(logger, requestController, concurrencyService);
3433
+ const internalPort = 0;
3434
+ const internalAddress = await internalTransport.listen({ port: internalPort });
3435
+ executionService.ipcAddress = internalAddress;
3436
+ const originalShutdown = transport.close.bind(transport);
3437
+ transport.close = async () => {
3438
+ await originalShutdown();
3439
+ await internalTransport.close();
3440
+ };
3227
3441
  } else {
3228
3442
  transport = new SocketTransport(logger, requestController, concurrencyService);
3229
3443
  const port = configService.get("port");
3230
3444
  address = await transport.listen({ port });
3445
+ executionService.ipcAddress = address;
3231
3446
  }
3232
- executionService.ipcAddress = address;
3233
3447
  await requestController.warmup();
3234
3448
  logger.info("Conduit server started");
3235
3449
  const shutdown = async () => {