Blame · Line-by-line history
mcp-private-read.test.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| 85fc0ef | 1 | /** |
| 2 | * Regression: the v1 "repo:*" READ tools must NOT be hardcoded public-only. | |
| 3 | * | |
| 4 | * They used to reject EVERY private repo with | |
| 5 | * `${owner}/${repo} is private; v1 MCP read tool is public-only` | |
| 6 | * even for the authenticated owner. That meant a user who signed into the | |
| 7 | * OAuth connector (e.g. Claude Desktop) could not read their own private | |
| 8 | * repos at all — Vapron never showed up. Now they gate on the caller's | |
| 9 | * actual access via resolveReadableRepo(): public, OR private + caller can | |
| 10 | * read (owner / collaborator / org member). | |
| 11 | * | |
| 12 | * Two invariants are locked here without needing a live DB: | |
| 13 | * 1. No read-tool description still advertises "public-only". | |
| 14 | * 2. Privacy contract: an unknown repo throws METHOD_NOT_FOUND (the same | |
| 15 | * error a private repo the caller can't see would throw — we never | |
| 16 | * confirm a private repo's existence). | |
| 17 | */ | |
| 18 | import { describe, it, expect } from "bun:test"; | |
| 19 | import { ERR_METHOD_NOT_FOUND, McpError } from "../lib/mcp"; | |
| 20 | import { defaultTools } from "../lib/mcp-tools"; | |
| 21 | ||
| 22 | const HAS_DB = Boolean(process.env.DATABASE_URL); | |
| 23 | const anonCtx = { userId: null, scopes: [] }; | |
| 24 | ||
| 25 | const READ_TOOLS = [ | |
| 26 | "gluecron_repo_read_file", | |
| 27 | "gluecron_repo_list_issues", | |
| 28 | "gluecron_repo_explain_codebase", | |
| 29 | "gluecron_repo_health", | |
| 30 | "gluecron_repo_search", | |
| 31 | ]; | |
| 32 | ||
| 33 | describe("v1 MCP read tools are no longer public-only", () => { | |
| 34 | it("no read-tool description advertises 'public-only'", () => { | |
| 35 | const tools = defaultTools(); | |
| 36 | for (const name of READ_TOOLS) { | |
| 37 | const handler = tools[name]; | |
| 38 | expect(handler).toBeDefined(); | |
| 39 | expect(handler.tool.description.toLowerCase()).not.toContain("public-only"); | |
| 40 | } | |
| 41 | }); | |
| 42 | ||
| 43 | it.skipIf(!HAS_DB)( | |
| 44 | "unknown repo → METHOD_NOT_FOUND (privacy contract), for the read tools that resolve a specific repo", | |
| 45 | async () => { | |
| 46 | const tools = defaultTools(); | |
| 47 | const cases: Array<{ name: string; args: Record<string, unknown> }> = [ | |
| 48 | { | |
| 49 | name: "gluecron_repo_read_file", | |
| 50 | args: { owner: "nobody-xyz-fixture", repo: "nope", path: "README.md" }, | |
| 51 | }, | |
| 52 | { | |
| 53 | name: "gluecron_repo_list_issues", | |
| 54 | args: { owner: "nobody-xyz-fixture", repo: "nope" }, | |
| 55 | }, | |
| 56 | { | |
| 57 | name: "gluecron_repo_explain_codebase", | |
| 58 | args: { owner: "nobody-xyz-fixture", repo: "nope" }, | |
| 59 | }, | |
| 60 | { | |
| 61 | name: "gluecron_repo_health", | |
| 62 | args: { owner: "nobody-xyz-fixture", repo: "nope" }, | |
| 63 | }, | |
| 64 | ]; | |
| 65 | for (const tc of cases) { | |
| 66 | const handler = tools[tc.name]; | |
| 67 | await expect(handler.run(tc.args, anonCtx)).rejects.toMatchObject({ | |
| 68 | code: ERR_METHOD_NOT_FOUND, | |
| 69 | }); | |
| 70 | } | |
| 71 | } | |
| 72 | ); | |
| 73 | }); |