@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 +13 -2
- package/dist/index.js +241 -42
- package/dist/index.js.map +1 -1
- package/dist/pyodide.worker.js.map +1 -0
- package/package.json +1 -1
- package/src/auth.cmd.ts +161 -14
- package/src/core/request.controller.ts +51 -3
- package/src/executors/pyodide.executor.ts +9 -4
- package/src/gateway/auth.service.ts +17 -7
- package/src/gateway/gateway.service.ts +10 -10
- package/src/gateway/upstream.client.ts +11 -3
- package/src/index.ts +20 -3
- package/src/sdk/sdk-generator.ts +26 -2
- package/tests/__snapshots__/assets.test.ts.snap +27 -3
- package/tests/dynamic.tool.test.ts +3 -3
- package/tests/gateway.manifest.test.ts +1 -1
- package/tests/gateway.service.test.ts +1 -1
- package/tests/reference_mcp.ts +5 -3
- package/tests/routing.test.ts +1 -1
- package/tests/sdk/sdk-generator.test.ts +3 -2
- package/tsup.config.ts +1 -1
- package/dist/executors/pyodide.worker.js.map +0 -1
- /package/dist/{executors/pyodide.worker.d.ts → pyodide.worker.d.ts} +0 -0
- /package/dist/{executors/pyodide.worker.js → pyodide.worker.js} +0 -0
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("
|
|
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": "
|
|
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.
|
|
853
|
+
return this.handleExecuteToolCall("typescript", toolArgs, context, id);
|
|
851
854
|
case "mcp_execute_python":
|
|
852
|
-
return this.
|
|
855
|
+
return this.handleExecuteToolCall("python", toolArgs, context, id);
|
|
853
856
|
case "mcp_execute_isolate":
|
|
854
|
-
return this.
|
|
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
|
|
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
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
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() +
|
|
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
|
|
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: '
|
|
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
|
|
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: '
|
|
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 (
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
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
|
|
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
|
|
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
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
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
|
-
|
|
3136
|
-
|
|
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(
|
|
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 (
|
|
3163
|
-
authUrl.searchParams.append("scope",
|
|
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").
|
|
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 () => {
|