Files
jiang 6a49d2209e feat(agent): unify MCP tools and adopt Responses delivery
Move project and host queries into native MCP with host-owned OAuth/context,
while retaining memory, skills, actions and publication plugins. Add nearby-model
queries and explicit chat/task/learning model defaults.

Adopt Responses HTTP/SSE and native steer/queue inbox delivery, preserving scoped
session ownership, pending input edits and investigation communication.

Disable local-provider discovery: Server /health allowed native vLLM to probe
/v1/models on port 8000. A native before/after regression prevents recurrence.

Validation: 170 focused Bun tests; service/plugin typechecks; native discovery,
provider-discovery and input-inbox smoke checks; no paid model inference.
2026-10-19 18:46:58 +08:00

88 lines
5.5 KiB
TypeScript

import { describe, expect, it } from "bun:test";
import express, { type Request } from "express";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { config } from "../../src/config.js";
import { attachTrustedAgentContext, getLocalAgentContext, ProductContextError } from "../../src/context/localContext.js";
import { registerMcpPolicyBridge, registerMcpStatusRoutes } from "../../src/routes/mcpRoutes.js";
import { removeRuntimeSessionContext, setRuntimeSessionContext } from "../../src/runtime/sessionContext.js";
import { SessionMetadataStore } from "../../src/sessions/metadataStore.js";
describe("MCP routes", () => {
it("protects the internal policy bridge against browser and context injection", async () => {
const app = express(); app.use(express.json());
let builds = 0;
registerMcpPolicyBridge(app, { internalToken: "MCP_TEST_ONLY", runtime: { mcpReadonlyTools: () => { builds++; return ["mcp_fixture_read"]; } } });
const server = app.listen(0, "127.0.0.1");
await new Promise<void>((resolve) => server.once("listening", resolve));
const url = `http://127.0.0.1:${(server.address() as { port: number }).port}/internal/mcp/session-policy`;
const sessionID = "mcp-route-test";
setRuntimeSessionContext({ sessionId: sessionID, clientSessionId: sessionID, actorKey: "PRIVATE_ACTOR", projectKey: "PRIVATE_PROJECT", traceId: "test", purpose: "task_execution" });
const post = (body: unknown, headers: Record<string, string> = {}) => fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "x-agent-internal-token": "MCP_TEST_ONLY", ...headers }, body: JSON.stringify(body) });
try {
expect((await post({ session_id: sessionID }, { "x-agent-internal-token": "wrong" })).status).toBe(403);
expect((await post({ session_id: sessionID }, { Origin: "https://browser" })).status).toBe(403);
expect((await post({ session_id: sessionID }, { "Sec-Fetch-Site": "same-origin" })).status).toBe(403);
expect((await post({ session_id: sessionID, project_id: "other" })).status).toBe(400);
expect((await post({ session_id: "missing" })).status).toBe(404);
expect(await (await post({ session_id: sessionID })).json()).toEqual({ mode: "readonly", readonly_tools: ["mcp_fixture_read"], query_tools: [] });
expect(builds).toBe(1);
} finally {
removeRuntimeSessionContext(sessionID);
await new Promise<void>((resolve) => server.close(() => resolve()));
}
});
it("requires verified session ownership and returns constant errors without credentials", async () => {
const originalFetch = globalThis.fetch;
const previousEnv = config.NODE_ENV;
const root = await mkdtemp(join(tmpdir(), "mcp-status-test-"));
const store = new SessionMetadataStore(root);
const app = express();
let calls = 0;
let failed = false;
globalThis.fetch = (async (input: Parameters<typeof fetch>[0], init: Parameters<typeof fetch>[1]) => {
if (String(input).endsWith("/api/v1/agent-auth-context")) {
const headers = new Headers(init?.headers);
return Response.json({ user_id: headers.get("Authorization") === "Bearer OWNER" ? "owner" : "other", project_id: headers.get("X-Project-Id"), network: "supply" });
}
return originalFetch(input, init);
}) as typeof fetch;
config.NODE_ENV = "production";
const owner = { header: (name: string) => ({ authorization: "Bearer OWNER", "x-project-id": "project" })[name.toLowerCase() as "authorization" | "x-project-id"] } as Request;
await attachTrustedAgentContext(owner);
await store.ensure({ ...getLocalAgentContext(owner), sessionId: "private-session" });
const router = express.Router();
router.use(async (req, res, next) => {
try { await attachTrustedAgentContext(req); next(); }
catch (error) { res.status(error instanceof ProductContextError ? error.status : 503).json({ message: "authentication failed" }); }
});
registerMcpStatusRoutes(router, { sessionMetadataStore: store, runtime: { mcpStatus: async () => {
calls++;
if (failed) throw new Error("SECRET_URL_AND_TOKEN");
return { configuration_errors: [], servers: [{ name: "mcp_fixture", type: "remote", status: "needs_auth", configured_protocol: "auto", tools: [] }] };
} } });
app.use("/api/v1", router);
const server = app.listen(0, "127.0.0.1");
await new Promise<void>((resolve) => server.once("listening", resolve));
const url = `http://127.0.0.1:${(server.address() as { port: number }).port}/api/v1/session/private-session/mcp`;
try {
expect((await originalFetch(url)).status).toBe(401);
const headers = { Authorization: "Bearer OWNER", "X-Project-Id": "project" };
expect((await originalFetch(url, { headers: { ...headers, Authorization: "Bearer OTHER" } })).status).toBe(404);
expect((await originalFetch(url, { headers: { ...headers, "X-Project-Id": "other" } })).status).toBe(404);
expect(calls).toBe(0);
expect(await (await originalFetch(url, { headers })).json()).toEqual({ servers: [{ name: "mcp_fixture", type: "remote", status: "needs_auth", configured_protocol: "auto", tools: [] }] });
failed = true;
const error = await originalFetch(url, { headers });
expect(error.status).toBe(503);
expect(await error.json()).toEqual({ message: "MCP runtime unavailable" });
} finally {
globalThis.fetch = originalFetch; config.NODE_ENV = previousEnv;
await new Promise<void>((resolve) => server.close(() => resolve()));
await rm(root, { recursive: true });
}
});
});