@mhingston5/conduit 1.1.6 → 1.1.8
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 +29 -1
- package/dist/index.js +274 -67
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/auth.cmd.ts +26 -14
- package/src/core/config.service.ts +22 -1
- package/src/core/middleware/auth.middleware.ts +1 -2
- package/src/core/security.service.ts +8 -8
- package/src/executors/isolate.executor.ts +39 -12
- package/src/gateway/auth.service.ts +55 -13
- package/src/gateway/gateway.service.ts +22 -14
- package/src/gateway/upstream.client.ts +172 -15
- package/src/index.ts +5 -1
- package/tests/__snapshots__/assets.test.ts.snap +17 -15
- package/tests/auth.service.test.ts +57 -0
- package/tests/config.service.test.ts +29 -1
- package/tests/middleware.test.ts +16 -13
- package/tests/routing.test.ts +1 -0
- package/tests/upstream.transports.test.ts +156 -0
- package/tests/debug.fallback.test.ts +0 -40
- package/tests/debug_upstream.ts +0 -69
package/README.md
CHANGED
|
@@ -66,6 +66,20 @@ upstreams:
|
|
|
66
66
|
- id: github
|
|
67
67
|
type: http
|
|
68
68
|
url: "http://localhost:3000/mcp"
|
|
69
|
+
|
|
70
|
+
# Remote MCP servers that use Streamable HTTP (preferred) / SSE:
|
|
71
|
+
- id: atlassian
|
|
72
|
+
type: streamableHttp
|
|
73
|
+
url: "https://mcp.atlassian.com/v1/sse"
|
|
74
|
+
credentials:
|
|
75
|
+
type: oauth2
|
|
76
|
+
clientId: ${ATLASSIAN_CLIENT_ID}
|
|
77
|
+
clientSecret: ${ATLASSIAN_CLIENT_SECRET}
|
|
78
|
+
tokenUrl: "https://auth.atlassian.com/oauth/token"
|
|
79
|
+
refreshToken: ${ATLASSIAN_REFRESH_TOKEN}
|
|
80
|
+
# Atlassian expects JSON token requests:
|
|
81
|
+
tokenRequestFormat: json
|
|
82
|
+
|
|
69
83
|
- id: slack
|
|
70
84
|
type: http
|
|
71
85
|
url: "https://your-mcp-server/mcp"
|
|
@@ -75,7 +89,8 @@ upstreams:
|
|
|
75
89
|
clientSecret: ${SLACK_CLIENT_SECRET}
|
|
76
90
|
tokenUrl: "https://slack.com/api/oauth.v2.access"
|
|
77
91
|
refreshToken: ${SLACK_REFRESH_TOKEN}
|
|
78
|
-
|
|
92
|
+
|
|
93
|
+
# Or use local stdio for testing:
|
|
79
94
|
- id: filesystem
|
|
80
95
|
type: stdio
|
|
81
96
|
command: npx
|
|
@@ -95,8 +110,21 @@ npx conduit auth \
|
|
|
95
110
|
--scopes <scopes>
|
|
96
111
|
```
|
|
97
112
|
|
|
113
|
+
For Atlassian (3LO), include `offline_access` and set the audience:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
npx conduit auth \
|
|
117
|
+
--client-id <id> \
|
|
118
|
+
--client-secret <secret> \
|
|
119
|
+
--auth-url "https://auth.atlassian.com/authorize?audience=api.atlassian.com&prompt=consent" \
|
|
120
|
+
--token-url "https://auth.atlassian.com/oauth/token" \
|
|
121
|
+
--scopes "offline_access,read:me"
|
|
122
|
+
```
|
|
123
|
+
|
|
98
124
|
This will start a temporary local server, open your browser for authorization, and print the generated `credentials` block for your `conduit.yaml`.
|
|
99
125
|
|
|
126
|
+
Note: some providers (including Atlassian) use rotating refresh tokens. Conduit will cache the latest refresh token in-memory while running, but it does not currently persist the rotated token back into `conduit.yaml`. If you restart Conduit and your old refresh token has expired/rotated, re-run `conduit auth` and update your config.
|
|
127
|
+
|
|
100
128
|
For GitHub MCP (remote server OAuth), you can auto-discover endpoints and use PKCE:
|
|
101
129
|
|
|
102
130
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
+
import { createRequire } from "module";
|
|
5
6
|
|
|
6
7
|
// src/core/config.service.ts
|
|
7
8
|
import { z } from "zod";
|
|
@@ -29,6 +30,8 @@ var UpstreamCredentialsSchema = z.object({
|
|
|
29
30
|
tokenUrl: z.string().optional(),
|
|
30
31
|
refreshToken: z.string().optional(),
|
|
31
32
|
scopes: z.array(z.string()).optional(),
|
|
33
|
+
tokenRequestFormat: z.enum(["form", "json"]).optional(),
|
|
34
|
+
tokenParams: z.record(z.string(), z.string()).optional(),
|
|
32
35
|
apiKey: z.string().optional(),
|
|
33
36
|
bearerToken: z.string().optional(),
|
|
34
37
|
headerName: z.string().optional()
|
|
@@ -39,6 +42,18 @@ var HttpUpstreamSchema = z.object({
|
|
|
39
42
|
url: z.string(),
|
|
40
43
|
credentials: UpstreamCredentialsSchema.optional()
|
|
41
44
|
});
|
|
45
|
+
var StreamableHttpUpstreamSchema = z.object({
|
|
46
|
+
id: z.string(),
|
|
47
|
+
type: z.literal("streamableHttp"),
|
|
48
|
+
url: z.string(),
|
|
49
|
+
credentials: UpstreamCredentialsSchema.optional()
|
|
50
|
+
});
|
|
51
|
+
var SseUpstreamSchema = z.object({
|
|
52
|
+
id: z.string(),
|
|
53
|
+
type: z.literal("sse"),
|
|
54
|
+
url: z.string(),
|
|
55
|
+
credentials: UpstreamCredentialsSchema.optional()
|
|
56
|
+
});
|
|
42
57
|
var StdioUpstreamSchema = z.object({
|
|
43
58
|
id: z.string(),
|
|
44
59
|
type: z.literal("stdio"),
|
|
@@ -46,7 +61,12 @@ var StdioUpstreamSchema = z.object({
|
|
|
46
61
|
args: z.array(z.string()).optional(),
|
|
47
62
|
env: z.record(z.string(), z.string()).optional()
|
|
48
63
|
});
|
|
49
|
-
var UpstreamInfoSchema = z.union([
|
|
64
|
+
var UpstreamInfoSchema = z.union([
|
|
65
|
+
HttpUpstreamSchema,
|
|
66
|
+
StreamableHttpUpstreamSchema,
|
|
67
|
+
SseUpstreamSchema,
|
|
68
|
+
StdioUpstreamSchema
|
|
69
|
+
]);
|
|
50
70
|
var ConfigSchema = z.object({
|
|
51
71
|
port: z.union([z.string(), z.number()]).default("3000").transform((v) => Number(v)),
|
|
52
72
|
nodeEnv: z.enum(["development", "production", "test"]).default("development"),
|
|
@@ -1061,7 +1081,12 @@ var RequestController = class {
|
|
|
1061
1081
|
import axios2 from "axios";
|
|
1062
1082
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
1063
1083
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
1084
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
1085
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
1064
1086
|
import { z as z2 } from "zod";
|
|
1087
|
+
import dns from "dns";
|
|
1088
|
+
import net2 from "net";
|
|
1089
|
+
import { Agent } from "undici";
|
|
1065
1090
|
var UpstreamClient = class {
|
|
1066
1091
|
logger;
|
|
1067
1092
|
info;
|
|
@@ -1070,6 +1095,9 @@ var UpstreamClient = class {
|
|
|
1070
1095
|
mcpClient;
|
|
1071
1096
|
transport;
|
|
1072
1097
|
connected = false;
|
|
1098
|
+
// Pinned-IP dispatchers per upstream origin (defends against DNS rebinding)
|
|
1099
|
+
dispatcherCache = /* @__PURE__ */ new Map();
|
|
1100
|
+
pinned;
|
|
1073
1101
|
constructor(logger, info, authService, urlValidator) {
|
|
1074
1102
|
this.logger = logger.child({ upstreamId: info.id });
|
|
1075
1103
|
this.info = info;
|
|
@@ -1092,11 +1120,116 @@ var UpstreamClient = class {
|
|
|
1092
1120
|
}, {
|
|
1093
1121
|
capabilities: {}
|
|
1094
1122
|
});
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
if (this.info.type === "streamableHttp") {
|
|
1126
|
+
const upstreamUrl = new URL(this.info.url);
|
|
1127
|
+
this.pinned = { origin: upstreamUrl.origin, hostname: upstreamUrl.hostname };
|
|
1128
|
+
this.transport = new StreamableHTTPClientTransport(upstreamUrl, {
|
|
1129
|
+
fetch: this.createAuthedFetch()
|
|
1130
|
+
});
|
|
1131
|
+
this.mcpClient = new Client({
|
|
1132
|
+
name: "conduit-gateway",
|
|
1133
|
+
version: "1.0.0"
|
|
1134
|
+
}, {
|
|
1135
|
+
capabilities: {}
|
|
1136
|
+
});
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
if (this.info.type === "sse") {
|
|
1140
|
+
const upstreamUrl = new URL(this.info.url);
|
|
1141
|
+
this.pinned = { origin: upstreamUrl.origin, hostname: upstreamUrl.hostname };
|
|
1142
|
+
this.mcpClient = new Client({
|
|
1143
|
+
name: "conduit-gateway",
|
|
1144
|
+
version: "1.0.0"
|
|
1145
|
+
}, {
|
|
1146
|
+
capabilities: {}
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
getDispatcher(origin, hostname, resolvedIp) {
|
|
1151
|
+
const existing = this.dispatcherCache.get(origin);
|
|
1152
|
+
if (existing && existing.resolvedIp === resolvedIp) {
|
|
1153
|
+
return existing.agent;
|
|
1154
|
+
}
|
|
1155
|
+
if (existing) {
|
|
1156
|
+
try {
|
|
1157
|
+
existing.agent.close();
|
|
1158
|
+
} catch {
|
|
1159
|
+
}
|
|
1095
1160
|
}
|
|
1161
|
+
const agent = new Agent({
|
|
1162
|
+
connect: {
|
|
1163
|
+
lookup: (lookupHostname, options, callback) => {
|
|
1164
|
+
if (lookupHostname === hostname) {
|
|
1165
|
+
callback(null, resolvedIp, net2.isIP(resolvedIp));
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
dns.lookup(lookupHostname, options, callback);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
this.dispatcherCache.set(origin, { resolvedIp, agent });
|
|
1173
|
+
return agent;
|
|
1174
|
+
}
|
|
1175
|
+
createAuthedFetch() {
|
|
1176
|
+
const creds = this.info.credentials;
|
|
1177
|
+
const pinned = this.pinned;
|
|
1178
|
+
const baseFetch = fetch;
|
|
1179
|
+
return async (input, init = {}) => {
|
|
1180
|
+
const requestUrlStr = (() => {
|
|
1181
|
+
if (typeof input === "string") return input;
|
|
1182
|
+
if (input instanceof URL) return input.toString();
|
|
1183
|
+
if (input instanceof Request) return input.url;
|
|
1184
|
+
return String(input);
|
|
1185
|
+
})();
|
|
1186
|
+
const requestUrl = pinned ? new URL(requestUrlStr, pinned.origin) : new URL(requestUrlStr);
|
|
1187
|
+
if (pinned && requestUrl.origin !== pinned.origin) {
|
|
1188
|
+
throw new Error(`Forbidden upstream redirect/origin: ${requestUrl.origin}`);
|
|
1189
|
+
}
|
|
1190
|
+
if (pinned && !pinned.resolvedIp) {
|
|
1191
|
+
const securityResult = await this.urlValidator.validateUrl(pinned.origin);
|
|
1192
|
+
if (!securityResult.valid) {
|
|
1193
|
+
throw new Error(securityResult.message || "Forbidden URL");
|
|
1194
|
+
}
|
|
1195
|
+
pinned.resolvedIp = securityResult.resolvedIp;
|
|
1196
|
+
}
|
|
1197
|
+
const headers = new Headers((input instanceof Request ? input.headers : void 0) || void 0);
|
|
1198
|
+
const initHeaders = new Headers(init.headers || {});
|
|
1199
|
+
for (const [k, v] of initHeaders.entries()) headers.set(k, v);
|
|
1200
|
+
if (creds) {
|
|
1201
|
+
const authHeaders = await this.authService.getAuthHeaders(creds);
|
|
1202
|
+
for (const [k, v] of Object.entries(authHeaders)) {
|
|
1203
|
+
headers.set(k, v);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
const request = input instanceof Request ? new Request(input, { ...init, headers, redirect: init.redirect ?? "manual" }) : new Request(requestUrl.toString(), { ...init, headers, redirect: init.redirect ?? "manual" });
|
|
1207
|
+
const dispatcher = pinned && pinned.resolvedIp ? this.getDispatcher(pinned.origin, pinned.hostname, pinned.resolvedIp) : void 0;
|
|
1208
|
+
return baseFetch(request, dispatcher ? { dispatcher } : void 0);
|
|
1209
|
+
};
|
|
1096
1210
|
}
|
|
1097
1211
|
async ensureConnected() {
|
|
1098
|
-
if (!this.mcpClient
|
|
1212
|
+
if (!this.mcpClient) return;
|
|
1213
|
+
if (!this.transport && this.info.type === "sse") {
|
|
1214
|
+
const authHeaders = this.info.credentials ? await this.authService.getAuthHeaders(this.info.credentials) : {};
|
|
1215
|
+
this.transport = new SSEClientTransport(new URL(this.info.url), {
|
|
1216
|
+
fetch: this.createAuthedFetch(),
|
|
1217
|
+
eventSourceInit: { headers: authHeaders },
|
|
1218
|
+
requestInit: { headers: authHeaders }
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
if (!this.transport) return;
|
|
1099
1222
|
if (this.connected) return;
|
|
1223
|
+
if (this.info.type === "streamableHttp" || this.info.type === "sse") {
|
|
1224
|
+
const securityResult = await this.urlValidator.validateUrl(this.info.url);
|
|
1225
|
+
if (!securityResult.valid) {
|
|
1226
|
+
this.logger.error({ url: this.info.url }, "Blocked upstream URL (SSRF)");
|
|
1227
|
+
throw new Error(securityResult.message || "Forbidden URL");
|
|
1228
|
+
}
|
|
1229
|
+
if (this.pinned) {
|
|
1230
|
+
this.pinned.resolvedIp = securityResult.resolvedIp;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1100
1233
|
try {
|
|
1101
1234
|
this.logger.debug("Connecting to upstream transport...");
|
|
1102
1235
|
await this.mcpClient.connect(this.transport);
|
|
@@ -1108,16 +1241,15 @@ var UpstreamClient = class {
|
|
|
1108
1241
|
}
|
|
1109
1242
|
}
|
|
1110
1243
|
async call(request, context) {
|
|
1111
|
-
const
|
|
1112
|
-
if (
|
|
1113
|
-
return this.
|
|
1114
|
-
} else {
|
|
1115
|
-
return this.callHttp(request, context);
|
|
1244
|
+
const usesMcpClientTransport = (info) => info.type === "stdio" || info.type === "streamableHttp" || info.type === "sse";
|
|
1245
|
+
if (usesMcpClientTransport(this.info)) {
|
|
1246
|
+
return this.callMcpClient(request);
|
|
1116
1247
|
}
|
|
1248
|
+
return this.callHttp(request, context);
|
|
1117
1249
|
}
|
|
1118
|
-
async
|
|
1250
|
+
async callMcpClient(request) {
|
|
1119
1251
|
if (!this.mcpClient) {
|
|
1120
|
-
return { jsonrpc: "2.0", id: request.id, error: { code: -32603, message: "
|
|
1252
|
+
return { jsonrpc: "2.0", id: request.id, error: { code: -32603, message: "MCP client not initialized" } };
|
|
1121
1253
|
}
|
|
1122
1254
|
try {
|
|
1123
1255
|
await this.ensureConnected();
|
|
@@ -1157,19 +1289,21 @@ var UpstreamClient = class {
|
|
|
1157
1289
|
};
|
|
1158
1290
|
}
|
|
1159
1291
|
} catch (error) {
|
|
1160
|
-
this.logger.error({ err: error }, "
|
|
1292
|
+
this.logger.error({ err: error }, "MCP call failed");
|
|
1161
1293
|
return {
|
|
1162
1294
|
jsonrpc: "2.0",
|
|
1163
1295
|
id: request.id,
|
|
1164
1296
|
error: {
|
|
1165
1297
|
code: error.code || -32603,
|
|
1166
|
-
message: error.message || "Internal error in
|
|
1298
|
+
message: error.message || "Internal error in MCP transport"
|
|
1167
1299
|
}
|
|
1168
1300
|
};
|
|
1169
1301
|
}
|
|
1170
1302
|
}
|
|
1171
1303
|
async callHttp(request, context) {
|
|
1172
|
-
if (this.info.type === "stdio"
|
|
1304
|
+
if (this.info.type === "stdio" || this.info.type === "streamableHttp" || this.info.type === "sse") {
|
|
1305
|
+
throw new Error("Unreachable");
|
|
1306
|
+
}
|
|
1173
1307
|
const url = this.info.url;
|
|
1174
1308
|
const headers = {
|
|
1175
1309
|
"Content-Type": "application/json",
|
|
@@ -1218,7 +1352,7 @@ var UpstreamClient = class {
|
|
|
1218
1352
|
}
|
|
1219
1353
|
}
|
|
1220
1354
|
async getManifest(context) {
|
|
1221
|
-
if (this.info.type !== "http") return null;
|
|
1355
|
+
if (this.info.type && this.info.type !== "http") return null;
|
|
1222
1356
|
try {
|
|
1223
1357
|
const baseUrl = this.info.url.replace(/\/$/, "");
|
|
1224
1358
|
const manifestUrl = `${baseUrl}/conduit.manifest.json`;
|
|
@@ -1258,6 +1392,8 @@ var AuthService = class {
|
|
|
1258
1392
|
logger;
|
|
1259
1393
|
// Cache tokens separately from credentials to avoid mutation
|
|
1260
1394
|
tokenCache = /* @__PURE__ */ new Map();
|
|
1395
|
+
// Keep the latest refresh token in-memory (rotating tokens)
|
|
1396
|
+
refreshTokenCache = /* @__PURE__ */ new Map();
|
|
1261
1397
|
// Prevent concurrent refresh requests for the same client
|
|
1262
1398
|
refreshLocks = /* @__PURE__ */ new Map();
|
|
1263
1399
|
constructor(logger) {
|
|
@@ -1302,25 +1438,53 @@ var AuthService = class {
|
|
|
1302
1438
|
}
|
|
1303
1439
|
this.logger.info({ tokenUrl: creds.tokenUrl, clientId: creds.clientId }, "Refreshing OAuth2 token");
|
|
1304
1440
|
try {
|
|
1305
|
-
const
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1441
|
+
const tokenUrl = creds.tokenUrl;
|
|
1442
|
+
const cachedRefreshToken = this.refreshTokenCache.get(cacheKey);
|
|
1443
|
+
const refreshToken = cachedRefreshToken || creds.refreshToken;
|
|
1444
|
+
if (!refreshToken) {
|
|
1445
|
+
throw new Error("OAuth2 credentials missing required fields for refresh");
|
|
1446
|
+
}
|
|
1447
|
+
const payload = {
|
|
1448
|
+
grant_type: "refresh_token",
|
|
1449
|
+
refresh_token: refreshToken,
|
|
1450
|
+
client_id: creds.clientId
|
|
1451
|
+
};
|
|
1309
1452
|
if (creds.clientSecret) {
|
|
1310
|
-
|
|
1453
|
+
payload.client_secret = creds.clientSecret;
|
|
1311
1454
|
}
|
|
1312
|
-
|
|
1455
|
+
if (creds.tokenParams) {
|
|
1456
|
+
Object.assign(payload, creds.tokenParams);
|
|
1457
|
+
}
|
|
1458
|
+
const requestFormat = (() => {
|
|
1459
|
+
if (creds.tokenRequestFormat) return creds.tokenRequestFormat;
|
|
1460
|
+
try {
|
|
1461
|
+
const hostname = new URL(tokenUrl).hostname;
|
|
1462
|
+
if (hostname === "auth.atlassian.com") return "json";
|
|
1463
|
+
} catch {
|
|
1464
|
+
}
|
|
1465
|
+
return "form";
|
|
1466
|
+
})();
|
|
1467
|
+
const response = requestFormat === "json" ? await axios3.post(tokenUrl, payload, {
|
|
1468
|
+
headers: {
|
|
1469
|
+
"Content-Type": "application/json",
|
|
1470
|
+
"Accept": "application/json"
|
|
1471
|
+
}
|
|
1472
|
+
}) : await axios3.post(tokenUrl, new URLSearchParams(payload), {
|
|
1313
1473
|
headers: {
|
|
1314
1474
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
1315
1475
|
"Accept": "application/json"
|
|
1316
1476
|
}
|
|
1317
1477
|
});
|
|
1318
|
-
const { access_token, expires_in } = response.data;
|
|
1319
|
-
const
|
|
1478
|
+
const { access_token, expires_in, refresh_token } = response.data;
|
|
1479
|
+
const expiresInRaw = Number(expires_in);
|
|
1480
|
+
const expiresInSeconds = Number.isFinite(expiresInRaw) ? expiresInRaw : 3600;
|
|
1320
1481
|
this.tokenCache.set(cacheKey, {
|
|
1321
1482
|
accessToken: access_token,
|
|
1322
1483
|
expiresAt: Date.now() + expiresInSeconds * 1e3
|
|
1323
1484
|
});
|
|
1485
|
+
if (typeof refresh_token === "string" && refresh_token.length > 0) {
|
|
1486
|
+
this.refreshTokenCache.set(cacheKey, refresh_token);
|
|
1487
|
+
}
|
|
1324
1488
|
return `Bearer ${access_token}`;
|
|
1325
1489
|
} catch (err) {
|
|
1326
1490
|
const errorMsg = err.response?.data?.error_description || err.response?.data?.error || err.message;
|
|
@@ -1545,11 +1709,18 @@ var GatewayService = class {
|
|
|
1545
1709
|
}
|
|
1546
1710
|
let tools = this.schemaCache.get(packageId);
|
|
1547
1711
|
if (!tools) {
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1712
|
+
if (typeof client.getManifest === "function") {
|
|
1713
|
+
try {
|
|
1714
|
+
const manifest = await client.getManifest(context);
|
|
1715
|
+
if (manifest && manifest.tools) {
|
|
1716
|
+
tools = manifest.tools;
|
|
1717
|
+
}
|
|
1718
|
+
} catch (e) {
|
|
1719
|
+
this.logger.debug({ upstreamId: packageId, err: e.message }, "Manifest fetch failed (will fallback)");
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
if (!tools) {
|
|
1723
|
+
try {
|
|
1553
1724
|
if (typeof client.listTools === "function") {
|
|
1554
1725
|
tools = await client.listTools();
|
|
1555
1726
|
} else {
|
|
@@ -1564,13 +1735,13 @@ var GatewayService = class {
|
|
|
1564
1735
|
this.logger.warn({ upstreamId: packageId, error: response.error }, "Failed to discover tools via RPC");
|
|
1565
1736
|
}
|
|
1566
1737
|
}
|
|
1738
|
+
} catch (e) {
|
|
1739
|
+
this.logger.error({ upstreamId: packageId, err: e.message }, "Error during tool discovery");
|
|
1567
1740
|
}
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
}
|
|
1572
|
-
} catch (e) {
|
|
1573
|
-
this.logger.error({ upstreamId: packageId, err: e.message }, "Error during tool discovery");
|
|
1741
|
+
}
|
|
1742
|
+
if (tools && tools.length > 0) {
|
|
1743
|
+
this.schemaCache.set(packageId, tools);
|
|
1744
|
+
this.logger.info({ upstreamId: packageId, toolCount: tools.length }, "Discovered tools from upstream");
|
|
1574
1745
|
}
|
|
1575
1746
|
}
|
|
1576
1747
|
if (!tools) tools = [];
|
|
@@ -1827,8 +1998,8 @@ var GatewayService = class {
|
|
|
1827
1998
|
};
|
|
1828
1999
|
|
|
1829
2000
|
// src/core/network.policy.service.ts
|
|
1830
|
-
import
|
|
1831
|
-
import
|
|
2001
|
+
import dns2 from "dns/promises";
|
|
2002
|
+
import net3 from "net";
|
|
1832
2003
|
import { LRUCache as LRUCache2 } from "lru-cache";
|
|
1833
2004
|
var NetworkPolicyService = class {
|
|
1834
2005
|
logger;
|
|
@@ -1869,9 +2040,9 @@ var NetworkPolicyService = class {
|
|
|
1869
2040
|
return { valid: false, message: "Access denied: private network access forbidden" };
|
|
1870
2041
|
}
|
|
1871
2042
|
}
|
|
1872
|
-
if (!
|
|
2043
|
+
if (!net3.isIP(hostname)) {
|
|
1873
2044
|
try {
|
|
1874
|
-
const lookup = await
|
|
2045
|
+
const lookup = await dns2.lookup(hostname, { all: true });
|
|
1875
2046
|
const resolvedIps = [];
|
|
1876
2047
|
for (const address of lookup) {
|
|
1877
2048
|
let ip = address.address;
|
|
@@ -1972,13 +2143,14 @@ var SecurityService = class {
|
|
|
1972
2143
|
checkRateLimit(key) {
|
|
1973
2144
|
return this.networkPolicy.checkRateLimit(key);
|
|
1974
2145
|
}
|
|
1975
|
-
|
|
1976
|
-
if (!this.ipcToken)
|
|
1977
|
-
return true;
|
|
1978
|
-
}
|
|
2146
|
+
isMasterToken(token) {
|
|
2147
|
+
if (!this.ipcToken) return true;
|
|
1979
2148
|
const expected = Buffer.from(this.ipcToken);
|
|
1980
|
-
const actual = Buffer.from(token);
|
|
1981
|
-
|
|
2149
|
+
const actual = Buffer.from(token || "");
|
|
2150
|
+
return expected.length === actual.length && crypto2.timingSafeEqual(expected, actual);
|
|
2151
|
+
}
|
|
2152
|
+
validateIpcToken(token) {
|
|
2153
|
+
if (this.isMasterToken(token)) {
|
|
1982
2154
|
return true;
|
|
1983
2155
|
}
|
|
1984
2156
|
return !!this.sessionManager.getSession(token);
|
|
@@ -2601,26 +2773,30 @@ var IsolateExecutor = class {
|
|
|
2601
2773
|
const jail = ctx.global;
|
|
2602
2774
|
let currentLogBytes = 0;
|
|
2603
2775
|
let currentErrorBytes = 0;
|
|
2776
|
+
let totalLogEntries = 0;
|
|
2604
2777
|
await jail.set("__log", new ivm.Callback((msg) => {
|
|
2778
|
+
if (totalLogEntries + 1 > limits.maxLogEntries) {
|
|
2779
|
+
throw new Error("[LIMIT_LOG_ENTRIES]");
|
|
2780
|
+
}
|
|
2605
2781
|
if (currentLogBytes + msg.length + 1 > limits.maxOutputBytes) {
|
|
2606
2782
|
throw new Error("[LIMIT_LOG]");
|
|
2607
2783
|
}
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
}
|
|
2784
|
+
totalLogEntries++;
|
|
2785
|
+
logs.push(msg);
|
|
2786
|
+
currentLogBytes += msg.length + 1;
|
|
2612
2787
|
}));
|
|
2613
2788
|
await jail.set("__error", new ivm.Callback((msg) => {
|
|
2789
|
+
if (totalLogEntries + 1 > limits.maxLogEntries) {
|
|
2790
|
+
throw new Error("[LIMIT_LOG_ENTRIES]");
|
|
2791
|
+
}
|
|
2614
2792
|
if (currentErrorBytes + msg.length + 1 > limits.maxOutputBytes) {
|
|
2615
2793
|
throw new Error("[LIMIT_OUTPUT]");
|
|
2616
2794
|
}
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
}
|
|
2795
|
+
totalLogEntries++;
|
|
2796
|
+
errors.push(msg);
|
|
2797
|
+
currentErrorBytes += msg.length + 1;
|
|
2621
2798
|
}));
|
|
2622
2799
|
let requestIdCounter = 0;
|
|
2623
|
-
const pendingToolCalls = /* @__PURE__ */ new Map();
|
|
2624
2800
|
await jail.set("__dispatchToolCall", new ivm.Callback((nameStr, argsStr) => {
|
|
2625
2801
|
const requestId = ++requestIdCounter;
|
|
2626
2802
|
const name = nameStr;
|
|
@@ -2756,6 +2932,28 @@ var IsolateExecutor = class {
|
|
|
2756
2932
|
}
|
|
2757
2933
|
};
|
|
2758
2934
|
}
|
|
2935
|
+
if (message.includes("[LIMIT_LOG_ENTRIES]")) {
|
|
2936
|
+
return {
|
|
2937
|
+
stdout: logs.join("\n"),
|
|
2938
|
+
stderr: errors.join("\n"),
|
|
2939
|
+
exitCode: null,
|
|
2940
|
+
error: {
|
|
2941
|
+
code: -32014 /* LogLimitExceeded */,
|
|
2942
|
+
message: "Log entry limit exceeded"
|
|
2943
|
+
}
|
|
2944
|
+
};
|
|
2945
|
+
}
|
|
2946
|
+
if (message.includes("[LIMIT_LOG]") || message.includes("[LIMIT_OUTPUT]")) {
|
|
2947
|
+
return {
|
|
2948
|
+
stdout: logs.join("\n"),
|
|
2949
|
+
stderr: errors.join("\n"),
|
|
2950
|
+
exitCode: null,
|
|
2951
|
+
error: {
|
|
2952
|
+
code: -32013 /* OutputLimitExceeded */,
|
|
2953
|
+
message: "Output limit exceeded"
|
|
2954
|
+
}
|
|
2955
|
+
};
|
|
2956
|
+
}
|
|
2759
2957
|
this.logger.error({ err }, "Isolate execution failed");
|
|
2760
2958
|
return {
|
|
2761
2959
|
stdout: logs.join("\n"),
|
|
@@ -3168,13 +3366,13 @@ var ExecutionService = class {
|
|
|
3168
3366
|
const packages = await this.gatewayService.listToolPackages();
|
|
3169
3367
|
const allBindings = [];
|
|
3170
3368
|
this.logger.debug({ packageCount: packages.length, packages: packages.map((p) => p.id) }, "Fetching tool bindings");
|
|
3171
|
-
for (const
|
|
3369
|
+
for (const pkg2 of packages) {
|
|
3172
3370
|
try {
|
|
3173
|
-
const stubs = await this.gatewayService.listToolStubs(
|
|
3174
|
-
this.logger.debug({ packageId:
|
|
3371
|
+
const stubs = await this.gatewayService.listToolStubs(pkg2.id, context);
|
|
3372
|
+
this.logger.debug({ packageId: pkg2.id, stubCount: stubs.length }, "Got stubs from package");
|
|
3175
3373
|
allBindings.push(...stubs.map((s) => toToolBinding(s.id, void 0, s.description)));
|
|
3176
3374
|
} catch (err) {
|
|
3177
|
-
this.logger.warn({ packageId:
|
|
3375
|
+
this.logger.warn({ packageId: pkg2.id, err: err.message }, "Failed to list stubs for package");
|
|
3178
3376
|
}
|
|
3179
3377
|
}
|
|
3180
3378
|
this.logger.info({ totalBindings: allBindings.length }, "Tool bindings ready for SDK generation");
|
|
@@ -3269,8 +3467,7 @@ var AuthMiddleware = class {
|
|
|
3269
3467
|
}
|
|
3270
3468
|
async handle(request, context, next) {
|
|
3271
3469
|
const providedToken = request.auth?.bearerToken || "";
|
|
3272
|
-
const
|
|
3273
|
-
const isMaster = !masterToken || providedToken === masterToken;
|
|
3470
|
+
const isMaster = this.securityService.isMasterToken(providedToken);
|
|
3274
3471
|
const isSession = !isMaster && this.securityService.validateIpcToken(providedToken);
|
|
3275
3472
|
if (!isMaster && !isSession) {
|
|
3276
3473
|
return {
|
|
@@ -3446,21 +3643,29 @@ async function handleAuth(options) {
|
|
|
3446
3643
|
return;
|
|
3447
3644
|
}
|
|
3448
3645
|
try {
|
|
3449
|
-
const
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3646
|
+
const payload = {
|
|
3647
|
+
grant_type: "authorization_code",
|
|
3648
|
+
code,
|
|
3649
|
+
redirect_uri: redirectUri,
|
|
3650
|
+
client_id: options.clientId
|
|
3651
|
+
};
|
|
3454
3652
|
if (options.clientSecret) {
|
|
3455
|
-
|
|
3653
|
+
payload.client_secret = options.clientSecret;
|
|
3456
3654
|
}
|
|
3457
3655
|
if (codeVerifier) {
|
|
3458
|
-
|
|
3656
|
+
payload.code_verifier = codeVerifier;
|
|
3459
3657
|
}
|
|
3460
3658
|
if (resolvedResource) {
|
|
3461
|
-
|
|
3659
|
+
payload.resource = resolvedResource;
|
|
3462
3660
|
}
|
|
3463
|
-
const
|
|
3661
|
+
const tokenHostname = new URL(resolvedTokenUrl).hostname;
|
|
3662
|
+
const useJson = tokenHostname === "auth.atlassian.com";
|
|
3663
|
+
const response = useJson ? await axios4.post(resolvedTokenUrl, payload, {
|
|
3664
|
+
headers: {
|
|
3665
|
+
"Content-Type": "application/json",
|
|
3666
|
+
"Accept": "application/json"
|
|
3667
|
+
}
|
|
3668
|
+
}) : await axios4.post(resolvedTokenUrl, new URLSearchParams(payload), {
|
|
3464
3669
|
headers: {
|
|
3465
3670
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
3466
3671
|
"Accept": "application/json"
|
|
@@ -3523,7 +3728,9 @@ async function handleAuth(options) {
|
|
|
3523
3728
|
|
|
3524
3729
|
// src/index.ts
|
|
3525
3730
|
var program = new Command();
|
|
3526
|
-
|
|
3731
|
+
var require2 = createRequire(import.meta.url);
|
|
3732
|
+
var pkg = require2("../package.json");
|
|
3733
|
+
program.name("conduit").description("A secure Code Mode execution substrate for MCP agents").version(pkg.version || "0.0.0");
|
|
3527
3734
|
program.command("serve", { isDefault: true }).description("Start the Conduit server").option("--stdio", "Use stdio transport").option("--config <path>", "Path to config file").action(async (options) => {
|
|
3528
3735
|
try {
|
|
3529
3736
|
await startServer(options);
|