import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client"; import { approveConsent, BASE, body, call, MODERN, patClient, quote, agentToken, seedToken, seedWorld, sha256Hex, sql, sqlRows, teardown, TestOAuthProvider, toolText, } from "./mcp-helpers"; import type { World } from "./mcp-helpers"; let world: World; beforeAll(async () => { world = await seedWorld(); }, 120_000); afterAll(async () => { await teardown(world); }); describe("authorization", () => { test("advertises protected resource and authorization server metadata", async () => { const prm = await body<{ resource: string }>(await fetch(`${BASE}/.well-known/oauth-protected-resource/mcp`)); expect(prm.resource).toBe(`${BASE}/mcp`); const as = await body<{ code_challenge_methods_supported: string[]; registration_endpoint: string }>( await fetch(`${BASE}/.well-known/oauth-authorization-server`), ); expect(as.code_challenge_methods_supported).toEqual(["S256"]); expect(as.registration_endpoint).toBe(`${BASE}/oauth/register`); }); test("rejects requests without a valid token", async () => { const missing = await fetch(`${BASE}/mcp`, { method: "POST", body: "{}", headers: { "content-type": "application/json" }, }); expect(missing.status).toBe(401); expect(missing.headers.get("www-authenticate")).toContain("resource_metadata="); const bogus = await fetch(`${BASE}/mcp`, { method: "POST", body: "{}", headers: { authorization: "Bearer gitfor_pat_nope" }, }); expect(bogus.status).toBe(401); expect(bogus.headers.get("www-authenticate")).toContain('error="invalid_token"'); }); test("full OAuth 2.1 flow: DCR, consent listing repos and scopes, PKCE, refresh rotation and reuse revocation", async () => { const provider = new TestOAuthProvider( `contents:write pulls:write issues:write actions:read repo:${world.owner}/${world.repo}`, ); const client = new Client( { name: "e2e-oauth", version: "1.0.0" }, { versionNegotiation: { mode: { pin: MODERN } } }, ); const transport = new StreamableHTTPClientTransport(new URL(`${BASE}/mcp`), { authProvider: provider }); await client.connect(transport).catch(() => undefined); expect(provider.authorizationUrl).not.toBeNull(); const consent = await approveConsent(world, provider.authorizationUrl as URL); expect(consent.page).toContain(`${world.owner}/${world.repo}`); expect(consent.page).toContain("Read and write"); expect(consent.iss).toBe(BASE); await transport.finishAuth(consent.code, consent.iss ?? undefined); const connected = new Client( { name: "e2e-oauth", version: "1.0.0" }, { versionNegotiation: { mode: { pin: MODERN } } }, ); await connected.connect(new StreamableHTTPClientTransport(new URL(`${BASE}/mcp`), { authProvider: provider })); const { tools } = await connected.listTools(); expect(tools.map((tool) => tool.name)).toContain("commit_files"); const commit = await call(connected, "commit_files", { owner: world.owner, repo: world.repo, branch: "main", message: "Initial commit from the MCP e2e test", files: [{ path: "AGENT.md", content: "# hello from an agent\n" }], }); expect(commit.isError).toBeFalsy(); const read = await call(connected, "read_file", { owner: world.owner, repo: world.repo, path: "AGENT.md", ref: "main", }); expect(read.structuredContent?.content).toBe("# hello from an agent\n"); const log = sqlRows<{ outcome: string }>( `SELECT outcome FROM mcp_audit WHERE user_id = ${quote(world.userId)} AND tool = 'commit_files' AND agent_name = 'E2E Agent'`, ); expect(log.map((row) => row.outcome)).toContain("ok"); const resource = await connected.readResource({ uri: `gitfor://${world.owner}/${world.repo}/main/AGENT.md` }); expect(JSON.stringify(resource.contents)).toContain("hello from an agent"); const restWithMcpToken = await fetch(`${BASE}/api/v1/repos/${world.owner}/${world.repo}`, { headers: { authorization: `Bearer ${provider.saved?.access_token}` }, }); expect(restWithMcpToken.status).toBe(401); const outside = await call(connected, "get_repo", { owner: world.owner, repo: world.otherRepo }); expect(outside.isError).toBe(true); expect(toolText(outside)).toContain("not found"); const refreshed = await fetch(`${BASE}/oauth/token`, { method: "POST", body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: provider.saved?.refresh_token ?? "", client_id: String(provider.info?.client_id), }), }); expect(refreshed.status).toBe(200); const rotated = await body<{ access_token: string }>(refreshed); const reused = await fetch(`${BASE}/oauth/token`, { method: "POST", body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: provider.saved?.refresh_token ?? "", client_id: String(provider.info?.client_id), }), }); expect(reused.status).toBe(400); const afterReuse = await fetch(`${BASE}/mcp`, { method: "POST", headers: { authorization: `Bearer ${rotated.access_token}`, "content-type": "application/json" }, body: "{}", }); expect(afterReuse.status).toBe(401); const old = await fetch(`${BASE}/mcp`, { method: "POST", headers: { authorization: `Bearer ${provider.saved?.access_token}`, "content-type": "application/json" }, body: "{}", }); expect(old.status).toBe(401); await connected.close(); }, 60_000); }); describe("token audience", () => { test("agent tokens only work on /mcp unless git and REST access was granted", async () => { const mcpOnly = await agentToken(world, `aud-mcp-${world.suffix}`, { contents: "read" }); const forge = await agentToken(world, `aud-forge-${world.suffix}`, { contents: "read" }, true); const repoPath = `${BASE}/api/v1/repos/${world.owner}/${world.repo}`; const gitPath = `${BASE}/${world.owner}/${world.repo}.git/info/refs?service=git-upload-pack`; const basic = (token: string) => `Basic ${btoa(`x:${token}`)}`; expect((await fetch(repoPath, { headers: { authorization: `Bearer ${mcpOnly}` } })).status).toBe(401); expect((await fetch(gitPath, { headers: { authorization: basic(mcpOnly) } })).status).not.toBe(200); expect((await fetch(repoPath, { headers: { authorization: `Bearer ${forge}` } })).status).toBe(200); expect((await fetch(gitPath, { headers: { authorization: basic(forge) } })).status).toBe(200); const { client, transport } = patClient(mcpOnly, "ignored"); await client.connect(transport); const listed = await call(client, "list_tree", { owner: world.owner, repo: world.repo }); expect(listed.isError).toBeFalsy(); await client.close(); }, 30_000); }); describe("tool permissions", () => { test("a read-only token is denied writes and the denial is audited", async () => { const token = seedToken(world, "e2e read-only", { contents: "read" }, [world.repoId]); const { client, transport } = patClient(token, "readonly-agent"); await client.connect(transport); const listing = await call(client, "list_tree", { owner: world.owner, repo: world.repo }); expect(listing.isError).toBeFalsy(); const denied = await call(client, "commit_files", { owner: world.owner, repo: world.repo, branch: "main", message: "should fail", files: [{ path: "nope.txt", content: "x" }], }); expect(denied.isError).toBe(true); const otherRepo = await call(client, "read_file", { owner: world.owner, repo: world.otherRepo, path: "README.md" }); expect(otherRepo.isError).toBe(true); const audits = sqlRows<{ outcome: string; agent_name: string }>( `SELECT outcome, agent_name FROM mcp_audit WHERE user_id = ${quote(world.userId)} AND tool = 'commit_files' AND outcome = 'denied'`, ); expect( audits.some((row) => row.agent_name === `unregistered agent via @${world.owner} [claims readonly-agent]`), ).toBe(true); await client.close(); }, 30_000); }); describe("agent workflow", () => { test("workspace branch, attributed pull request, and elicitation-confirmed token delegation", async () => { const token = await agentToken(world, "workflow-agent", { contents: "write", pulls: "write" }, true); const client = new Client( { name: "workflow-agent", version: "1.0.0" }, { versionNegotiation: { mode: { pin: MODERN } }, capabilities: { elicitation: { form: {} } } }, ); let prompted = ""; client.setRequestHandler("elicitation/create", async (request) => { prompted = String(request.params.message); return { action: "accept", content: { approve: true } }; }); await client.connect( new StreamableHTTPClientTransport(new URL(`${BASE}/mcp`), { requestInit: { headers: { authorization: `Bearer ${token}`, "x-gitfor-agent": "spoofed-name" } }, }), ); const workspace = await call(client, "create_workspace", { owner: world.owner, repo: world.repo, task: `Fix ${world.suffix}`, }); expect(workspace.isError).toBeFalsy(); const branch = String(workspace.structuredContent?.branch); expect(branch).toBe(`agent/workflow-agent/fix-${world.suffix}`); const duplicate = await call(client, "create_workspace", { owner: world.owner, repo: world.repo, task: `Fix ${world.suffix}`, }); expect(duplicate.isError).toBe(true); const commit = await call(client, "commit_files", { owner: world.owner, repo: world.repo, branch, message: "Agent fix", files: [{ path: "fix.txt", content: "fixed\n" }], }); expect(commit.isError).toBeFalsy(); const opened = await call(client, "open_pull", { owner: world.owner, repo: world.repo, title: "Agent fix", head: branch, }); expect(opened.isError).toBeFalsy(); const pull = opened.structuredContent?.pull as { number: number; body: string }; expect(pull.body).toContain("workflow-agent"); expect(pull.body).not.toContain("spoofed-name"); expect(pull.body).toContain(`@${world.owner}`); const details = await call(client, "get_pull", { owner: world.owner, repo: world.repo, number: pull.number }); expect(details.structuredContent?.patch).toContain("+fixed"); const commits = details.structuredContent?.commits as Array<{ sha: string }>; expect(commits.length).toBe(1); const minted = await call(client, "create_repo_token", { owner: world.owner, repo: world.repo, name: "sub-agent", permissions: { contents: "read" }, }); expect(minted.isError).toBeFalsy(); expect(prompted).toContain("sub-agent"); expect(String(minted.structuredContent?.token)).toStartWith("gitfor_pat_"); const escalate = await call(client, "create_repo_token", { owner: world.owner, repo: world.repo, name: "too much", permissions: { admin: "write" }, }); expect(escalate.isError).toBe(true); const child = String(minted.structuredContent?.token); const childWorks = await fetch(`${BASE}/api/v1/repos/${world.owner}/${world.repo}`, { headers: { authorization: `Bearer ${child}` }, }); expect(childWorks.status).toBe(200); await call(client, "release_claims", { owner: world.owner, repo: world.repo }); await client.close(); sql(`DELETE FROM access_tokens WHERE token_hash = ${quote(sha256Hex(token))}`); const childRevoked = await fetch(`${BASE}/api/v1/repos/${world.owner}/${world.repo}`, { headers: { authorization: `Bearer ${child}` }, }); expect(childRevoked.status).toBe(401); }, 60_000); });