From e9d0bfb9195d9cf7c057700deb3f408f1a6e1ec3 Mon Sep 17 00:00:00 2001 From: CodeGovindz Date: Tue, 27 Oct 2026 01:02:42 +0530 Subject: [PATCH] P3: remote MCP server and Alan plugin - POST /api/mcp: stateless Streamable HTTP MCP server (JSON responses, new server per request) with two read-only tools: - verify_pr: check every claim in a PR; returns the markdown report and a compact JSON summary. - check_claims: check claims an agent is about to write against the PR's diff and CI before posting them. Draft reports never enter the report cache. - The repo is an Alan plugin marketplace: .cursor-plugin/marketplace.json and plugins/receipts/ (manifest, mcp.json, receipts-verify skill, logo). Passes tryalan-ai/plugins' scripts/validate.mjs. mcp.json gets the deployment URL once the app is on Vercel. - Logo doubles as the site icon. --- .cursor-plugin/marketplace.json | 14 ++ docs/PLAN.md | 10 +- plugins/receipts/.cursor-plugin/plugin.json | 22 ++++ plugins/receipts/assets/logo.png | Bin 0 -> 3285 bytes plugins/receipts/mcp.json | 8 ++ .../receipts/skills/receipts-verify/SKILL.md | 40 ++++++ src/app/api/mcp/route.ts | 34 +++++ src/app/favicon.ico | Bin 15086 -> 0 bytes src/app/icon.png | Bin 0 -> 3285 bytes src/core/pipeline.ts | 45 ++++++- src/mcp/server.ts | 124 ++++++++++++++++++ 11 files changed, 292 insertions(+), 5 deletions(-) create mode 100644 .cursor-plugin/marketplace.json create mode 100644 plugins/receipts/.cursor-plugin/plugin.json create mode 100644 plugins/receipts/assets/logo.png create mode 100644 plugins/receipts/mcp.json create mode 100644 plugins/receipts/skills/receipts-verify/SKILL.md create mode 100644 src/app/api/mcp/route.ts delete mode 100644 src/app/favicon.ico create mode 100644 src/app/icon.png create mode 100644 src/mcp/server.ts diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json new file mode 100644 index 0000000..9e51cc8 --- /dev/null +++ b/.cursor-plugin/marketplace.json @@ -0,0 +1,14 @@ +{ + "name": "Receipts", + "owner": { + "name": "Govind Sharma", + "url": "https://github.com/CodeGovindz/receipts" + }, + "plugins": [ + { + "name": "receipts", + "source": "plugins/receipts", + "description": "Check every claim in a pull request against its real diff and CI, and check your own claims before you write them." + } + ] +} diff --git a/docs/PLAN.md b/docs/PLAN.md index a5c81a6..9c5a152 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -63,7 +63,7 @@ Overall grade: **FAIL** if any claim is CONTRADICTED; **PASS** if all are VERIFI ├─ scripts/ │ ├─ verify.ts CLI: npx tsx scripts/verify.ts [--json] │ └─ bedrock-smoke.ts calls all three models once, prints replies + latency ("npm run smoke") -├─ plugin/receipts/ Alan plugin (Cursor plugin format; see §11) +├─ .cursor-plugin/marketplace.json + plugins/receipts/ the repo is itself an Alan marketplace (see §11) │ ├─ .cursor-plugin/plugin.json │ ├─ mcp.json points to https:///api/mcp │ └─ skills/receipts-verify/SKILL.md @@ -191,9 +191,9 @@ PR text is untrusted. Wrap in clear delimiters (`` / ``), tell Format per `github.com/tryalan-ai/plugins` (README + CONTRIBUTING.md, verified): -- `plugin/receipts/.cursor-plugin/plugin.json` — fields: `name` (`receipts`), `displayName`, `version`, `description`, `author`, `homepage`, `repository`, `license: MIT`, `category`, `keywords`, `"mcpServers": "./mcp.json"`, and `"skills": "./skills/"`. -- `plugin/receipts/mcp.json` — `{ "mcpServers": { "receipts": { "type": "http", "url": "https:///api/mcp" } } }` (remote https only; no credentials needed — server holds AWS/GitHub keys). -- `plugin/receipts/skills/receipts-verify/SKILL.md` — skill behavior: after opening/updating a PR, call `verify_pr`; on FAIL fix the code or correct the description, re-run until PASS or MIXED with UNVERIFIED items listed; attach the markdown report as a test-evidence artifact / PR comment; before writing a PR description, use `check_claims` to avoid over-claiming. `name` equals the directory; `description` states when to use it. +- `plugins/receipts/.cursor-plugin/plugin.json` — fields: `name` (`receipts`), `displayName`, `version`, `description`, `author`, `homepage`, `repository`, `license: MIT`, `category`, `keywords`, `"mcpServers": "./mcp.json"`, and `"skills": "./skills/"`. +- `plugins/receipts/mcp.json` — `{ "mcpServers": { "receipts": { "type": "http", "url": "https:///api/mcp" } } }` (remote https only; no credentials needed — server holds AWS/GitHub keys). +- `plugins/receipts/skills/receipts-verify/SKILL.md` — skill behavior: after opening/updating a PR, call `verify_pr`; on FAIL fix the code or correct the description, re-run until PASS or MIXED with UNVERIFIED items listed; attach the markdown report as a test-evidence artifact / PR comment; before writing a PR description, use `check_claims` to avoid over-claiming. `name` equals the directory; `description` states when to use it. - Marketplace: P3 **prepares** the PR to `tryalan-ai/plugins` (adds `plugins/receipts/`, `marketplace.json` entry, `node scripts/validate.mjs`, `node scripts/probe-auth.mjs receipts ` expecting `no-auth`) — **but does not open it**; the owner opens it personally. - Validate the deployed server early with `@modelcontextprotocol/inspector`. @@ -269,3 +269,5 @@ Fixtures recorded from demo PRs; vitest for `signals`/`guard`/`report` (grading ## 17. Out of scope for MVP Posting comments from the web app, auth, database/persistence, private repos, GitHub Action (stretch goal only). + +**P3 notes:** the repo root is itself an Alan plugin marketplace (`.cursor-plugin/marketplace.json` + `plugins/receipts/`), so an Alan admin can add `CodeGovindz/receipts` under Settings → Plugins → Add marketplace and install Receipts directly; the same folder can be copied into a PR to `tryalan-ai/plugins`. Alan's `scripts/validate.mjs` passes on it. The MCP route is stateless (`WebStandardStreamableHTTPServerTransport`, JSON responses) with tools `verify_pr` and `check_claims`; `check_claims` swaps the PR's text for the agent's draft claims and never writes the report cache. diff --git a/plugins/receipts/.cursor-plugin/plugin.json b/plugins/receipts/.cursor-plugin/plugin.json new file mode 100644 index 0000000..ee06ffd --- /dev/null +++ b/plugins/receipts/.cursor-plugin/plugin.json @@ -0,0 +1,22 @@ +{ + "name": "receipts", + "displayName": "Receipts", + "version": "1.0.0", + "description": "Check every claim in a pull request against its real diff and CI, and check your own claims before you write them.", + "author": { + "name": "Govind Sharma" + }, + "homepage": "https://github.com/CodeGovindz/receipts", + "repository": "https://github.com/CodeGovindz/receipts", + "license": "MIT", + "category": "code-and-ci", + "keywords": [ + "pull-requests", + "code-review", + "verification", + "ci" + ], + "logo": "assets/logo.png", + "mcpServers": "./mcp.json", + "skills": "./skills/" +} diff --git a/plugins/receipts/assets/logo.png b/plugins/receipts/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c6379d046fa7fabec1ddcd6d8cc875b9460a7545 GIT binary patch literal 3285 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 AW&i*H literal 0 HcmV?d00001 diff --git a/plugins/receipts/mcp.json b/plugins/receipts/mcp.json new file mode 100644 index 0000000..8487eaa --- /dev/null +++ b/plugins/receipts/mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "receipts": { + "type": "http", + "url": "https://DEPLOYMENT-HOST/api/mcp" + } + } +} diff --git a/plugins/receipts/skills/receipts-verify/SKILL.md b/plugins/receipts/skills/receipts-verify/SKILL.md new file mode 100644 index 0000000..bde5070 --- /dev/null +++ b/plugins/receipts/skills/receipts-verify/SKILL.md @@ -0,0 +1,40 @@ +--- +name: receipts-verify +description: Check that what a pull request says it does matches its real diff and CI. Use after opening or updating a pull request, before writing or editing a PR description, and when reviewing someone else's (or another agent's) PR that claims "added tests", "all tests pass", "no breaking changes" or similar. +--- + +# Receipts: every claim, with a receipt + +The Receipts MCP server checks claims about a GitHub pull request against the PR's actual diff, changed test files and CI checks. Hosts prefix tool names (in Alan: `receipts_receipts__verify_pr` and `receipts_receipts__check_claims`); the names below are the upstream ones. + +Each claim comes back as: + +- **VERIFIED**: the diff or CI supports it, with links to the lines or checks. +- **CONTRADICTED**: the evidence shows it is false, with links. +- **UNVERIFIED**: nothing checkable supports or refutes it (for example "tests pass locally", or CI skipped the test jobs). + +The PR gets **PASS** (all verified), **FAIL** (anything contradicted) or **MIXED**. + +## Before you write a PR description + +1. Push the branch and open the PR (a short placeholder description is fine). +2. Call `check_claims` with the `pr_url` and the statements you plan to make, one per item, for example `["Adds tests for the login timeout", "All tests pass in CI", "No public API changes"]`. +3. Drop or rewrite every CONTRADICTED claim. For each UNVERIFIED one, either make it checkable (add the test, wait for CI) or say plainly in the description that it is unverified and why. +4. Write the final description from what survived. + +## After you open or update a PR + +1. Call `verify_pr` with the `pr_url`. Pass `fresh: true` after pushing new commits. +2. If the grade is **FAIL**, fix the code or correct the description, push, and run `verify_pr` again with `fresh: true`. +3. Stop when the grade is PASS, or MIXED with every UNVERIFIED item explained in the description. +4. Post the markdown from the first content block as a PR comment with your GitHub tools, and attach it to the task as testing evidence. Its first line is the marker ``; if an earlier Receipts comment exists, update it instead of adding another. + +## Reviewing someone else's PR + +Run `verify_pr` and lead your review with the CONTRADICTED items, then the UNVERIFIED ones. Quote the evidence links instead of re-deriving them. + +## Rules + +- Never claim a check passed because Receipts said UNVERIFIED. Unverified means nobody has shown it yet. +- A run takes 10 to 60 seconds. Don't call it in a loop without pushing changes in between. +- Only public GitHub repositories are supported. If the tool errors (rate limit, private repo), say so instead of guessing the verdicts. diff --git a/src/app/api/mcp/route.ts b/src/app/api/mcp/route.ts new file mode 100644 index 0000000..80fa270 --- /dev/null +++ b/src/app/api/mcp/route.ts @@ -0,0 +1,34 @@ +/** + * Remote MCP server (Streamable HTTP, stateless): a fresh server and + * transport per request, so it works on serverless functions. JSON responses + * instead of SSE, since every tool call finishes in one response. + */ +import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"; +import { clientKey } from "@/lib/ratelimit"; +import { createReceiptsMcpServer } from "@/mcp/server"; + +export const maxDuration = 300; + +export async function POST(request: Request): Promise { + const server = createReceiptsMcpServer(clientKey(request.headers)); + const transport = new WebStandardStreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + enableJsonResponse: true, + }); + await server.connect(transport); + try { + return await transport.handleRequest(request); + } finally { + await server.close(); + } +} + +function methodNotAllowed(): Response { + return Response.json( + { jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed: this MCP server is stateless, use POST." }, id: null }, + { status: 405, headers: { Allow: "POST" } }, + ); +} + +export const GET = methodNotAllowed; +export const DELETE = methodNotAllowed; diff --git a/src/app/favicon.ico b/src/app/favicon.ico deleted file mode 100644 index af98450595e8b8efd9e505cddc5ed705b665a4d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15086 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 AW&i*H literal 0 HcmV?d00001 diff --git a/src/core/pipeline.ts b/src/core/pipeline.ts index 953469a..8525c4f 100644 --- a/src/core/pipeline.ts +++ b/src/core/pipeline.ts @@ -44,6 +44,8 @@ export interface RunOnBundleOptions { config: ReceiptsConfig; onEvent?: (event: PipelineEvent) => void; fresh?: boolean; + /** Store the report in the cache (default true; false for draft checks, whose bundle isn't the real PR). */ + cache?: boolean; /** Time the GitHub fetch took, for honest timings on live runs (default 0). */ fetchMs?: number; } @@ -152,7 +154,7 @@ export async function runReceiptsOnBundle( verifyAttempts: verification.attempts, }, }); - reportCache.set(key, report); + if (options.cache !== false) reportCache.set(key, report); emit({ type: "done", report }); return report; } catch (error) { @@ -196,3 +198,44 @@ export async function runReceipts(prUrl: string, options: RunReceiptsOptions = { throw error; } } + +/** Max draft claims check_claims accepts in one call. */ +export const MAX_DRAFT_CLAIMS = 12; + +/** + * Checks claims an agent is ABOUT to make (e.g. a draft PR description) + * against the PR's real diff and CI, before it writes them. The drafts + * replace the PR's own title, body and commits as the extraction source, so + * the quote guard validates against exactly what the agent submitted. + * Never cached: the same head SHA answers different drafts. + */ +export async function runClaimCheck( + prUrl: string, + claims: string[], + options: Omit = {}, +): Promise { + loadDotEnv(); + const config = options.config ?? getConfig(); + const drafts = claims.map((claim) => claim.trim()).filter((claim) => claim !== ""); + if (drafts.length === 0) throw new Error("Provide at least one claim to check."); + if (drafts.length > MAX_DRAFT_CLAIMS) { + throw new Error(`Check at most ${MAX_DRAFT_CLAIMS} claims per call (got ${drafts.length}).`); + } + + const fetchStarted = Date.now(); + options.onEvent?.({ type: "fetching" }); + const bundle = await fetchPrBundle(prUrl, { githubToken: config.githubToken }); + const draftBundle: PrBundle = { + ...bundle, + title: "", + body: drafts.map((claim) => `- ${claim}`).join("\n"), + commits: [], + }; + return runReceiptsOnBundle(draftBundle, { + config, + onEvent: options.onEvent, + fresh: true, + cache: false, + fetchMs: Date.now() - fetchStarted, + }); +} diff --git a/src/mcp/server.ts b/src/mcp/server.ts new file mode 100644 index 0000000..d71cb3d --- /dev/null +++ b/src/mcp/server.ts @@ -0,0 +1,124 @@ +/** + * Receipts as an MCP server (plan §10/§11). Two read-only tools: + * - verify_pr: check every claim in an existing PR. + * - check_claims: check claims an agent is about to write, before it writes them. + * Each result carries the GitHub-comment-ready markdown plus a compact JSON + * summary. The server has no GitHub write access: agents post the markdown + * with their own GitHub tools. + */ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { MAX_DRAFT_CLAIMS, runClaimCheck, runReceipts } from "@/core/pipeline"; +import { toMarkdown } from "@/core/report"; +import type { Report } from "@/core/types"; +import { takeRun } from "@/lib/ratelimit"; + +type ToolResult = { + content: Array<{ type: "text"; text: string }>; + isError?: boolean; +}; + +/** The fields an agent acts on, without the full report's timings and quotes. */ +function summarize(report: Report) { + return { + prUrl: report.prUrl, + headSha: report.headSha, + grade: report.grade, + trustScore: report.trustScore, + claims: report.claims.map((result) => ({ + claim: result.claim.text, + verdict: result.verdict, + reasoning: result.reasoning, + evidence: result.evidence.map((item) => item.url).filter((url): url is string => url !== null), + })), + signals: report.signals, + }; +} + +function success(report: Report): ToolResult { + return { + content: [ + { type: "text", text: toMarkdown(report) }, + { type: "text", text: JSON.stringify(summarize(report)) }, + ], + }; +} + +function failure(error: unknown): ToolResult { + return { + content: [{ type: "text", text: `Receipts failed: ${error instanceof Error ? error.message : String(error)}` }], + isError: true, + }; +} + +/** One server per request (stateless transport); `clientKey` rate-limits tool calls. */ +export function createReceiptsMcpServer(clientKey: string): McpServer { + const server = new McpServer({ name: "receipts", version: "1.0.0" }); + + const limited = (): ToolResult | null => { + const limit = takeRun(clientKey); + if (limit.ok) return null; + return failure( + new Error(`rate limit reached (10 runs per hour); retry in ${Math.ceil(limit.retryAfterSec / 60)} min.`), + ); + }; + + server.registerTool( + "verify_pr", + { + title: "Verify a pull request's claims", + description: + "Extracts every checkable claim from a GitHub pull request's title, description and commits " + + "(\"added tests\", \"all tests pass\", \"no breaking changes\"…) and checks each one against the " + + "real diff and CI. Each claim gets VERIFIED, CONTRADICTED or UNVERIFIED with evidence links; the " + + "PR gets PASS (all verified), FAIL (any contradicted) or MIXED. Takes 10-60 s. Returns a " + + "markdown report ready to post as a PR comment, then a JSON summary.", + inputSchema: { + pr_url: z.string().describe("Public GitHub PR URL, e.g. https://github.com/owner/repo/pull/123"), + fresh: z.boolean().optional().describe("Skip the cache and re-check (e.g. after pushing fixes)."), + }, + annotations: { readOnlyHint: true, openWorldHint: true }, + }, + async ({ pr_url, fresh }) => { + const blocked = limited(); + if (blocked) return blocked; + try { + return success(await runReceipts(pr_url, { fresh })); + } catch (error) { + return failure(error); + } + }, + ); + + server.registerTool( + "check_claims", + { + title: "Check claims before writing them", + description: + "Checks claims you are ABOUT to make about a pull request (for example the bullet points of a " + + "PR description you are drafting) against its real diff and CI, before you post them. Use it " + + "to avoid over-claiming: rewrite or drop anything CONTRADICTED, and say plainly what stays " + + "UNVERIFIED. The PR must already exist (push the branch and open it first).", + inputSchema: { + pr_url: z.string().describe("Public GitHub PR URL the claims are about."), + claims: z + .array(z.string()) + .min(1) + .max(MAX_DRAFT_CLAIMS) + .describe(`1-${MAX_DRAFT_CLAIMS} claims, one statement each, e.g. "Adds tests for the login timeout".`), + }, + annotations: { readOnlyHint: true, openWorldHint: true }, + }, + async ({ pr_url, claims }) => { + const blocked = limited(); + if (blocked) return blocked; + try { + return success(await runClaimCheck(pr_url, claims)); + } catch (error) { + return failure(error); + } + }, + ); + + return server; +}