Skip to content
220 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { readFileSync, readdirSync } from "node:fs";
3import { DatabaseSync } from "node:sqlite";
4import { test } from "node:test";
5
6import * as connectors from "./connectors.ts";
7import type { Fetch } from "./mcp-client.ts";
8
9function fakeD1(): D1Database {
10 const db = new DatabaseSync(":memory:");
11 const dir = new URL("../migrations/", import.meta.url);
12 for (const file of readdirSync(dir).sort()) db.exec(readFileSync(new URL(file, dir), "utf8"));
13 const statement = (sql: string, params: unknown[] = []) => ({
14 bind: (...values: unknown[]) => statement(sql, values),
15 first: async () => (db.prepare(sql).get(...(params as never[])) as unknown) ?? null,
16 run: async () => {
17 const r = db.prepare(sql).run(...(params as never[]));
18 return { meta: { changes: Number(r.changes) } };
19 },
20 all: async () => ({ results: db.prepare(sql).all(...(params as never[])) }),
21 });
22 const batch = async (statements: { run: () => Promise<unknown> }[]) => Promise.all(statements.map((s) => s.run()));
23 return { prepare: (sql: string) => statement(sql), batch } as unknown as D1Database;
24}
25
26const KEY = "ab".repeat(32);
27const ws = "wsp_acme";
28const at = new Date("2026-10-11T12:00:00.000Z");
29
30/** An MCP server with an OAuth authorization server beside it, and a tool, all in one fake fetch. */
31function fakeServer(options: { needsAuth?: boolean; register?: boolean; token?: string; expiresIn?: number } = {}): { fetchFn: Fetch; seen: string[] } {
32 const seen: string[] = [];
33 const needsAuth = options.needsAuth ?? true;
34 const token = options.token ?? "tok-1";
35 let refreshed = 0;
36 const fetchFn: Fetch = async (url, init) => {
37 const u = new URL(url);
38 const auth = new Headers(init?.headers).get("authorization") ?? new Headers(init?.headers).get("x-key");
39 seen.push(`${init?.method ?? "GET"} ${u.pathname}${auth ? ` [${auth}]` : ""}`);
40 if (u.pathname === "/.well-known/oauth-protected-resource/mcp") return Response.json({ resource: "https://mcp.example.com/mcp", authorization_servers: ["https://auth.example.com"] });
41 if (u.hostname === "auth.example.com" && u.pathname === "/.well-known/oauth-authorization-server") {
42 return Response.json({ issuer: "https://auth.example.com", authorization_endpoint: "https://auth.example.com/authorize", token_endpoint: "https://auth.example.com/token", ...(options.register === false ? {} : { registration_endpoint: "https://auth.example.com/register" }), code_challenge_methods_supported: ["S256"], scopes_supported: ["read", "write"] });
43 }
44 if (u.pathname === "/register") return Response.json({ client_id: "client-1" }, { status: 201 });
45 if (u.pathname === "/token") {
46 const form = new URLSearchParams(String(init?.body));
47 if (form.get("grant_type") === "refresh_token") {
48 refreshed++;
49 return Response.json({ access_token: `tok-refreshed-${refreshed}`, refresh_token: "ref-2", expires_in: 3600 });
50 }
51 if (form.get("code") !== "code-1" || !form.get("code_verifier")) return Response.json({ error: "invalid_grant" }, { status: 400 });
52 return Response.json({ access_token: token, refresh_token: "ref-1", expires_in: options.expiresIn ?? 3600 });
53 }
54 if (u.pathname === "/mcp") {
55 if (needsAuth && auth !== `Bearer ${token}` && !auth?.startsWith("Bearer tok-refreshed") && auth !== "secret-key") {
56 return new Response("", { status: 401, headers: { "www-authenticate": 'Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp"' } });
57 }
58 const body = JSON.parse(String(init?.body)) as { id?: number; method: string };
59 if (body.method === "initialize") return Response.json({ jsonrpc: "2.0", id: body.id, result: { protocolVersion: "2025-06-18", capabilities: { tools: {} }, serverInfo: { name: "example" } } }, { headers: { "mcp-session-id": "s1" } });
60 if (body.method === "notifications/initialized") return new Response(null, { status: 202 });
61 if (body.method === "tools/list") return Response.json({ jsonrpc: "2.0", id: body.id, result: { tools: [{ name: "search", description: "Searches", annotations: { readOnlyHint: true }, inputSchema: { type: "object", properties: { q: { type: "string" } } } }, { name: "create", description: "Creates" }] } });
62 if (body.method === "tools/call") return Response.json({ jsonrpc: "2.0", id: body.id, result: { content: [{ type: "text", text: "found 2" }] } });
63 return Response.json({ jsonrpc: "2.0", id: body.id, error: { code: -32601, message: "no" } });
64 }
65 return new Response("no", { status: 404 });
66 };
67 return { fetchFn, seen };
68}
69
70test("secrets are sealed to one connector and never come back out for another", async () => {
71 const key = await connectors.sealingKey(KEY);
72 assert.ok(key);
73 const sealed = await connectors.seal(key, "con_1", { header: "secret-key" });
74 assert.deepEqual(await connectors.open(key, "con_1", sealed), { header: "secret-key" });
75 assert.equal(await connectors.open(key, "con_2", sealed), null);
76 assert.equal(await connectors.sealingKey("short"), null);
77 assert.equal(await connectors.sealingKey(undefined), null);
78});
79
80test("a connector from the directory signs in with the browser: discovery, registration, PKCE, tokens kept sealed, tools listed, token refreshed when old", async () => {
81 const db = fakeD1();
82 const { fetchFn, seen } = fakeServer({ expiresIn: 300 });
83 const deps: connectors.Deps = { key: await connectors.sealingKey(KEY), fetchFn, site: "https://g1t.sh", now: at };
84 // The directory entry's address is Notion's; the fake answers any host, so the flow is what is checked.
85 const added = await connectors.addConnector(db, deps, ws, { from: { kind: "address", name: "example", url: "https://mcp.example.com/mcp" }, auth: "oauth" }, "chase");
86 assert.ok(added.ok);
87 const row = added.value;
88 assert.equal(connectors.stateOf(row), "needs_sign_in");
89 assert.equal(row.secrets, null);
90 const started = await connectors.startSignIn(db, deps, row, "https://g1t.sh/acme/-/customize/connectors/callback");
91 assert.ok(started.ok);
92 const url = new URL(started.value.url);
93 assert.equal(url.origin + url.pathname, "https://auth.example.com/authorize");
94 assert.equal(url.searchParams.get("client_id"), "client-1");
95 assert.equal(url.searchParams.get("code_challenge_method"), "S256");
96 assert.equal(url.searchParams.get("resource"), "https://mcp.example.com/mcp");
97 assert.equal(url.searchParams.get("scope"), "read write");
98 const state = url.searchParams.get("state")!;
99 assert.equal(connectors.connectorOfState(state), row.id);
100 assert.ok(seen.some((s) => s.startsWith("POST /register")));
101 const pending = (await connectors.connectorById(db, ws, row.id))!;
102 const wrong = await connectors.finishSignIn(db, deps, pending, "con_x.nope", "code-1", "chase");
103 assert.equal(!wrong.ok && wrong.error.code, "invalid");
104 const finished = await connectors.finishSignIn(db, deps, pending, state, "code-1", "chase");
105 assert.ok(finished.ok);
106 assert.equal(connectors.stateOf(finished.value), "connected");
107 assert.equal(finished.value.signed_in_as, "chase");
108 assert.equal(finished.value.pending, null);
109 assert.deepEqual(JSON.parse(finished.value.tools).map((t: { name: string; kind: string }) => [t.name, t.kind]), [
110 ["search", "read"],
111 ["create", "write"],
112 ]);
113 assert.ok(finished.value.secrets && !finished.value.secrets.includes("tok-1"), "the token is sealed");
114 // Headers now: the bearer token; later, past expiry, refreshed and kept.
115 assert.deepEqual(await connectors.headersFor(db, deps, finished.value), { Authorization: "Bearer tok-1" });
116 const later: connectors.Deps = { ...deps, now: new Date(at.getTime() + 400_000) };
117 const fresh = (await connectors.connectorById(db, ws, row.id))!;
118 assert.deepEqual(await connectors.headersFor(db, later, fresh), { Authorization: "Bearer tok-refreshed-1" });
119 assert.deepEqual(await connectors.headersFor(db, later, (await connectors.connectorById(db, ws, row.id))!), { Authorization: "Bearer tok-refreshed-1" }, "the refreshed token was kept");
120 // Signed out: needs a sign-in again, and nothing secret remains.
121 const out = await connectors.signOut(db, deps, fresh);
122 assert.ok(out.ok);
123 assert.equal(connectors.stateOf(out.value), "needs_sign_in");
124 assert.equal(out.value.secrets, null);
125});
126
127test("a connector with a key sends it as a header; without CONNECTORS_KEY no key can be kept", async () => {
128 const db = fakeD1();
129 const { fetchFn, seen } = fakeServer();
130 const deps: connectors.Deps = { key: await connectors.sealingKey(KEY), fetchFn, site: "https://g1t.sh", now: at };
131 const added = await connectors.addConnector(db, deps, ws, { from: { kind: "address", name: "keyed", url: "https://mcp.example.com/mcp" }, auth: "header", header: { name: "X-Key", value: "secret-key" } }, "chase");
132 assert.ok(added.ok);
133 assert.equal(connectors.stateOf(added.value), "connected");
134 assert.equal(added.value.header_name, "X-Key");
135 assert.equal(JSON.parse(added.value.tools).length, 2);
136 assert.ok(seen.some((s) => s.includes("[secret-key]")));
137 assert.deepEqual(await connectors.headersFor(db, deps, added.value), { "X-Key": "secret-key" });
138 const keyless: connectors.Deps = { ...deps, key: null };
139 const refused = await connectors.addConnector(db, keyless, ws, { from: { kind: "address", name: "other", url: "https://mcp.example.com/mcp" }, auth: "header", header: { name: "X-Key", value: "x" } }, "chase");
140 assert.equal(!refused.ok && refused.error.code, "invalid");
141 // An open server needs nothing.
142 const open = await connectors.addConnector(db, deps, ws, { from: { kind: "address", name: "open", url: "https://open.example.com/mcp" }, auth: "none" }, "chase");
143 assert.ok(open.ok);
144 assert.equal(connectors.stateOf(open.value), "problem", "the fake answers only its own host with tools");
145 // Names are unique; a second with the same name gets a number; addresses are checked.
146 const again = await connectors.addConnector(db, deps, ws, { from: { kind: "address", name: "keyed", url: "https://mcp.example.com/mcp" }, auth: "none" }, "chase");
147 assert.ok(again.ok);
148 assert.equal(again.value.name, "keyed-2");
149 const local = await connectors.addConnector(db, deps, ws, { from: { kind: "address", name: "local", url: "http://localhost:3000/mcp" }, auth: "none" }, "chase");
150 assert.equal(!local.ok && local.error.code, "invalid");
151 const directory = await connectors.addConnector(db, deps, ws, { from: { kind: "directory", id: "notion" }, auth: "none" }, "chase");
152 assert.ok(directory.ok);
153 assert.deepEqual({ name: directory.value.name, title: directory.value.title, auth: directory.value.auth, url: directory.value.url }, { name: "notion", title: "Notion", auth: "oauth", url: "https://mcp.notion.com/mcp" });
154 assert.equal(!(await connectors.addConnector(db, deps, ws, { from: { kind: "directory", id: "sentry" }, auth: "none" }, "chase")).ok, true, "only directory entries with a server");
155});
156
157test("grants: every agent, a team, one agent; the closest wins; an agent's turn gets connected ones as servers", async () => {
158 const db = fakeD1();
159 const { fetchFn } = fakeServer();
160 const deps: connectors.Deps = { key: await connectors.sealingKey(KEY), fetchFn, site: "https://g1t.sh", now: at };
161 const a = (await connectors.addConnector(db, deps, ws, { from: { kind: "address", name: "alpha", url: "https://mcp.example.com/mcp" }, auth: "header", header: { name: "X-Key", value: "secret-key" } }, "chase")) as { ok: true; value: connectors.ConnectorRow };
162 const b = (await connectors.addConnector(db, deps, ws, { from: { kind: "address", name: "beta", url: "https://mcp.example.com/mcp" }, auth: "oauth" }, "chase")) as { ok: true; value: connectors.ConnectorRow };
163 assert.ok(a.ok && b.ok);
164 assert.ok((await connectors.grant(db, deps, ws, a.value.id, "workspace", "", "chase")).ok);
165 assert.ok((await connectors.grant(db, deps, ws, a.value.id, "agent", "agt_1", "chase")).ok);
166 assert.ok((await connectors.grant(db, deps, ws, b.value.id, "team", "qa", "chase")).ok);
167 const same = await connectors.grant(db, deps, ws, a.value.id, "workspace", "", "ana");
168 assert.ok(same.ok && same.value.granted_by === "chase", "the same grant twice is one");
169 const reach = await connectors.reaching(db, ws, "agt_1", ["qa"]);
170 assert.deepEqual(
171 reach.map((r) => [r.row.name, r.via]),
172 [
173 ["alpha", "agent"],
174 ["beta", "team"],
175 ],
176 );
177 assert.deepEqual((await connectors.reaching(db, ws, "agt_2", [])).map((r) => [r.row.name, r.via]), [["alpha", "workspace"]]);
178 const server = connectors.asServer(a.value);
179 assert.equal(server.id, a.value.id);
180 assert.equal(server.tools.length, 2);
181 const lines = connectors.agentConnectorsOut(reach, [], { teamName: (slug) => slug.toUpperCase() });
182 assert.deepEqual(
183 lines.map((l) => [l.name, l.state, l.via_label, l.tools]),
184 [
185 ["alpha", "connected", "This agent", 2],
186 ["beta", "needs_sign_in", "Team BETA", 0],
187 ],
188 );
189 const labels = { agents: new Map([["agt_1", { handle: "otto", display_name: "Otto" }]]), teams: new Map([["qa", "Quality"]]) };
190 const out = await connectors.connectorsOut(db, ws, labels);
191 assert.deepEqual(out.find((c) => c.name === "alpha")!.grants.map((g) => g.label), ["Every agent", "@otto"]);
192 assert.deepEqual(out.find((c) => c.name === "beta")!.grants.map((g) => g.label), ["Quality"]);
193 const grantId = out.find((c) => c.name === "alpha")!.grants[1]!.id;
194 assert.ok((await connectors.revoke(db, ws, grantId)).ok);
195 assert.deepEqual((await connectors.reaching(db, ws, "agt_1", [])).map((r) => r.via), ["workspace"]);
196 // Removed: gone from agents at once, with its grants and secrets.
197 assert.ok((await connectors.removeConnector(db, deps, ws, a.value.id)).ok);
198 assert.deepEqual((await connectors.reaching(db, ws, "agt_1", ["qa"])).map((r) => [r.row.id, r.via]), [[b.value.id, "team"]]);
199 assert.equal((await db.prepare("SELECT secrets FROM connectors WHERE id = ?").bind(a.value.id).first<{ secrets: string | null }>())!.secrets, null);
200});
201
202test("a switched-off plugin's connectors stop reaching agents; switched on, they are back", async () => {
203 const db = fakeD1();
204 const { fetchFn } = fakeServer();
205 const deps: connectors.Deps = { key: await connectors.sealingKey(KEY), fetchFn, site: "https://g1t.sh", now: at };
206 await db
207 .prepare("INSERT INTO plugin_installs (id, workspace_id, ref, name, marketplace, description, fetch, installed_by, installed_at, updated_at, enabled) VALUES ('plg_1', ?, 'release@acme', 'release', 'acme', '', '{}', 'chase', ?, ?, 1)")
208 .bind(ws, at.toISOString(), at.toISOString())
209 .run();
210 const fromPlugin = (await connectors.addConnector(db, deps, ws, { from: { kind: "plugin", ref: "release@acme", server: "tracker" }, auth: "header", header: { name: "X-Key", value: "secret-key" } }, "chase", { title: "Release: tracker", url: "https://mcp.example.com/mcp" })) as { ok: true; value: connectors.ConnectorRow };
211 const byHand = (await connectors.addConnector(db, deps, ws, { from: { kind: "address", name: "mine", url: "https://mcp.example.com/mcp" }, auth: "header", header: { name: "X-Key", value: "secret-key" } }, "chase")) as { ok: true; value: connectors.ConnectorRow };
212 assert.ok(fromPlugin.ok && byHand.ok);
213 await connectors.grant(db, deps, ws, fromPlugin.value.id, "workspace", "", "chase");
214 await connectors.grant(db, deps, ws, byHand.value.id, "workspace", "", "chase");
215 assert.deepEqual((await connectors.reaching(db, ws, "agt_1", [])).map((r) => r.row.name).sort(), ["mine", "tracker"]);
216 await db.prepare("UPDATE plugin_installs SET enabled = 0 WHERE id = 'plg_1'").run();
217 assert.deepEqual((await connectors.reaching(db, ws, "agt_1", [])).map((r) => r.row.name), ["mine"]);
218 await db.prepare("UPDATE plugin_installs SET enabled = 1 WHERE id = 'plg_1'").run();
219 assert.equal((await connectors.reaching(db, ws, "agt_1", [])).length, 2);
220});