@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 +13 -2
- package/dist/index.d.ts +1 -2
- package/dist/index.js +265 -51
- package/dist/index.js.map +1 -1
- package/dist/pyodide.worker.js.map +1 -0
- package/package.json +3 -3
- 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 +21 -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.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(() =>
|
|
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.
|
|
853
|
+
return this.handleExecuteToolCall("typescript", toolArgs, context, id);
|
|
839
854
|
case "mcp_execute_python":
|
|
840
|
-
return this.
|
|
855
|
+
return this.handleExecuteToolCall("python", toolArgs, context, id);
|
|
841
856
|
case "mcp_execute_isolate":
|
|
842
|
-
return this.
|
|
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
|
|
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
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
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() +
|
|
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
|
|
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: '
|
|
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
|
|
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: '
|
|
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 (
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
|
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 &&
|
|
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
|
|
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
|
|
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 (
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
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
|
|
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
|
|
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
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
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
|
-
|
|
3121
|
-
|
|
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(
|
|
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 (
|
|
3148
|
-
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);
|
|
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").
|
|
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 () => {
|