doer-agent 0.9.5 → 0.9.6
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/dist/agent-codex-app-rpc.js +24 -2
- package/dist/agent-codex-cli.js +12 -0
- package/dist/agent-codex-cli.test.js +9 -0
- package/dist/agent-settings.js +6 -0
- package/dist/codex-app-server-manager.js +93 -17
- package/dist/mcp-oauth-relay.js +64 -0
- package/dist/mcp-oauth-relay.test.js +43 -0
- package/package.json +1 -1
|
@@ -65,7 +65,25 @@ function normalizeCodexAppRpcRequest(args) {
|
|
|
65
65
|
const timeoutMs = typeof args.request.timeoutMs === "number" && Number.isFinite(args.request.timeoutMs)
|
|
66
66
|
? Math.min(180_000, Math.max(1_000, Math.trunc(args.request.timeoutMs)))
|
|
67
67
|
: undefined;
|
|
68
|
-
if (!requestId || !requestAgentId || requestAgentId !== args.agentId
|
|
68
|
+
if (!requestId || !requestAgentId || requestAgentId !== args.agentId) {
|
|
69
|
+
throw new Error("invalid codex app rpc request");
|
|
70
|
+
}
|
|
71
|
+
if (actionRaw === "mcp-oauth-callback") {
|
|
72
|
+
const path = typeof args.request.path === "string" ? args.request.path.trim() : "";
|
|
73
|
+
const search = typeof args.request.search === "string" ? args.request.search.trim() : "";
|
|
74
|
+
if (!path.startsWith("/") || search.length > 16_384) {
|
|
75
|
+
throw new Error("invalid MCP OAuth callback request");
|
|
76
|
+
}
|
|
77
|
+
return { requestId, action: "mcp-oauth-callback", path, search };
|
|
78
|
+
}
|
|
79
|
+
if (actionRaw === "mcp-oauth-logout") {
|
|
80
|
+
const name = typeof args.request.name === "string" ? args.request.name.trim() : "";
|
|
81
|
+
if (!/^[A-Za-z0-9_-]+$/.test(name)) {
|
|
82
|
+
throw new Error("invalid MCP OAuth logout request");
|
|
83
|
+
}
|
|
84
|
+
return { requestId, action: "mcp-oauth-logout", name };
|
|
85
|
+
}
|
|
86
|
+
if (actionRaw !== "request" || !method) {
|
|
69
87
|
throw new Error("invalid codex app rpc request");
|
|
70
88
|
}
|
|
71
89
|
return {
|
|
@@ -82,7 +100,11 @@ async function handleCodexAppRpcMessage(args) {
|
|
|
82
100
|
const payload = JSON.parse(codexAppRpcCodec.decode(args.msg.data));
|
|
83
101
|
const request = normalizeCodexAppRpcRequest({ request: payload, agentId: args.agentId });
|
|
84
102
|
requestId = request.requestId;
|
|
85
|
-
const result =
|
|
103
|
+
const result = request.action === "request"
|
|
104
|
+
? applyCodexAppRpcOmitRules(request.method, await args.manager.request(request.method, request.params, request.timeoutMs))
|
|
105
|
+
: request.action === "mcp-oauth-callback"
|
|
106
|
+
? await args.manager.relayMcpOauthCallback(request.path, request.search)
|
|
107
|
+
: await args.manager.logoutMcpServer(request.name);
|
|
86
108
|
args.msg.respond(codexAppRpcCodec.encode(JSON.stringify({
|
|
87
109
|
requestId,
|
|
88
110
|
ok: true,
|
package/dist/agent-codex-cli.js
CHANGED
|
@@ -41,6 +41,9 @@ function buildRemoteMcpServerConfigArgs(args) {
|
|
|
41
41
|
"--config",
|
|
42
42
|
`${prefix}.enabled=${args.enabled === false ? "false" : "true"}`,
|
|
43
43
|
];
|
|
44
|
+
if (args.auth) {
|
|
45
|
+
configArgs.push("--config", `${prefix}.auth=${toTomlStringLiteral(args.auth)}`);
|
|
46
|
+
}
|
|
44
47
|
if (args.bearerTokenEnvVar?.trim()) {
|
|
45
48
|
configArgs.push("--config", `${prefix}.bearer_token_env_var=${toTomlStringLiteral(args.bearerTokenEnvVar.trim())}`);
|
|
46
49
|
}
|
|
@@ -50,6 +53,12 @@ function buildRemoteMcpServerConfigArgs(args) {
|
|
|
50
53
|
if (Object.keys(args.envHttpHeaders ?? {}).length > 0) {
|
|
51
54
|
configArgs.push("--config", `${prefix}.env_http_headers=${toTomlStringMap(args.envHttpHeaders ?? {})}`);
|
|
52
55
|
}
|
|
56
|
+
if ((args.scopes ?? []).length > 0) {
|
|
57
|
+
configArgs.push("--config", `${prefix}.scopes=${toTomlStringArray(args.scopes ?? [])}`);
|
|
58
|
+
}
|
|
59
|
+
if (args.oauthResource?.trim()) {
|
|
60
|
+
configArgs.push("--config", `${prefix}.oauth_resource=${toTomlStringLiteral(args.oauthResource.trim())}`);
|
|
61
|
+
}
|
|
53
62
|
return configArgs;
|
|
54
63
|
}
|
|
55
64
|
function hasDirectCodexBinary() {
|
|
@@ -157,7 +166,10 @@ export function buildCustomMcpConfigArgs(servers) {
|
|
|
157
166
|
configArgs.push(...buildRemoteMcpServerConfigArgs({
|
|
158
167
|
serverName,
|
|
159
168
|
url: server.url,
|
|
169
|
+
auth: server.auth,
|
|
160
170
|
bearerTokenEnvVar: server.bearerTokenEnvVar,
|
|
171
|
+
scopes: server.scopes,
|
|
172
|
+
oauthResource: server.oauthResource,
|
|
161
173
|
httpHeaders: Object.fromEntries(server.httpHeaders.map((header) => [header.key, header.value])),
|
|
162
174
|
envHttpHeaders: Object.fromEntries(server.envHttpHeaders.map((header) => [header.key, header.value])),
|
|
163
175
|
enabled: true,
|
|
@@ -9,7 +9,10 @@ test("buildCustomMcpConfigArgs builds streamable HTTP MCP overrides", () => {
|
|
|
9
9
|
args: [],
|
|
10
10
|
env: [],
|
|
11
11
|
url: "https://example.com/mcp",
|
|
12
|
+
auth: "oauth",
|
|
12
13
|
bearerTokenEnvVar: "MCP_TOKEN",
|
|
14
|
+
scopes: ["read:docs"],
|
|
15
|
+
oauthResource: "https://example.com/",
|
|
13
16
|
httpHeaders: [{ key: "X-Region", value: "seoul" }],
|
|
14
17
|
envHttpHeaders: [{ key: "X-API-Key", value: "MCP_API_KEY" }],
|
|
15
18
|
enabled: true,
|
|
@@ -17,9 +20,12 @@ test("buildCustomMcpConfigArgs builds streamable HTTP MCP overrides", () => {
|
|
|
17
20
|
assert.deepEqual(args, [
|
|
18
21
|
"--config", 'mcp_servers.remote_docs.url="https://example.com/mcp"',
|
|
19
22
|
"--config", "mcp_servers.remote_docs.enabled=true",
|
|
23
|
+
"--config", 'mcp_servers.remote_docs.auth="oauth"',
|
|
20
24
|
"--config", 'mcp_servers.remote_docs.bearer_token_env_var="MCP_TOKEN"',
|
|
21
25
|
"--config", 'mcp_servers.remote_docs.http_headers={ "X-Region" = "seoul" }',
|
|
22
26
|
"--config", 'mcp_servers.remote_docs.env_http_headers={ "X-API-Key" = "MCP_API_KEY" }',
|
|
27
|
+
"--config", 'mcp_servers.remote_docs.scopes=["read:docs"]',
|
|
28
|
+
"--config", 'mcp_servers.remote_docs.oauth_resource="https://example.com/"',
|
|
23
29
|
]);
|
|
24
30
|
});
|
|
25
31
|
test("buildCustomMcpConfigArgs keeps stdio MCP overrides", () => {
|
|
@@ -30,7 +36,10 @@ test("buildCustomMcpConfigArgs keeps stdio MCP overrides", () => {
|
|
|
30
36
|
args: ["-y", "local-mcp"],
|
|
31
37
|
env: [{ key: "LOCAL_TOKEN", value: "secret" }],
|
|
32
38
|
url: "",
|
|
39
|
+
auth: "oauth",
|
|
33
40
|
bearerTokenEnvVar: "",
|
|
41
|
+
scopes: [],
|
|
42
|
+
oauthResource: "",
|
|
34
43
|
httpHeaders: [],
|
|
35
44
|
envHttpHeaders: [],
|
|
36
45
|
enabled: true,
|
package/dist/agent-settings.js
CHANGED
|
@@ -237,7 +237,10 @@ function normalizeAgentMcpServer(value) {
|
|
|
237
237
|
args: normalizeStringArray(raw.args),
|
|
238
238
|
env,
|
|
239
239
|
url,
|
|
240
|
+
auth: raw.auth === "chatgpt" ? "chatgpt" : "oauth",
|
|
240
241
|
bearerTokenEnvVar: typeof raw.bearerTokenEnvVar === "string" ? raw.bearerTokenEnvVar.trim() : "",
|
|
242
|
+
scopes: normalizeStringArray(raw.scopes),
|
|
243
|
+
oauthResource: typeof raw.oauthResource === "string" ? raw.oauthResource.trim() : "",
|
|
241
244
|
httpHeaders: normalizeMcpHeaderEntries(raw.httpHeaders),
|
|
242
245
|
envHttpHeaders: normalizeMcpHeaderEntries(raw.envHttpHeaders),
|
|
243
246
|
enabled: raw.enabled !== false,
|
|
@@ -423,7 +426,10 @@ export async function toAgentSettingsPublic(args) {
|
|
|
423
426
|
value: variable.value,
|
|
424
427
|
})),
|
|
425
428
|
url: server.url,
|
|
429
|
+
auth: server.auth,
|
|
426
430
|
bearerTokenEnvVar: server.bearerTokenEnvVar,
|
|
431
|
+
scopes: [...server.scopes],
|
|
432
|
+
oauthResource: server.oauthResource,
|
|
427
433
|
httpHeaders: server.httpHeaders.map((header) => ({ ...header })),
|
|
428
434
|
envHttpHeaders: server.envHttpHeaders.map((header) => ({ ...header })),
|
|
429
435
|
enabled: server.enabled,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { buildAgentSettingsEnvPatch, readAgentModelInstructions, resolveAgentModelInstructionsFilePath, } from "./agent-settings.js";
|
|
2
|
-
import { buildCustomMcpConfigArgs, buildDaemonMcpConfigArgs, buildMobileMcpConfigArgs, buildThreadsMcpConfigArgs } from "./agent-codex-cli.js";
|
|
2
|
+
import { buildCustomMcpConfigArgs, buildDaemonMcpConfigArgs, buildMobileMcpConfigArgs, buildThreadsMcpConfigArgs, spawnManagedCodexCommand, } from "./agent-codex-cli.js";
|
|
3
3
|
import { CodexAppServerClient } from "./codex-app-server-client.js";
|
|
4
4
|
import { startCodexChatBridge } from "./codex-chat-bridge.js";
|
|
5
|
+
import { allocateMcpOauthCallbackPort, buildMcpOauthCallbackBaseUrl, relayMcpOauthCallbackToLocalListener, } from "./mcp-oauth-relay.js";
|
|
5
6
|
function toTomlStringLiteral(value) {
|
|
6
7
|
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
7
8
|
}
|
|
@@ -59,6 +60,8 @@ async function buildCodexAppServerArgs(args) {
|
|
|
59
60
|
...buildConfigArg("personality", toTomlStringLiteral(args.settings.general.personality)),
|
|
60
61
|
...buildConfigArg("approval_policy", toTomlStringLiteral("never")),
|
|
61
62
|
...buildConfigArg("sandbox_mode", toTomlStringLiteral("danger-full-access")),
|
|
63
|
+
...buildConfigArg("mcp_oauth_callback_port", String(args.mcpOauthCallbackPort)),
|
|
64
|
+
...buildConfigArg("mcp_oauth_callback_url", toTomlStringLiteral(args.mcpOauthCallbackUrl)),
|
|
62
65
|
];
|
|
63
66
|
const customInstructions = await readAgentModelInstructions(args.workspaceRoot);
|
|
64
67
|
if (customInstructions) {
|
|
@@ -140,18 +143,34 @@ export function createCodexAppServerManager(args) {
|
|
|
140
143
|
let client = null;
|
|
141
144
|
let providerProxy = null;
|
|
142
145
|
let createPromise = null;
|
|
146
|
+
let mcpOauthCallbackPortPromise = null;
|
|
143
147
|
let generation = 0;
|
|
144
148
|
const notificationListeners = new Set();
|
|
149
|
+
const mcpOauthCallbackUrl = buildMcpOauthCallbackBaseUrl({
|
|
150
|
+
serverBaseUrl: args.serverBaseUrl,
|
|
151
|
+
userId: args.userId,
|
|
152
|
+
agentId: args.agentId,
|
|
153
|
+
});
|
|
154
|
+
const mcpOauthCallbackPathPrefix = new URL(mcpOauthCallbackUrl).pathname;
|
|
155
|
+
const resolveMcpOauthCallbackPort = async () => {
|
|
156
|
+
if (!mcpOauthCallbackPortPromise) {
|
|
157
|
+
mcpOauthCallbackPortPromise = allocateMcpOauthCallbackPort();
|
|
158
|
+
}
|
|
159
|
+
return await mcpOauthCallbackPortPromise;
|
|
160
|
+
};
|
|
145
161
|
const createClient = async () => {
|
|
146
162
|
const settings = await args.readAgentSettingsConfig({ workspaceRoot: args.workspaceRoot });
|
|
147
163
|
const resolved = await resolveAppServerSettings({ settings, onLog: args.onLog });
|
|
148
164
|
providerProxy = resolved.proxy;
|
|
165
|
+
const mcpOauthCallbackPort = await resolveMcpOauthCallbackPort();
|
|
149
166
|
const appServerArgs = await buildCodexAppServerArgs({
|
|
150
167
|
agentId: args.agentId,
|
|
151
168
|
agentToken: args.agentToken,
|
|
152
169
|
workspaceRoot: args.workspaceRoot,
|
|
153
170
|
agentProjectDir: args.agentProjectDir,
|
|
154
171
|
serverBaseUrl: args.serverBaseUrl,
|
|
172
|
+
mcpOauthCallbackPort,
|
|
173
|
+
mcpOauthCallbackUrl,
|
|
155
174
|
settings: resolved.settings,
|
|
156
175
|
userId: args.userId,
|
|
157
176
|
});
|
|
@@ -164,7 +183,7 @@ export function createCodexAppServerManager(args) {
|
|
|
164
183
|
settings: resolved.settings,
|
|
165
184
|
userId: args.userId,
|
|
166
185
|
});
|
|
167
|
-
args.onLog?.(`starting codex app-server model=${resolveCodexModel(resolved.settings)} reasoningEffort=${resolved.settings.codex.reasoningEffort} personality=${resolved.settings.general.personality} computerUse=${resolved.settings.codex.computerUseEnabled} browserUse=${resolved.settings.codex.browserUseEnabled} mcpServers=${resolved.settings.mcp.servers.filter((server) => server.enabled).length}`);
|
|
186
|
+
args.onLog?.(`starting codex app-server model=${resolveCodexModel(resolved.settings)} reasoningEffort=${resolved.settings.codex.reasoningEffort} personality=${resolved.settings.general.personality} computerUse=${resolved.settings.codex.computerUseEnabled} browserUse=${resolved.settings.codex.browserUseEnabled} mcpServers=${resolved.settings.mcp.servers.filter((server) => server.enabled).length} mcpOauthCallbackPort=${mcpOauthCallbackPort}`);
|
|
168
187
|
return new CodexAppServerClient({
|
|
169
188
|
cwd: args.workspaceRoot,
|
|
170
189
|
args: appServerArgs,
|
|
@@ -202,6 +221,61 @@ export function createCodexAppServerManager(args) {
|
|
|
202
221
|
}
|
|
203
222
|
}
|
|
204
223
|
};
|
|
224
|
+
const restartClient = async (reason) => {
|
|
225
|
+
generation += 1;
|
|
226
|
+
const activeClient = client;
|
|
227
|
+
const activeProxy = providerProxy;
|
|
228
|
+
client = null;
|
|
229
|
+
providerProxy = null;
|
|
230
|
+
createPromise = null;
|
|
231
|
+
if (!activeClient) {
|
|
232
|
+
args.onLog?.(`codex app-server restart requested before start reason=${reason}`);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
args.onLog?.(`restarting codex app-server reason=${reason}`);
|
|
236
|
+
await activeClient.stop();
|
|
237
|
+
await activeProxy?.stop().catch((error) => {
|
|
238
|
+
args.onLog?.(`failed to stop provider proxy: ${error instanceof Error ? error.message : String(error)}`);
|
|
239
|
+
});
|
|
240
|
+
};
|
|
241
|
+
const runMcpLogout = async (name) => {
|
|
242
|
+
const settings = await args.readAgentSettingsConfig({ workspaceRoot: args.workspaceRoot });
|
|
243
|
+
const server = settings.mcp.servers.find((candidate) => candidate.name === name && candidate.transport === "streamable_http");
|
|
244
|
+
if (!server) {
|
|
245
|
+
throw new Error("Remote MCP server not found");
|
|
246
|
+
}
|
|
247
|
+
const env = await buildCodexAppServerEnv({
|
|
248
|
+
agentId: args.agentId,
|
|
249
|
+
agentToken: args.agentToken,
|
|
250
|
+
workspaceRoot: args.workspaceRoot,
|
|
251
|
+
serverBaseUrl: args.serverBaseUrl,
|
|
252
|
+
resolveCodexHomePath: args.resolveCodexHomePath,
|
|
253
|
+
settings,
|
|
254
|
+
userId: args.userId,
|
|
255
|
+
});
|
|
256
|
+
const child = spawnManagedCodexCommand({
|
|
257
|
+
codexArgs: ["mcp", "logout", name, ...buildCustomMcpConfigArgs([server])],
|
|
258
|
+
taskWorkspace: args.workspaceRoot,
|
|
259
|
+
env,
|
|
260
|
+
agentToken: args.agentToken,
|
|
261
|
+
});
|
|
262
|
+
let stdout = "";
|
|
263
|
+
let stderr = "";
|
|
264
|
+
child.stdout?.on("data", (chunk) => {
|
|
265
|
+
stdout = `${stdout}${chunk}`.slice(-16_384);
|
|
266
|
+
});
|
|
267
|
+
child.stderr?.on("data", (chunk) => {
|
|
268
|
+
stderr = `${stderr}${chunk}`.slice(-16_384);
|
|
269
|
+
});
|
|
270
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
271
|
+
child.once("error", reject);
|
|
272
|
+
child.once("exit", resolve);
|
|
273
|
+
});
|
|
274
|
+
if (exitCode !== 0) {
|
|
275
|
+
throw new Error(stderr.trim() || stdout.trim() || `MCP OAuth logout failed with exit code ${exitCode ?? "unknown"}`);
|
|
276
|
+
}
|
|
277
|
+
await restartClient(`mcp oauth logout ${name}`);
|
|
278
|
+
};
|
|
205
279
|
return {
|
|
206
280
|
onNotification(listener) {
|
|
207
281
|
notificationListeners.add(listener);
|
|
@@ -213,23 +287,25 @@ export function createCodexAppServerManager(args) {
|
|
|
213
287
|
const activeClient = await getClient();
|
|
214
288
|
return await activeClient.request(method, params, timeoutMs);
|
|
215
289
|
},
|
|
216
|
-
async
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (!activeClient) {
|
|
224
|
-
args.onLog?.(`codex app-server restart requested before start reason=${reason}`);
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
args.onLog?.(`restarting codex app-server reason=${reason}`);
|
|
228
|
-
await activeClient.stop();
|
|
229
|
-
await activeProxy?.stop().catch((error) => {
|
|
230
|
-
args.onLog?.(`failed to stop provider proxy: ${error instanceof Error ? error.message : String(error)}`);
|
|
290
|
+
async relayMcpOauthCallback(path, search) {
|
|
291
|
+
await getClient();
|
|
292
|
+
return await relayMcpOauthCallbackToLocalListener({
|
|
293
|
+
callbackPort: await resolveMcpOauthCallbackPort(),
|
|
294
|
+
callbackPathPrefix: mcpOauthCallbackPathPrefix,
|
|
295
|
+
path,
|
|
296
|
+
search,
|
|
231
297
|
});
|
|
232
298
|
},
|
|
299
|
+
async logoutMcpServer(name) {
|
|
300
|
+
const normalizedName = name.trim();
|
|
301
|
+
if (!/^[A-Za-z0-9_-]+$/.test(normalizedName)) {
|
|
302
|
+
throw new Error("Invalid MCP server name");
|
|
303
|
+
}
|
|
304
|
+
await runMcpLogout(normalizedName);
|
|
305
|
+
},
|
|
306
|
+
async restart(reason) {
|
|
307
|
+
await restartClient(reason);
|
|
308
|
+
},
|
|
233
309
|
async stop() {
|
|
234
310
|
const activeClient = client;
|
|
235
311
|
const activeProxy = providerProxy;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
const MAX_CALLBACK_QUERY_LENGTH = 16_384;
|
|
3
|
+
const MAX_CALLBACK_BODY_LENGTH = 512_000;
|
|
4
|
+
export function buildMcpOauthCallbackBaseUrl(args) {
|
|
5
|
+
const url = new URL(`/api/mcp-oauth/callback/${encodeURIComponent(args.userId)}/${encodeURIComponent(args.agentId)}`, `${args.serverBaseUrl.replace(/\/+$/, "")}/`);
|
|
6
|
+
return url.toString().replace(/\/$/, "");
|
|
7
|
+
}
|
|
8
|
+
export async function allocateMcpOauthCallbackPort() {
|
|
9
|
+
return await new Promise((resolve, reject) => {
|
|
10
|
+
const server = net.createServer();
|
|
11
|
+
server.unref();
|
|
12
|
+
server.once("error", reject);
|
|
13
|
+
server.listen(0, "0.0.0.0", () => {
|
|
14
|
+
const address = server.address();
|
|
15
|
+
if (!address || typeof address === "string") {
|
|
16
|
+
server.close();
|
|
17
|
+
reject(new Error("Failed to allocate MCP OAuth callback port"));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const port = address.port;
|
|
21
|
+
server.close((error) => {
|
|
22
|
+
if (error) {
|
|
23
|
+
reject(error);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
resolve(port);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export async function relayMcpOauthCallbackToLocalListener(args) {
|
|
32
|
+
if (!Number.isInteger(args.callbackPort) || args.callbackPort < 1 || args.callbackPort > 65_535) {
|
|
33
|
+
throw new Error("MCP OAuth callback listener is unavailable");
|
|
34
|
+
}
|
|
35
|
+
if (!args.path.startsWith(`${args.callbackPathPrefix}/`)) {
|
|
36
|
+
throw new Error("Invalid MCP OAuth callback path");
|
|
37
|
+
}
|
|
38
|
+
if (args.search.length > MAX_CALLBACK_QUERY_LENGTH) {
|
|
39
|
+
throw new Error("MCP OAuth callback query is too large");
|
|
40
|
+
}
|
|
41
|
+
const target = new URL(`http://127.0.0.1:${args.callbackPort}`);
|
|
42
|
+
target.pathname = args.path;
|
|
43
|
+
target.search = args.search;
|
|
44
|
+
const response = await fetch(target, {
|
|
45
|
+
method: "GET",
|
|
46
|
+
redirect: "manual",
|
|
47
|
+
signal: AbortSignal.timeout(30_000),
|
|
48
|
+
});
|
|
49
|
+
const body = await response.text();
|
|
50
|
+
if (body.length > MAX_CALLBACK_BODY_LENGTH) {
|
|
51
|
+
throw new Error("MCP OAuth callback response is too large");
|
|
52
|
+
}
|
|
53
|
+
const headers = {};
|
|
54
|
+
for (const name of ["content-type", "location", "cache-control"]) {
|
|
55
|
+
const value = response.headers.get(name);
|
|
56
|
+
if (value)
|
|
57
|
+
headers[name] = value;
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
status: response.status,
|
|
61
|
+
headers,
|
|
62
|
+
body,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import http from "node:http";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import { buildMcpOauthCallbackBaseUrl, relayMcpOauthCallbackToLocalListener, } from "./mcp-oauth-relay.js";
|
|
5
|
+
test("buildMcpOauthCallbackBaseUrl scopes callbacks to the user and agent", () => {
|
|
6
|
+
assert.equal(buildMcpOauthCallbackBaseUrl({
|
|
7
|
+
serverBaseUrl: "https://doer.example.com/",
|
|
8
|
+
userId: "user id",
|
|
9
|
+
agentId: "agent/id",
|
|
10
|
+
}), "https://doer.example.com/api/mcp-oauth/callback/user%20id/agent%2Fid");
|
|
11
|
+
});
|
|
12
|
+
test("relayMcpOauthCallbackToLocalListener preserves the callback path and query", async () => {
|
|
13
|
+
const server = http.createServer((request, response) => {
|
|
14
|
+
response.statusCode = 200;
|
|
15
|
+
response.setHeader("content-type", "text/html; charset=utf-8");
|
|
16
|
+
response.end(`<p>${request.url}</p>`);
|
|
17
|
+
});
|
|
18
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
19
|
+
const address = server.address();
|
|
20
|
+
assert.ok(address && typeof address !== "string");
|
|
21
|
+
try {
|
|
22
|
+
const result = await relayMcpOauthCallbackToLocalListener({
|
|
23
|
+
callbackPort: address.port,
|
|
24
|
+
callbackPathPrefix: "/api/mcp-oauth/callback/user/agent",
|
|
25
|
+
path: "/api/mcp-oauth/callback/user/agent/callback-id",
|
|
26
|
+
search: "?code=abc&state=xyz",
|
|
27
|
+
});
|
|
28
|
+
assert.equal(result.status, 200);
|
|
29
|
+
assert.equal(result.headers["content-type"], "text/html; charset=utf-8");
|
|
30
|
+
assert.equal(result.body, "<p>/api/mcp-oauth/callback/user/agent/callback-id?code=abc&state=xyz</p>");
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
test("relayMcpOauthCallbackToLocalListener rejects callbacks outside the configured path", async () => {
|
|
37
|
+
await assert.rejects(relayMcpOauthCallbackToLocalListener({
|
|
38
|
+
callbackPort: 4321,
|
|
39
|
+
callbackPathPrefix: "/api/mcp-oauth/callback/user/agent",
|
|
40
|
+
path: "/api/mcp-oauth/callback/other/agent/callback-id",
|
|
41
|
+
search: "",
|
|
42
|
+
}), /Invalid MCP OAuth callback path/);
|
|
43
|
+
});
|