| 1 | import assert from "node:assert/strict"; |
| 2 | import { readFileSync, readdirSync } from "node:fs"; |
| 3 | import { DatabaseSync } from "node:sqlite"; |
| 4 | import { test } from "node:test"; |
| 5 | |
| 6 | import * as connectors from "./connectors.ts"; |
| 7 | import type { Fetch } from "./mcp-client.ts"; |
| 8 | |
| 9 | function 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 | |
| 26 | const KEY = "ab".repeat(32); |
| 27 | const ws = "wsp_acme"; |
| 28 | const 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. */ |
| 31 | function 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 | |
| 70 | test("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 | |
| 80 | test("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 | |
| 127 | test("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 | |
| 157 | test("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 | |
| 202 | test("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 | }); |