From 951ddd48643a73e231a0c738b51ffb198bd984c9 Mon Sep 17 00:00:00 2001 From: Ilyaas Kapadia <86218345+IlyaasK@users.noreply.github.com> Date: Fri, 7 Aug 2026 16:06:13 -0400 Subject: [PATCH 1/2] Check MCP documentation schema parity --- .github/workflows/ci.yml | 14 +++ scripts/check-doc-schema-parity.test.ts | 60 +++++++++++ scripts/check-doc-schema-parity.ts | 126 ++++++++++++++++++++++++ 3 files changed, 200 insertions(+) create mode 100644 scripts/check-doc-schema-parity.test.ts create mode 100644 scripts/check-doc-schema-parity.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index acfcb0b6..bb8f8a8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,3 +28,17 @@ jobs: - name: Test run: bun test + + - name: Check out Kernel docs + uses: actions/checkout@v4 + with: + repository: kernel/docs + path: kernel-docs + sparse-checkout: | + reference/mcp-server/tools/manage-browser-pools.mdx + reference/mcp-server/tools/manage-profiles.mdx + reference/mcp-server/tools/manage-proxies.mdx + sparse-checkout-cone-mode: false + + - name: Check MCP documentation schema parity + run: bun scripts/check-doc-schema-parity.ts "$GITHUB_WORKSPACE/kernel-docs" diff --git a/scripts/check-doc-schema-parity.test.ts b/scripts/check-doc-schema-parity.test.ts new file mode 100644 index 00000000..112286f2 --- /dev/null +++ b/scripts/check-doc-schema-parity.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, test } from "bun:test"; +import { parityError, parseParameterNames } from "./check-doc-schema-parity"; + +describe("parseParameterNames", () => { + test("reads individual and grouped parameter cells", () => { + const markdown = ` +## Parameters + +| Parameter | Description | +| --- | --- | +| \`action\` | Required. | +| \`profile_id\` or \`profile_name\` | Choose one. | + +## Examples +`; + + expect([...parseParameterNames(markdown)].sort()).toEqual([ + "action", + "profile_id", + "profile_name", + ]); + }); + + test("rejects a missing parameter table", () => { + expect(() => parseParameterNames("## Examples\n")).toThrow( + "missing Parameters section", + ); + }); + + test("reads a parameter table at the end of a document", () => { + expect([ + ...parseParameterNames(`## Parameters + +| Parameter | Description | +| --- | --- | +| \`action\` | Required. | +`), + ]).toEqual(["action"]); + }); +}); + +describe("parityError", () => { + test("accepts equal parameter sets", () => { + expect( + parityError("manage_profiles", new Set(["action"]), new Set(["action"])), + ).toBeUndefined(); + }); + + test("reports undocumented and stale parameters", () => { + expect( + parityError( + "manage_profiles", + new Set(["action", "query"]), + new Set(["action", "old_query"]), + ), + ).toBe( + "manage_profiles: missing from docs: query; not in schema: old_query", + ); + }); +}); diff --git a/scripts/check-doc-schema-parity.ts b/scripts/check-doc-schema-parity.ts new file mode 100644 index 00000000..bbb9f8b9 --- /dev/null +++ b/scripts/check-doc-schema-parity.ts @@ -0,0 +1,126 @@ +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { registerBrowserPoolCapabilities } from "@/lib/mcp/tools/browser-pools"; +import { registerProfileCapabilities } from "@/lib/mcp/tools/profiles"; +import { registerProxyTools } from "@/lib/mcp/tools/proxies"; + +const documentedTools = { + manage_browser_pools: "reference/mcp-server/tools/manage-browser-pools.mdx", + manage_profiles: "reference/mcp-server/tools/manage-profiles.mdx", + manage_proxies: "reference/mcp-server/tools/manage-proxies.mdx", +} as const; + +export function parseParameterNames(markdown: string): Set { + const heading = markdown.match(/^## Parameters\s*$/m); + if (!heading || heading.index === undefined) { + throw new Error("missing Parameters section"); + } + const parameters = markdown + .slice(heading.index + heading[0].length) + .split(/^##\s/m, 1)[0]; + + const names = new Set(); + for (const line of parameters.split("\n")) { + const firstCell = line.match(/^\|\s*(.*?)\s*\|/)?.[1]; + if (!firstCell) continue; + + for (const match of firstCell.matchAll(/`([a-z][a-z0-9_]*)`/g)) { + names.add(match[1]); + } + } + if (names.size === 0) { + throw new Error("Parameters table contains no parameter names"); + } + return names; +} + +export function parityError( + toolName: string, + schemaNames: Set, + documentedNames: Set, +): string | undefined { + const missing = [...schemaNames].filter((name) => !documentedNames.has(name)); + const stale = [...documentedNames].filter((name) => !schemaNames.has(name)); + if (missing.length === 0 && stale.length === 0) return undefined; + + const details = []; + if (missing.length > 0) + details.push(`missing from docs: ${missing.sort().join(", ")}`); + if (stale.length > 0) + details.push(`not in schema: ${stale.sort().join(", ")}`); + return `${toolName}: ${details.join("; ")}`; +} + +async function listDurableToolParameters(): Promise>> { + const server = new McpServer({ name: "schema-parity", version: "0.0.0" }); + registerBrowserPoolCapabilities(server); + registerProfileCapabilities(server); + registerProxyTools(server); + + const client = new Client({ name: "schema-parity", version: "0.0.0" }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + await Promise.all([ + server.connect(serverTransport), + client.connect(clientTransport), + ]); + + try { + const { tools } = await client.listTools(); + return new Map( + tools.map((tool) => [ + tool.name, + new Set(Object.keys(tool.inputSchema.properties ?? {})), + ]), + ); + } finally { + await Promise.all([client.close(), server.close()]); + } +} + +export async function checkDocSchemaParity(docsRoot: string): Promise { + const schemas = await listDurableToolParameters(); + const errors: string[] = []; + + for (const [toolName, relativePath] of Object.entries(documentedTools)) { + const schemaNames = schemas.get(toolName); + if (!schemaNames) { + errors.push(`${toolName}: tool was not registered`); + continue; + } + + const markdown = await readFile(join(docsRoot, relativePath), "utf8"); + try { + const error = parityError( + toolName, + schemaNames, + parseParameterNames(markdown), + ); + if (error) errors.push(error); + } catch (error) { + errors.push( + `${toolName}: ${error instanceof Error ? error.message : error}`, + ); + } + } + + if (errors.length > 0) { + throw new Error(`MCP documentation schema drift:\n${errors.join("\n")}`); + } +} + +if (import.meta.main) { + const docsRoot = process.argv[2]; + if (!docsRoot) { + throw new Error( + "usage: bun scripts/check-doc-schema-parity.ts ", + ); + } + await checkDocSchemaParity(docsRoot); + console.log( + "MCP documentation parameter tables match the registered schemas.", + ); +} From c5423c526ba89bcb7c382b549ff5d8c868c7944a Mon Sep 17 00:00:00 2001 From: Ilyaas Kapadia <86218345+IlyaasK@users.noreply.github.com> Date: Wed, 12 Aug 2026 18:19:49 -0400 Subject: [PATCH 2/2] Clarify documentation parity scope --- .github/workflows/ci.yml | 5 ++-- ...c-top-level-parameter-name-parity.test.ts} | 5 +++- ...ck-doc-top-level-parameter-name-parity.ts} | 25 +++++++++++++------ 3 files changed, 25 insertions(+), 10 deletions(-) rename scripts/{check-doc-schema-parity.test.ts => check-doc-top-level-parameter-name-parity.test.ts} (93%) rename scripts/{check-doc-schema-parity.ts => check-doc-top-level-parameter-name-parity.ts} (83%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb8f8a8a..5cfa6ca5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,7 @@ jobs: run: bun test - name: Check out Kernel docs + # Track docs main deliberately so either repository can detect cross-repo drift. uses: actions/checkout@v4 with: repository: kernel/docs @@ -40,5 +41,5 @@ jobs: reference/mcp-server/tools/manage-proxies.mdx sparse-checkout-cone-mode: false - - name: Check MCP documentation schema parity - run: bun scripts/check-doc-schema-parity.ts "$GITHUB_WORKSPACE/kernel-docs" + - name: Check MCP documentation top-level parameter-name parity + run: bun scripts/check-doc-top-level-parameter-name-parity.ts "$GITHUB_WORKSPACE/kernel-docs" diff --git a/scripts/check-doc-schema-parity.test.ts b/scripts/check-doc-top-level-parameter-name-parity.test.ts similarity index 93% rename from scripts/check-doc-schema-parity.test.ts rename to scripts/check-doc-top-level-parameter-name-parity.test.ts index 112286f2..7fb2c29f 100644 --- a/scripts/check-doc-schema-parity.test.ts +++ b/scripts/check-doc-top-level-parameter-name-parity.test.ts @@ -1,5 +1,8 @@ import { describe, expect, test } from "bun:test"; -import { parityError, parseParameterNames } from "./check-doc-schema-parity"; +import { + parityError, + parseParameterNames, +} from "./check-doc-top-level-parameter-name-parity"; describe("parseParameterNames", () => { test("reads individual and grouped parameter cells", () => { diff --git a/scripts/check-doc-schema-parity.ts b/scripts/check-doc-top-level-parameter-name-parity.ts similarity index 83% rename from scripts/check-doc-schema-parity.ts rename to scripts/check-doc-top-level-parameter-name-parity.ts index bbb9f8b9..53791b02 100644 --- a/scripts/check-doc-schema-parity.ts +++ b/scripts/check-doc-top-level-parameter-name-parity.ts @@ -7,6 +7,7 @@ import { registerBrowserPoolCapabilities } from "@/lib/mcp/tools/browser-pools"; import { registerProfileCapabilities } from "@/lib/mcp/tools/profiles"; import { registerProxyTools } from "@/lib/mcp/tools/proxies"; +// Add every newly documented durable tool here so CI includes its parameter table. const documentedTools = { manage_browser_pools: "reference/mcp-server/tools/manage-browser-pools.mdx", manage_profiles: "reference/mcp-server/tools/manage-profiles.mdx", @@ -55,12 +56,18 @@ export function parityError( } async function listDurableToolParameters(): Promise>> { - const server = new McpServer({ name: "schema-parity", version: "0.0.0" }); + const server = new McpServer({ + name: "top-level-parameter-name-parity", + version: "0.0.0", + }); registerBrowserPoolCapabilities(server); registerProfileCapabilities(server); registerProxyTools(server); - const client = new Client({ name: "schema-parity", version: "0.0.0" }); + const client = new Client({ + name: "top-level-parameter-name-parity", + version: "0.0.0", + }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ @@ -81,7 +88,9 @@ async function listDurableToolParameters(): Promise>> { } } -export async function checkDocSchemaParity(docsRoot: string): Promise { +export async function checkDocTopLevelParameterNameParity( + docsRoot: string, +): Promise { const schemas = await listDurableToolParameters(); const errors: string[] = []; @@ -108,7 +117,9 @@ export async function checkDocSchemaParity(docsRoot: string): Promise { } if (errors.length > 0) { - throw new Error(`MCP documentation schema drift:\n${errors.join("\n")}`); + throw new Error( + `MCP documentation top-level parameter-name drift:\n${errors.join("\n")}`, + ); } } @@ -116,11 +127,11 @@ if (import.meta.main) { const docsRoot = process.argv[2]; if (!docsRoot) { throw new Error( - "usage: bun scripts/check-doc-schema-parity.ts ", + "usage: bun scripts/check-doc-top-level-parameter-name-parity.ts ", ); } - await checkDocSchemaParity(docsRoot); + await checkDocTopLevelParameterNameParity(docsRoot); console.log( - "MCP documentation parameter tables match the registered schemas.", + "MCP documentation top-level parameter names match the registered schemas.", ); }