@mhingston5/conduit 1.1.4 → 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.js CHANGED
@@ -750,6 +750,9 @@ var RequestController = class {
750
750
  // Standard MCP method name
751
751
  case "mcp_discover_tools":
752
752
  return this.handleDiscoverTools(params, context, id);
753
+ case "resources/list":
754
+ case "prompts/list":
755
+ return { jsonrpc: "2.0", id, result: { items: [] } };
753
756
  case "mcp_list_tool_packages":
754
757
  return this.handleListToolPackages(params, context, id);
755
758
  case "mcp_list_tool_stubs":
@@ -847,15 +850,45 @@ var RequestController = class {
847
850
  const { name, arguments: toolArgs } = params;
848
851
  switch (name) {
849
852
  case "mcp_execute_typescript":
850
- return this.handleExecuteTypeScript(toolArgs, context, id);
853
+ return this.handleExecuteToolCall("typescript", toolArgs, context, id);
851
854
  case "mcp_execute_python":
852
- return this.handleExecutePython(toolArgs, context, id);
855
+ return this.handleExecuteToolCall("python", toolArgs, context, id);
853
856
  case "mcp_execute_isolate":
854
- return this.handleExecuteIsolate(toolArgs, context, id);
857
+ return this.handleExecuteToolCall("isolate", toolArgs, context, id);
855
858
  }
856
859
  const response = await this.gatewayService.callTool(name, toolArgs, context);
857
860
  return { ...response, id };
858
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
+ }
859
892
  async handleExecuteTypeScript(params, context, id) {
860
893
  if (!params) return this.errorResponse(id, -32602, "Missing parameters");
861
894
  const { code, limits, allowedTools } = params;
@@ -1023,23 +1056,29 @@ var UpstreamClient = class {
1023
1056
  }
1024
1057
  try {
1025
1058
  await this.ensureConnected();
1026
- if (request.method === "list_tools") {
1059
+ if (request.method === "list_tools" || request.method === "tools/list") {
1027
1060
  const result = await this.mcpClient.listTools();
1028
1061
  return {
1029
1062
  jsonrpc: "2.0",
1030
1063
  id: request.id,
1031
1064
  result
1032
1065
  };
1033
- } else if (request.method === "call_tool") {
1066
+ } else if (request.method === "call_tool" || request.method === "tools/call") {
1034
1067
  const params = request.params;
1035
1068
  const result = await this.mcpClient.callTool({
1036
1069
  name: params.name,
1037
1070
  arguments: params.arguments
1038
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
+ };
1039
1078
  return {
1040
1079
  jsonrpc: "2.0",
1041
1080
  id: request.id,
1042
- result
1081
+ result: normalizedResult
1043
1082
  };
1044
1083
  } else {
1045
1084
  const result = await this.mcpClient.request(
@@ -1193,21 +1232,29 @@ var AuthService = class {
1193
1232
  }
1194
1233
  }
1195
1234
  async doRefresh(creds, cacheKey) {
1196
- if (!creds.tokenUrl || !creds.refreshToken || !creds.clientId || !creds.clientSecret) {
1235
+ if (!creds.tokenUrl || !creds.refreshToken || !creds.clientId) {
1197
1236
  throw new Error("OAuth2 credentials missing required fields for refresh");
1198
1237
  }
1199
1238
  this.logger.info({ tokenUrl: creds.tokenUrl, clientId: creds.clientId }, "Refreshing OAuth2 token");
1200
1239
  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
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
+ }
1206
1252
  });
1207
1253
  const { access_token, expires_in } = response.data;
1254
+ const expiresInSeconds = Number(expires_in) || 3600;
1208
1255
  this.tokenCache.set(cacheKey, {
1209
1256
  accessToken: access_token,
1210
- expiresAt: Date.now() + expires_in * 1e3
1257
+ expiresAt: Date.now() + expiresInSeconds * 1e3
1211
1258
  });
1212
1259
  return `Bearer ${access_token}`;
1213
1260
  } catch (err) {
@@ -1314,7 +1361,7 @@ import addFormats from "ajv-formats";
1314
1361
  var BUILT_IN_TOOLS = [
1315
1362
  {
1316
1363
  name: "mcp_execute_typescript",
1317
- 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(...)`).",
1318
1365
  inputSchema: {
1319
1366
  type: "object",
1320
1367
  properties: {
@@ -1325,7 +1372,7 @@ var BUILT_IN_TOOLS = [
1325
1372
  allowedTools: {
1326
1373
  type: "array",
1327
1374
  items: { type: "string" },
1328
- 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.'
1329
1376
  }
1330
1377
  },
1331
1378
  required: ["code"]
@@ -1333,7 +1380,7 @@ var BUILT_IN_TOOLS = [
1333
1380
  },
1334
1381
  {
1335
1382
  name: "mcp_execute_python",
1336
- 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(...)`).",
1337
1384
  inputSchema: {
1338
1385
  type: "object",
1339
1386
  properties: {
@@ -1344,7 +1391,7 @@ var BUILT_IN_TOOLS = [
1344
1391
  allowedTools: {
1345
1392
  type: "array",
1346
1393
  items: { type: "string" },
1347
- 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.'
1348
1395
  }
1349
1396
  },
1350
1397
  required: ["code"]
@@ -1352,7 +1399,7 @@ var BUILT_IN_TOOLS = [
1352
1399
  },
1353
1400
  {
1354
1401
  name: "mcp_execute_isolate",
1355
- 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.",
1356
1403
  inputSchema: {
1357
1404
  type: "object",
1358
1405
  properties: {
@@ -1363,7 +1410,7 @@ var BUILT_IN_TOOLS = [
1363
1410
  allowedTools: {
1364
1411
  type: "array",
1365
1412
  items: { type: "string" },
1366
- 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.'
1367
1414
  }
1368
1415
  },
1369
1416
  required: ["code"]
@@ -1428,7 +1475,7 @@ var GatewayService = class {
1428
1475
  const response = await client.call({
1429
1476
  jsonrpc: "2.0",
1430
1477
  id: "discovery",
1431
- method: "list_tools"
1478
+ method: "tools/list"
1432
1479
  }, context);
1433
1480
  if (response.result?.tools) {
1434
1481
  tools = response.result.tools;
@@ -1476,7 +1523,7 @@ var GatewayService = class {
1476
1523
  const response = await client.call({
1477
1524
  jsonrpc: "2.0",
1478
1525
  id: "discovery",
1479
- method: "list_tools"
1526
+ method: "tools/list"
1480
1527
  // Standard MCP method
1481
1528
  }, context);
1482
1529
  if (response.result?.tools) {
@@ -1578,7 +1625,7 @@ var GatewayService = class {
1578
1625
  response = await client.call({
1579
1626
  jsonrpc: "2.0",
1580
1627
  id: context.correlationId,
1581
- method: "call_tool",
1628
+ method: "tools/call",
1582
1629
  params: {
1583
1630
  name: toolName,
1584
1631
  arguments: params
@@ -1606,7 +1653,7 @@ var GatewayService = class {
1606
1653
  const response = await client.call({
1607
1654
  jsonrpc: "2.0",
1608
1655
  id: "health",
1609
- method: "list_tools"
1656
+ method: "tools/list"
1610
1657
  }, context);
1611
1658
  upstreamStatus[id] = response.error ? "degraded" : "active";
1612
1659
  } catch (err) {
@@ -2213,9 +2260,15 @@ var PyodideExecutor = class {
2213
2260
  });
2214
2261
  }
2215
2262
  createWorker(limits) {
2216
- let workerPath = path5.resolve(__dirname3, "./pyodide.worker.js");
2217
- if (!fs5.existsSync(workerPath)) {
2218
- 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(", ")}`);
2219
2272
  }
2220
2273
  return new Worker(workerPath, {
2221
2274
  execArgv: process.execArgv.includes("--loader") ? process.execArgv : [],
@@ -2691,7 +2744,7 @@ var SDKGenerator = class {
2691
2744
  } else {
2692
2745
  lines.push("const __allowedTools = null;");
2693
2746
  }
2694
- lines.push("const tools = {");
2747
+ lines.push("const _tools = {");
2695
2748
  for (const [namespace, tools] of grouped.entries()) {
2696
2749
  const safeNamespace = this.isValidIdentifier(namespace) ? namespace : `["${this.escapeString(namespace)}"]`;
2697
2750
  if (this.isValidIdentifier(namespace)) {
@@ -2729,6 +2782,18 @@ var SDKGenerator = class {
2729
2782
  lines.push(` },`);
2730
2783
  }
2731
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
+ `);
2732
2797
  lines.push("(globalThis as any).tools = tools;");
2733
2798
  return lines.join("\n");
2734
2799
  }
@@ -2804,7 +2869,7 @@ var SDKGenerator = class {
2804
2869
  } else {
2805
2870
  lines.push("const __allowedTools = null;");
2806
2871
  }
2807
- lines.push("const tools = {");
2872
+ lines.push("const _tools = {");
2808
2873
  for (const [namespace, tools] of grouped.entries()) {
2809
2874
  const safeNamespace = this.isValidIdentifier(namespace) ? namespace : `["${this.escapeString(namespace)}"]`;
2810
2875
  if (this.isValidIdentifier(namespace)) {
@@ -2840,6 +2905,18 @@ var SDKGenerator = class {
2840
2905
  lines.push(` },`);
2841
2906
  }
2842
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
+ `);
2843
2920
  return lines.join("\n");
2844
2921
  }
2845
2922
  /**
@@ -3100,11 +3177,97 @@ import Fastify2 from "fastify";
3100
3177
  import axios4 from "axios";
3101
3178
  import open from "open";
3102
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
+ }
3103
3248
  async function handleAuth(options) {
3104
3249
  const port = options.port || 3333;
3105
3250
  const redirectUri = `http://localhost:${port}/callback`;
3106
3251
  const state = uuidv43();
3252
+ const codeVerifier = options.usePkce ? createCodeVerifier() : void 0;
3253
+ const codeChallenge = codeVerifier ? createCodeChallenge(codeVerifier) : void 0;
3107
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
+ }
3108
3271
  return new Promise((resolve, reject) => {
3109
3272
  fastify.get("/callback", async (request, reply) => {
3110
3273
  const { code, state: returnedState, error, error_description } = request.query;
@@ -3119,12 +3282,25 @@ async function handleAuth(options) {
3119
3282
  return;
3120
3283
  }
3121
3284
  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
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
+ }
3128
3304
  });
3129
3305
  const { refresh_token, access_token } = response.data;
3130
3306
  console.log("\n--- Authentication Successful ---\n");
@@ -3132,9 +3308,14 @@ async function handleAuth(options) {
3132
3308
  console.log("credentials:");
3133
3309
  console.log(" type: oauth2");
3134
3310
  console.log(` clientId: ${options.clientId}`);
3135
- console.log(` clientSecret: ${options.clientSecret}`);
3136
- console.log(` tokenUrl: "${options.tokenUrl}"`);
3311
+ if (options.clientSecret) {
3312
+ console.log(` clientSecret: ${options.clientSecret}`);
3313
+ }
3314
+ console.log(` tokenUrl: "${resolvedTokenUrl}"`);
3137
3315
  console.log(` refreshToken: "${refresh_token || "N/A (No refresh token returned)"}"`);
3316
+ if (resolvedScopes) {
3317
+ console.log(` scopes: ["${resolvedScopes.split(" ").join('", "')}"]`);
3318
+ }
3138
3319
  if (!refresh_token) {
3139
3320
  console.log('\nWarning: No refresh token was returned. Ensure your app has "offline_access" scope or similar.');
3140
3321
  }
@@ -3154,13 +3335,20 @@ async function handleAuth(options) {
3154
3335
  reject(err);
3155
3336
  return;
3156
3337
  }
3157
- const authUrl = new URL(options.authUrl);
3338
+ const authUrl = new URL(resolvedAuthUrl);
3158
3339
  authUrl.searchParams.append("client_id", options.clientId);
3159
3340
  authUrl.searchParams.append("redirect_uri", redirectUri);
3160
3341
  authUrl.searchParams.append("response_type", "code");
3161
3342
  authUrl.searchParams.append("state", state);
3162
- if (options.scopes) {
3163
- 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);
3164
3352
  }
3165
3353
  console.log(`Opening browser to: ${authUrl.toString()}`);
3166
3354
  console.log("Waiting for callback...");
@@ -3180,15 +3368,17 @@ program.command("serve", { isDefault: true }).description("Start the Conduit ser
3180
3368
  process.exit(1);
3181
3369
  }
3182
3370
  });
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) => {
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) => {
3184
3372
  try {
3185
3373
  await handleAuth({
3186
3374
  clientId: options.clientId,
3187
3375
  clientSecret: options.clientSecret,
3188
3376
  authUrl: options.authUrl,
3189
3377
  tokenUrl: options.tokenUrl,
3378
+ mcpUrl: options.mcpUrl,
3190
3379
  scopes: options.scopes,
3191
- port: parseInt(options.port, 10)
3380
+ port: parseInt(options.port, 10),
3381
+ usePkce: options.pkce || Boolean(options.mcpUrl)
3192
3382
  });
3193
3383
  console.log("\nSuccess! Configuration generated.");
3194
3384
  } catch (err) {
@@ -3239,12 +3429,21 @@ async function startServer() {
3239
3429
  transport = new StdioTransport(logger, requestController, concurrencyService);
3240
3430
  await transport.start();
3241
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
+ };
3242
3441
  } else {
3243
3442
  transport = new SocketTransport(logger, requestController, concurrencyService);
3244
3443
  const port = configService.get("port");
3245
3444
  address = await transport.listen({ port });
3445
+ executionService.ipcAddress = address;
3246
3446
  }
3247
- executionService.ipcAddress = address;
3248
3447
  await requestController.warmup();
3249
3448
  logger.info("Conduit server started");
3250
3449
  const shutdown = async () => {