@polygraphso/mcp
Advanced tools
| /** | ||
| * `request_grade` — add an ungraded MCP server to polygraph's public grading | ||
| * queue. The natural follow-up when `check_server` returns not_available. | ||
| * | ||
| * Tool description is LLM-facing — the agent reads it to decide when to | ||
| * invoke. Voice rules from brand-foundation.md apply: plain English, no | ||
| * empty intensifiers, no "AI safety" framing, no overclaim. | ||
| * | ||
| * No email: the caller is an agent, so we take no contact details. The MCP | ||
| * server passes the connected client's identity (agent_id) so the queue | ||
| * knows who asked, without prompting the user. The grade is fulfilled | ||
| * best-effort; read it later by calling `check_server` again. | ||
| */ | ||
| import { z } from "zod"; | ||
| import { PolygraphApiError, postGradeRequest } from "../api.js"; | ||
| export const REQUEST_TOOL_NAME = "request_grade"; | ||
| export const REQUEST_TOOL_TITLE = "Request a polygraph grade for an MCP server"; | ||
| export const REQUEST_TOOL_DESCRIPTION = [ | ||
| "Add an MCP server to polygraph.so's public grading queue.", | ||
| "", | ||
| "Use this when `check_server` returns not_available and you want the server", | ||
| "graded. The request is free and best-effort — polygraph runs the litmus", | ||
| "test and publishes the grade, then you read it by calling `check_server`", | ||
| "again later. This does not return a grade synchronously.", | ||
| "", | ||
| "No contact details are needed. Requesting the same server twice is a no-op,", | ||
| "not a duplicate.", | ||
| "", | ||
| "Input: `server_ref` — the same registry-prefixed identifier `check_server`", | ||
| "takes (e.g. npm/@scope/name, pypi/name, github/owner/repo).", | ||
| "", | ||
| "Returns `{ status: 'queued', created, demand }` — `created` is false if it", | ||
| "was already queued, `demand` is how many requests stand behind it.", | ||
| ].join("\n"); | ||
| export const requestInputShape = { | ||
| server_ref: z | ||
| .string() | ||
| .min(1) | ||
| .max(512) | ||
| .describe("Registry-prefixed server identifier, e.g. 'npm/@modelcontextprotocol/server-filesystem', 'pypi/mcp-server-git', or 'github/anthropic/mcp-server-foo'."), | ||
| }; | ||
| const requestInputSchema = z.object(requestInputShape); | ||
| /** | ||
| * @param agentId identity of the connected MCP client (name/version), passed | ||
| * through so the queue can attribute the request. Undefined when the client | ||
| * didn't announce itself. | ||
| */ | ||
| export async function handleRequestGrade(input, agentId) { | ||
| try { | ||
| const body = await postGradeRequest(input.server_ref, agentId); | ||
| const text = body.created | ||
| ? `Queued ${input.server_ref} for grading (${body.demand} request(s) behind it). It'll be graded best-effort — call check_server again later to read the result.` | ||
| : `${input.server_ref} was already in the queue (${body.demand} request(s) behind it). Call check_server again later to read the result.`; | ||
| return { | ||
| content: [{ type: "text", text }], | ||
| structuredContent: body, | ||
| }; | ||
| } | ||
| catch (err) { | ||
| return errorResult(err); | ||
| } | ||
| } | ||
| function errorResult(err) { | ||
| if (err instanceof PolygraphApiError) { | ||
| return { | ||
| content: [{ type: "text", text: err.message }], | ||
| isError: true, | ||
| }; | ||
| } | ||
| const msg = err instanceof Error ? err.message : String(err); | ||
| return { | ||
| content: [{ type: "text", text: `polygraph.so request failed: ${msg}` }], | ||
| isError: true, | ||
| }; | ||
| } |
+39
-2
@@ -9,4 +9,5 @@ /** | ||
| * Endpoints (already shipped; see web/app/api/cli/): | ||
| * POST /api/cli/check → { server_ref } → graded | not_available | ||
| * GET /api/cli/list → { servers, total } | ||
| * POST /api/cli/check → { server_ref } → graded | not_available | ||
| * GET /api/cli/list → { servers, total } | ||
| * POST /api/cli/grade-request → { server_ref, source, agent_id? } → queued | ||
| * | ||
@@ -73,2 +74,38 @@ * Network failures throw `PolygraphApiError` with a stable `kind` so the | ||
| } | ||
| export async function postGradeRequest(serverRef, agentId) { | ||
| const url = `${apiBaseUrl()}/api/cli/grade-request`; | ||
| const body = { | ||
| server_ref: serverRef, | ||
| source: "mcp", | ||
| }; | ||
| if (agentId) | ||
| body.agent_id = agentId; | ||
| let res; | ||
| try { | ||
| res = await fetch(url, { | ||
| method: "POST", | ||
| headers: { "content-type": "application/json" }, | ||
| body: JSON.stringify(body), | ||
| }); | ||
| } | ||
| catch (err) { | ||
| const msg = err instanceof Error ? err.message : String(err); | ||
| throw new PolygraphApiError("network", `couldn't reach polygraph.so (${msg}).`); | ||
| } | ||
| // 400 → the ref was rejected; propagate the server's message. | ||
| if (res.status === 400) { | ||
| let errBody; | ||
| try { | ||
| errBody = (await res.json()); | ||
| } | ||
| catch { | ||
| errBody = {}; | ||
| } | ||
| throw new PolygraphApiError("http", errBody.error ?? "polygraph.so rejected the server_ref as malformed.", 400); | ||
| } | ||
| if (!res.ok) { | ||
| throw new PolygraphApiError("http", `polygraph.so returned ${res.status}.`, res.status); | ||
| } | ||
| return readJson(res); | ||
| } | ||
| export async function getList() { | ||
@@ -75,0 +112,0 @@ const url = `${apiBaseUrl()}/api/cli/list`; |
+32
-5
@@ -10,8 +10,11 @@ #!/usr/bin/env node | ||
| * | ||
| * Tools registered for v0.1.0: | ||
| * - check_server → POST /api/cli/check | ||
| * - list_servers → GET /api/cli/list | ||
| * Tools registered: | ||
| * - check_server → POST /api/cli/check (read) | ||
| * - list_servers → GET /api/cli/list (read) | ||
| * - request_grade → POST /api/cli/grade-request (write: queue an ungraded | ||
| * server; the natural follow-up to a not_available check) | ||
| * | ||
| * `notify_about` is deferred to v0.2 because POST /api/notify hasn't shipped | ||
| * yet — see packages/mcp/README.md "Roadmap". | ||
| * Email notification stays a web-only funnel (/notify) — an agent has no | ||
| * inbox, so request_grade takes no contact details and instead attributes | ||
| * the request to the connected client (agent_id). | ||
| */ | ||
@@ -25,2 +28,14 @@ import { readFileSync } from "node:fs"; | ||
| import { LIST_TOOL_DESCRIPTION, LIST_TOOL_NAME, LIST_TOOL_TITLE, handleList, } from "./tools/list.js"; | ||
| import { REQUEST_TOOL_DESCRIPTION, REQUEST_TOOL_NAME, REQUEST_TOOL_TITLE, handleRequestGrade, requestInputShape, } from "./tools/request.js"; | ||
| /** | ||
| * Identity of the connected MCP client (the "agent"), from the initialize | ||
| * handshake — e.g. "claude-ai/1.2.0". Undefined before initialize or when the | ||
| * client didn't announce itself. Used to attribute grade requests. | ||
| */ | ||
| function clientAgentId(server) { | ||
| const client = server.server.getClientVersion(); | ||
| if (!client?.name) | ||
| return undefined; | ||
| return client.version ? `${client.name}/${client.version}` : client.name; | ||
| } | ||
| function readVersion() { | ||
@@ -68,2 +83,14 @@ const here = dirname(fileURLToPath(import.meta.url)); | ||
| }, handleList); | ||
| server.registerTool(REQUEST_TOOL_NAME, { | ||
| title: REQUEST_TOOL_TITLE, | ||
| description: REQUEST_TOOL_DESCRIPTION, | ||
| inputSchema: requestInputShape, | ||
| annotations: { | ||
| title: REQUEST_TOOL_TITLE, | ||
| readOnlyHint: false, | ||
| destructiveHint: false, | ||
| idempotentHint: true, | ||
| openWorldHint: true, | ||
| }, | ||
| }, (args) => handleRequestGrade(args, clientAgentId(server))); | ||
| return server; | ||
@@ -70,0 +97,0 @@ } |
+11
-8
@@ -28,12 +28,15 @@ /** | ||
| " github/<owner>/<repo> (e.g. github/anthropic/mcp-server-foo)", | ||
| "An optional `@<version>` suffix is accepted but ignored — the polygraph is", | ||
| "looked up by the versionless server identity.", | ||
| "An optional `@<version>` suffix looks up the grade for that EXACT version; a", | ||
| "bare ref returns the latest graded version. A grade for one version never", | ||
| "applies to another.", | ||
| "", | ||
| "Returns one of:", | ||
| " - graded: `polygraph` is the published grade ('A'|'B'|'D'|'F', no C), and", | ||
| " `polygraph_detail` carries the per-check results (C-01/C-02/C-03),", | ||
| " tool-surface fingerprint, and methodology version.", | ||
| " - not_available: this server hasn't been graded yet. The response includes", | ||
| " a notify URL the user can subscribe to. Treat this as 'no data' —", | ||
| " neither safe nor unsafe.", | ||
| " `polygraph_detail` carries the per-check results (C-01/C-02/C-03), the", | ||
| " tool-surface fingerprint, the methodology version, and `resolved_version`", | ||
| " (the version the grade was run against).", | ||
| " - not_available: this server (or the requested version) hasn't been graded", | ||
| " yet. Treat this as 'no data' — neither safe nor unsafe. The response's", | ||
| " `message` explains next steps: call `request_grade` to add it to the", | ||
| " public grading queue, or run the `self_grade` command to grade it now.", | ||
| ].join("\n"); | ||
@@ -45,3 +48,3 @@ export const checkInputShape = { | ||
| .max(512) | ||
| .describe("Registry-prefixed server identifier, e.g. 'npm/@modelcontextprotocol/server-filesystem', 'pypi/mcp-server-git', or 'github/anthropic/mcp-server-foo'. Optional '@<version>' suffix is accepted but ignored."), | ||
| .describe("Registry-prefixed server identifier, e.g. 'npm/@modelcontextprotocol/server-filesystem', 'pypi/mcp-server-git', or 'github/anthropic/mcp-server-foo'. An optional '@<version>' suffix looks up that exact version; a bare ref returns the latest graded version."), | ||
| }; | ||
@@ -48,0 +51,0 @@ const checkInputSchema = z.object(checkInputShape); |
+8
-9
| { | ||
| "name": "@polygraphso/mcp", | ||
| "version": "0.2.0", | ||
| "version": "0.3.0", | ||
| "description": "MCP server for polygraph.so — independent, lab-evaluated trust grades for MCP servers. Lets AI agents check the polygraph of a server before recommending or installing it.", | ||
@@ -30,9 +30,2 @@ "license": "Apache-2.0", | ||
| ], | ||
| "scripts": { | ||
| "build": "tsc", | ||
| "prepublishOnly": "npm run build", | ||
| "typecheck": "tsc --noEmit", | ||
| "test": "vitest run", | ||
| "start": "node dist/index.js" | ||
| }, | ||
| "engines": { | ||
@@ -52,3 +45,9 @@ "node": ">=18" | ||
| "access": "public" | ||
| }, | ||
| "scripts": { | ||
| "build": "tsc", | ||
| "typecheck": "tsc --noEmit", | ||
| "test": "vitest run", | ||
| "start": "node dist/index.js" | ||
| } | ||
| } | ||
| } |
+28
-16
@@ -5,11 +5,10 @@ # @polygraphso/mcp | ||
| A polygraph is the result polygraph.so issues for an MCP server: an adoption tier (Top 10 / 25 / 50 / 100), and — once behavioral evaluation has run — an A–F grade plus an evidence URL. This package lets an agent check the polygraph for a server before recommending or installing it. | ||
| A polygraph is the behavioral trust grade polygraph.so issues for an MCP server: a letter grade (A/B/D/F) from an adversarial litmus test, backed by per-check results, a tool-surface fingerprint, and an evidence URL anyone can re-run. This package lets an agent check the polygraph for a server before recommending or installing it. | ||
| ## Tools | ||
| - **`check_server`** — look up the polygraph for a specific MCP server (`server_ref` like `npm/@modelcontextprotocol/server-filesystem`). | ||
| - **`list_servers`** — enumerate every server polygraph tracks, tier-sorted. | ||
| - **`check_server`** — look up the published polygraph for a specific MCP server (`server_ref` like `npm/@modelcontextprotocol/server-filesystem`). An optional `@<version>` suffix looks up that exact version. | ||
| - **`list_servers`** — enumerate every server polygraph has graded, sorted by grade (A first). | ||
| - **`request_grade`** — add an ungraded server to polygraph's public grading queue. The natural follow-up when `check_server` returns `not_available`. Free and best-effort: polygraph runs the litmus test and publishes the grade, which you read later by calling `check_server` again. No contact details are asked of the agent. | ||
| `notify_about` (request a polygraph for an untracked server) lands in **v0.2** — the underlying endpoint isn't published yet. | ||
| ## Install in Claude Desktop | ||
@@ -70,18 +69,32 @@ | ||
| `check_server({ server_ref: "npm/lodash" })`: | ||
| `check_server({ server_ref: "npm/@modelcontextprotocol/server-filesystem" })` — a graded server: | ||
| ```json | ||
| { | ||
| "status": "tracked", | ||
| "adoption_tier": "top10", | ||
| "polygraph": null, | ||
| "notify_url": "https://polygraph.so/notify?for=npm/lodash" | ||
| "status": "graded", | ||
| "polygraph": "A", | ||
| "polygraph_detail": { | ||
| "methodology_version": "litmus-v11", | ||
| "resolved_version": "2.1.0", | ||
| "evidence_url": "https://polygraph.so/mcp/npm/@modelcontextprotocol/server-filesystem" | ||
| } | ||
| } | ||
| ``` | ||
| - `status` is `"tracked"` if polygraph is evaluating the server, `"not_available"` otherwise. | ||
| - `adoption_tier` is `top10` / `top25` / `top50` / `top100`, or `null` if the server is tracked but unranked. | ||
| - `polygraph` is `null` until the behavioral evaluation lands (v0 has the adoption side; the litmus harness is shipping next). | ||
| - `notify_url` is where a user can subscribe to be notified when the polygraph is published. | ||
| An ungraded server: | ||
| ```json | ||
| { | ||
| "status": "not_available", | ||
| "notify_url": "https://polygraph.so/notify?for=npm/obscure-mcp-server", | ||
| "message": "No published polygraph for npm/obscure-mcp-server yet — treat it as unevaluated. Call request_grade to add it to the public queue, or grade it yourself with the self_grade command.", | ||
| "self_grade": "npx -y -p @polygraphso/litmus polygraphso-litmus litmus npm/obscure-mcp-server" | ||
| } | ||
| ``` | ||
| - `status` is `"graded"` when a published grade exists, `"not_available"` otherwise. | ||
| - `polygraph` is the published grade — `"A" | "B" | "D" | "F"` (no C). | ||
| - `polygraph_detail` carries the per-check results (C-01/C-02/C-03), the tool-surface fingerprint, the methodology version, and `resolved_version` (the version the grade was run against). | ||
| - On `not_available`: `message` explains the next steps, `self_grade` is a one-shot command to grade the server yourself, and `notify_url` is where a user can subscribe to be notified when the polygraph is published. Call `request_grade` to add the server to the public queue. | ||
| `list_servers()`: | ||
@@ -94,4 +107,3 @@ | ||
| "server_ref": "npm/@modelcontextprotocol/server-filesystem", | ||
| "adoption_tier": "top10", | ||
| "polygraph": null | ||
| "polygraph": "A" | ||
| } | ||
@@ -98,0 +110,0 @@ ], |
33769
27.39%8
14.29%436
48.3%132
10%3
50%