From bb98d9c55c53b9f226288ef965340e85acf52999 Mon Sep 17 00:00:00 2001 From: "mintlify[bot]" <109931778+mintlify[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 02:32:30 +0000 Subject: [PATCH 1/6] Updated mintlify pages - Updated mcp/setup.mdx - Updated snippets/connect-hosts.mdx - Updated index.mdx - Updated agents/import.mdx - Updated docs.json Mintlify-Source: dashboard-editor --- agents/import.mdx | 4 + docs.json | 9 +- index.mdx | 10 +- mcp/setup.mdx | 113 +++++----------------- snippets/connect-hosts.mdx | 190 +++++++++---------------------------- 5 files changed, 82 insertions(+), 244 deletions(-) diff --git a/agents/import.mdx b/agents/import.mdx index a9352f5..0d5d541 100644 --- a/agents/import.mdx +++ b/agents/import.mdx @@ -7,6 +7,10 @@ icon: file-import Drop a package, pick a GitHub repo, or paste a card or runtime link. Proofable inspects the source, registers identity on your profile, and asks you to reconnect apps. It does not copy secrets or run the package. + +Help me bring my existing agent into Proofable. If this tool is not connected, follow https://docs.proofable.me/mcp/setup first. Ask for the repository, package, or runtime link, inspect it using the existing import flow, and let me review before registering it. Keep credentials out. Reuse existing connections and reconnect apps only with my approval. Then help me complete one useful task. + + Supported inputs include OpenClaw workspace files, Hermes config and skills, Agent Skills `SKILL.md` folders, A2A Agent Cards, MCP URLs, GitHub repositories, and zip or tar packages. ## From the product diff --git a/docs.json b/docs.json index 0f2f007..0ca6635 100644 --- a/docs.json +++ b/docs.json @@ -564,10 +564,6 @@ "source": "/cookbook/server-advanced", "destination": "/integrations/server" }, - { - "source": "/install", - "destination": "https://proofable.me/install" - }, { "source": "/mcp/ide-plugin", "destination": "/mcp/setup" @@ -711,6 +707,11 @@ { "source": "/sdks/security", "destination": "/security" + }, + { + "source": "/install", + "destination": "/mcp/setup", + "permanent": true } ], "contextual": { diff --git a/index.mdx b/index.mdx index 68127b7..2b4a71c 100644 --- a/index.mdx +++ b/index.mdx @@ -9,17 +9,17 @@ icon: book "twitter:image": "/images/og-default.png" --- -Choose the path that matches what you are building. +Use Proofable in your AI tool, or build it into your product. Verify a person or account, then let your server check the proof before it grants access. - - One-step MCP setup and agent skills for Claude, Cursor, ChatGPT, Hermes, OpenClaw, and compatible clients. + + Copy a setup prompt or open the install button for your tool. - - Run a public check, create your own proof, and see the record your product can verify. + + Bring a repository, package, or runtime link and review the import. diff --git a/mcp/setup.mdx b/mcp/setup.mdx index 1a8b38b..4f64061 100644 --- a/mcp/setup.mdx +++ b/mcp/setup.mdx @@ -5,116 +5,51 @@ description: "Add the Proofable MCP endpoint to any MCP client, then connect and icon: plug --- -import ConnectHosts from '/snippets/connect-hosts.mdx'; - -One endpoint works everywhere. The hosts below are shortcuts; anything else that speaks MCP uses the same URL. +Paste this into your AI tool, or choose your client below. Your tool opens sign-in when it needs your account. -Interactive clients sign in with OAuth. Servers and CI use an [access key](/deployment/access-keys). Both use `https://mcp.proofable.me/mcp` and the same profile. + +Set up Proofable in this tool using https://docs.proofable.me/mcp/setup. Use its supported plugin or one-click install when available. In a terminal-capable tool, use npx -y @proofable/sdk setup for the MCP server and workflow skill. Reuse an existing connection. Complete the sign-in this tool opens, then load my profile once. Ask what I want to do and help me finish one useful task, such as reviewing GitHub code or summarizing Gmail. Connect only the app that task needs, with my approval, or let me paste context. If I bring an existing agent, guide its import and let me review before registering it. Ask before granting access, sending, publishing, or spending. Report what actually worked and any action I still need to take. + ## Connect your client - - -Proofable answers **Passed**, **Action needed**, or **Blocked**. - -## From a terminal - -The CLI writes the same entry for any tool, plus the Proofable workflow skill: - -```bash -npx -y @proofable/sdk setup -``` - -With the CLI installed globally, run `proofable setup`. If a Proofable plugin is already installed, setup does not add a second `proofable` entry. [CLI reference](/sdks/cli) - -## After you connect - -### Show your profile - -```text -Show my Proofable profile and current proofs. -``` - -This loads the signed-in profile and current proofs. Do not fetch every proof as the first step. - -### Load an existing agent - -```text -Show what my agents are allowed to do. -``` - -This loads agents on the signed-in profile. Create or import one if you do not have one yet. +import ConnectHosts from '/snippets/connect-hosts.mdx'; -```bash -npx -y @proofable/sdk mount --apply cursor -``` + -`--apply` accepts `cursor`, `claude`, `codex`, `hermes`, `openclaw`, or `opencode`. VS Code uses `--apply cursor`. [Load agent context](/agents/runtime-mount) +## Do your first task -### Create an agent +Ask your AI for the outcome you want. It can guide the next connection or use context you paste. ```text -Create or import an agent on my Proofable profile. Set spend and action limits. Then confirm it is ready. +Review my GitHub pull requests for bugs and risky changes. Help me connect GitHub if needed, with my approval, or let me paste a diff. ``` -This calls `proofable_agent_create`, then `proofable_agent_link` to confirm. The agent lives on your signed-in profile unless you ask for a dedicated spend account. - -### Read a proof - ```text -Read my current proofs. If I give you a proof ID, fetch that record. +Summarize Gmail messages that need my attention and draft replies for my review. Help me connect Gmail if needed. Do not send anything. ``` -This calls `proofable_proofs_get`. Use it when you need a specific record, not as the first action after Connect. Use `proofable_proofs_check` only for yes/no eligibility. - -### Link apps - -Open **Connections** on [proofable.me](https://proofable.me) to link the apps your agents use. [Connections](/connections) - -### Sell from your assistant - -1. For paid listings, set up payouts under [Payouts](https://proofable.me/profile?tab=credits). -2. [Create a listing](https://proofable.me/profile/portals/new). Name it, say what buyers get, and set the price. -3. Add what buyers must verify, and set a price or keep it free. -4. Set discovery to **Listed**, then share the link. +To bring an agent you already use, follow [Import an agent](/agents/import). To choose a starter, [create an agent on your profile](https://proofable.me/profile/agents/new). An agent identity is optional for using your profile directly. -To embed a gate in your own app, see [Sell access with one gate](/gates/sell-access). - -## Paste this prompt - -Give your assistant this prompt to connect and set up in one pass: - -```text -# Proofable connect - -Connect this host to Proofable. -1. Install Proofable (plugin, or add https://mcp.proofable.me/mcp). Do not add a second `proofable` entry. -2. Tell me to sign in if I am not signed in. -3. Ask for my Proofable profile and current proofs. Then show what my agents are allowed to do. Before spend, publish, secrets, or a sensitive tool, check current proof first. Summarize as Passed, Action needed, or Blocked. -4. If I want to sell: open payouts at https://proofable.me/profile?tab=credits, then create a listing at https://proofable.me/profile/portals/new. -5. If I give you a proof ID, read that record. Do not invent proof IDs. +## From a terminal -Endpoint: https://mcp.proofable.me/mcp -Docs: https://docs.proofable.me/mcp/setup +```bash +npx -y @proofable/sdk setup ``` -## Disconnect +The CLI registers the hosted server and installs the workflow skill for supported clients. It reuses existing plugin entries. [CLI reference](/sdks/cli) -```bash -npx -y @proofable/sdk disconnect -``` +## Building an app -This revokes the stored OAuth token. For an access key, add `--access-key ` to revoke the key and remove it from local MCP config. +Use the [JavaScript SDK](/sdks/javascript), [HTTP API](/api/overview), or [hosted verification](/verification/hosted) for your product. Servers and CI use [access keys](/deployment/access-keys). ## Troubleshooting -- **No tools appear.** Restart the MCP client after you register the endpoint. -- **Session dropped or Unauthorized.** Run `npx -y @proofable/sdk refresh` first: it reuses the saved refresh token with no browser round trip. Sign out and sign in again only if that fails. -- **The host has no Connect button.** Use the host's own MCP sign-in after you register the endpoint. Only Claude connectors and Devin render a control called Connect. -- **Two Proofable servers.** A marketplace plugin and a manual config both registered Proofable. Remove the extra `proofable` entry. If you used the CLI, run `npx -y @proofable/sdk setup` again to drop the leftover entry. -- **Confirm the connection.** Run `npx -y @proofable/sdk doctor --live`. -- **Agent permission missing.** Create or refresh the agent permission on Proofable, then run `mount` again. -- **Search-only endpoint.** Use `https://mcp.proofable.me/mcp`. `https://docs.proofable.me/mcp` only searches these docs. -- **Upgrading from `@neus`.** [Migrate from @neus](/migrate) has the rename map and fixes for old setups. +- No tools appear. Restart your client after adding Proofable. +- Session expired. Run `npx -y @proofable/sdk refresh` before signing in again. +- Confirm the connection. Run `npx -y @proofable/sdk doctor --live`. +- Two Proofable entries. Keep one plugin or manual server entry. Running setup again removes a leftover entry when the plugin already supplies it. +- Need agent context in a project. Run `npx -y @proofable/sdk mount --apply `. See [Load agent context](/agents/runtime-mount). +- Search-only endpoint. Use `https://mcp.proofable.me/mcp`. The docs MCP endpoint only searches documentation. -[MCP overview](/mcp/overview) lists every tool. To run an agent on your own infrastructure, see [Private cloud](/deployment/private-cloud). +To disconnect, run `npx -y @proofable/sdk disconnect`. [MCP tools](/mcp/tools) and [authentication](/mcp/auth) cover the full reference. diff --git a/snippets/connect-hosts.mdx b/snippets/connect-hosts.mdx index 0645285..a8487f1 100644 --- a/snippets/connect-hosts.mdx +++ b/snippets/connect-hosts.mdx @@ -1,163 +1,61 @@ - - - - Proofable works in any app that speaks MCP. The endpoint and the sign-in are the same everywhere. + + [Add Proofable to Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=proofable&config=eyJ1cmwiOiJodHRwczovL21jcC5wcm9vZmFibGUubWUvbWNwIn0%3D) - 1. Open the app's MCP or connector settings. - 2. Add `https://mcp.proofable.me/mcp`. - 3. Complete the sign-in your client opens. - - Try it: - - ```text - Show my Proofable profile and current proofs. - ``` - - + Enable Proofable in Customize and complete the sign-in Cursor opens. - - - - Works in Claude on the web and in Claude Desktop. - - 1. Open **Customize**, then **Connectors**. - 2. Click **+**, then **Add custom connector**. - 3. Enter `https://mcp.proofable.me/mcp` and click **Add**. - 4. Click **Connect** and sign in to Proofable. - - On Team and Enterprise plans, an owner adds the connector first under **Organization settings**, then **Connectors**. Members then click **Connect**. - - Try it: - - ```text - Show my Proofable profile and current proofs. - ``` - - - Open [proofable.me/install](https://proofable.me/install) and choose **Open in Cursor**. Or add Proofable to `~/.cursor/mcp.json`: - - ```json - { - "mcpServers": { - "proofable": { - "url": "https://mcp.proofable.me/mcp" - } - } - } - ``` - - Cursor adds Proofable. Turn it on in Customize, and Cursor opens the sign-in. - - Try it: - - ```text - Show my Proofable profile and current proofs. - ``` - - - Install the Proofable plugin. It registers the server and adds the Proofable workflow skills. - - ```text - /plugin marketplace add proofable/mcp - /plugin install proofable-mcp@proofable - ``` - - Or register only the server: + + Run these in Claude Code to install the server and workflow skills: - ```bash - claude mcp add --transport http proofable https://mcp.proofable.me/mcp - ``` + ```text + /plugin marketplace add proofable/mcp + /plugin install proofable-mcp@proofable + ``` - Use one of the two, not both. Then run `/mcp` and sign in to Proofable. - - Try it: - - ```text - Show my Proofable profile and current proofs. - ``` - - - ```bash - npx -y @proofable/sdk setup --client codex - codex mcp login proofable - ``` - - Setup adds Proofable to `~/.codex/config.toml`. The login command opens browser sign-in. - - Try it: - - ```text - Show my Proofable profile and current proofs. - ``` - - - Open [proofable.me/install](https://proofable.me/install) and choose **Open in VS Code**. Or add Proofable to `.vscode/mcp.json`: - - ```json - { - "servers": { - "proofable": { - "type": "http", - "url": "https://mcp.proofable.me/mcp" - } - } - } - ``` + Run `/mcp`, select Proofable, and complete sign-in. + + + [Open Claude](https://claude.ai/) - VS Code adds Proofable. Start it from **MCP: List Servers**, and VS Code opens the sign-in. + In Customize, open Connectors and add a custom connector with this URL: - Try it: + ```text + https://mcp.proofable.me/mcp + ``` - ```text - Show my Proofable profile and current proofs. - ``` - - - Add the hosted server to the client's MCP settings: + Choose Connect and complete sign-in. On Team and Enterprise, an owner adds the connector first. + + + Run in your terminal: - ```json - { - "mcpServers": { - "proofable": { - "type": "http", - "url": "https://mcp.proofable.me/mcp" - } - } - } - ``` + ```bash + npx -y @proofable/sdk setup --client codex + codex mcp login proofable + ``` - If the client rejects the `type` field, remove it and keep `url`. Then complete the sign-in your client opens. + Codex opens browser sign-in. + + + [Open ChatGPT](https://chatgpt.com/) - Try it: + Enable developer mode in Settings, add a custom MCP connection with the URL below, and complete the sign-in ChatGPT opens. Your plan or workspace policy must allow custom connections. - ```text - Show my Proofable profile and current proofs. - ``` - - - For servers, CI, scheduled jobs, containers, and agents that cannot use browser OAuth. + ```text + https://mcp.proofable.me/mcp + ``` + + + [Add Proofable to VS Code](vscode:mcp/install?%7B%22name%22%3A%22proofable%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fmcp.proofable.me%2Fmcp%22%7D) - 1. Create a key under [Access keys](https://proofable.me/profile?tab=account). - 2. Store it in the environment as `PROOFABLE_ACCESS_KEY`. - 3. Send it as a Bearer token: + Start Proofable from MCP: List Servers and complete the sign-in VS Code opens. + + + Add this URL in your client's MCP settings, then complete its sign-in: - ```json - { - "mcpServers": { - "proofable": { - "type": "http", - "url": "https://mcp.proofable.me/mcp", - "headers": { - "Authorization": "Bearer ${PROOFABLE_ACCESS_KEY}" - } - } - } - } - ``` + ```text + https://mcp.proofable.me/mcp + ``` - Treat the key like a password. [Access keys](/deployment/access-keys) covers the SDK, containers, and rotation. - - + For terminal clients, ask your agent to follow the setup prompt above. [Authentication](/mcp/auth) covers clients that require a server key. From aaa268f278b30ec8e8769905f45cc87b0e03ed5a Mon Sep 17 00:00:00 2001 From: Chris Leal <184772742+chrisjleal@users.noreply.github.com> Date: Sun, 27 Sep 2026 21:34:38 -0500 Subject: [PATCH 2/6] Route retired install links directly to setup docs --- scripts/validate-doc-routes.mjs | 410 ++++++++++++++++---------------- 1 file changed, 205 insertions(+), 205 deletions(-) diff --git a/scripts/validate-doc-routes.mjs b/scripts/validate-doc-routes.mjs index 681515e..de160e1 100644 --- a/scripts/validate-doc-routes.mjs +++ b/scripts/validate-doc-routes.mjs @@ -1,134 +1,134 @@ -#!/usr/bin/env node - -import fs from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); -const docsRoot = repoRoot; -const config = JSON.parse(fs.readFileSync(path.join(docsRoot, 'docs.json'), 'utf8')); -const errors = []; - -function walkFiles(directory) { - return fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => { - const target = path.join(directory, entry.name); - return entry.isDirectory() ? walkFiles(target) : [target]; - }); -} - -const pngSignature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); -for (const file of walkFiles(path.join(docsRoot, 'images')).filter(file => file.endsWith('.png'))) { - const signature = fs.readFileSync(file).subarray(0, pngSignature.length); - if (!signature.equals(pngSignature)) { - errors.push(`Invalid PNG asset: ${path.relative(repoRoot, file)}`); - } -} - -function collectPageIds(value, ids = []) { - if (Array.isArray(value)) { - for (const entry of value) collectPageIds(entry, ids); - return ids; - } - if (!value || typeof value !== 'object') return ids; - if (Array.isArray(value.pages)) { - for (const page of value.pages) { - if (typeof page === 'string') ids.push(page); - } - } - for (const child of Object.values(value)) collectPageIds(child, ids); - return ids; -} - -function pageRoute(pageId) { - const withoutIndex = pageId.replace(/\/index$/, ''); - return withoutIndex ? `/${withoutIndex}` : '/'; -} - -function pageFile(pageId) { - return path.join(docsRoot, `${pageId}.mdx`); -} - -const pageIds = [...new Set(collectPageIds(config.navigation))]; -const routes = new Set(['/']); -for (const pageId of pageIds) { - routes.add(pageRoute(pageId)); - if (!fs.existsSync(pageFile(pageId))) { - errors.push(`Navigation page is missing: ${pageId}.mdx`); - } -} - -const mdxFiles = walkFiles(docsRoot).filter( - file => file.endsWith('.mdx') && !file.startsWith(path.join(docsRoot, 'snippets', path.sep)), -); -for (const file of mdxFiles) { - const relative = path.relative(docsRoot, file).replaceAll(path.sep, '/').replace(/\.mdx$/, ''); - routes.add(pageRoute(relative)); -} - -function readFrontmatter(source, file) { - const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/); - if (!match) { - errors.push(`${path.relative(repoRoot, file)} is missing frontmatter`); - return null; - } - - const fields = {}; - for (const line of match[1].split(/\r?\n/)) { - const field = line.match(/^(title|description):\s*(.+?)\s*$/); - if (!field) continue; - fields[field[1]] = field[2].replace(/^(["'])([\s\S]*)\1$/, '$2').trim(); - } - - return { fields, body: source.slice(match[0].length) }; -} - -const uniqueFrontmatterFields = new Map([ - ['title', new Map()], - ['description', new Map()], -]); - -for (const file of mdxFiles) { - const source = fs.readFileSync(file, 'utf8'); - const frontmatter = readFrontmatter(source, file); - if (!frontmatter) continue; - - for (const [fieldName, seenValues] of uniqueFrontmatterFields) { - const value = frontmatter.fields[fieldName]; - if (!value) { - errors.push(`${path.relative(repoRoot, file)} is missing frontmatter ${fieldName}`); - continue; - } - - const normalizedValue = value.toLocaleLowerCase('en-US'); - const firstFile = seenValues.get(normalizedValue); - if (firstFile) { - errors.push( - `Duplicate ${fieldName} "${value}": ${path.relative(repoRoot, firstFile)} and ${path.relative(repoRoot, file)}`, - ); - } else { - seenValues.set(normalizedValue, file); - } - } - - const description = frontmatter.fields.description; - if (description && frontmatter.body.includes(description)) { - errors.push(`${path.relative(repoRoot, file)} repeats its frontmatter description verbatim in the page body`); - } -} - -const redirects = Array.isArray(config.redirects) ? config.redirects : []; -const redirectSources = new Set(redirects.map(row => row.source)); -for (const redirect of redirects) { - const destination = String(redirect.destination || '').split(/[?#]/, 1)[0]; - if (!destination || destination.includes(':')) continue; - if (!routes.has(destination) && !redirectSources.has(destination)) { - errors.push(`Redirect destination is missing: ${redirect.source} -> ${destination}`); - } -} - +#!/usr/bin/env node + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const docsRoot = repoRoot; +const config = JSON.parse(fs.readFileSync(path.join(docsRoot, 'docs.json'), 'utf8')); +const errors = []; + +function walkFiles(directory) { + return fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => { + const target = path.join(directory, entry.name); + return entry.isDirectory() ? walkFiles(target) : [target]; + }); +} + +const pngSignature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); +for (const file of walkFiles(path.join(docsRoot, 'images')).filter(file => file.endsWith('.png'))) { + const signature = fs.readFileSync(file).subarray(0, pngSignature.length); + if (!signature.equals(pngSignature)) { + errors.push(`Invalid PNG asset: ${path.relative(repoRoot, file)}`); + } +} + +function collectPageIds(value, ids = []) { + if (Array.isArray(value)) { + for (const entry of value) collectPageIds(entry, ids); + return ids; + } + if (!value || typeof value !== 'object') return ids; + if (Array.isArray(value.pages)) { + for (const page of value.pages) { + if (typeof page === 'string') ids.push(page); + } + } + for (const child of Object.values(value)) collectPageIds(child, ids); + return ids; +} + +function pageRoute(pageId) { + const withoutIndex = pageId.replace(/\/index$/, ''); + return withoutIndex ? `/${withoutIndex}` : '/'; +} + +function pageFile(pageId) { + return path.join(docsRoot, `${pageId}.mdx`); +} + +const pageIds = [...new Set(collectPageIds(config.navigation))]; +const routes = new Set(['/']); +for (const pageId of pageIds) { + routes.add(pageRoute(pageId)); + if (!fs.existsSync(pageFile(pageId))) { + errors.push(`Navigation page is missing: ${pageId}.mdx`); + } +} + +const mdxFiles = walkFiles(docsRoot).filter( + file => file.endsWith('.mdx') && !file.startsWith(path.join(docsRoot, 'snippets', path.sep)), +); +for (const file of mdxFiles) { + const relative = path.relative(docsRoot, file).replaceAll(path.sep, '/').replace(/\.mdx$/, ''); + routes.add(pageRoute(relative)); +} + +function readFrontmatter(source, file) { + const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/); + if (!match) { + errors.push(`${path.relative(repoRoot, file)} is missing frontmatter`); + return null; + } + + const fields = {}; + for (const line of match[1].split(/\r?\n/)) { + const field = line.match(/^(title|description):\s*(.+?)\s*$/); + if (!field) continue; + fields[field[1]] = field[2].replace(/^(["'])([\s\S]*)\1$/, '$2').trim(); + } + + return { fields, body: source.slice(match[0].length) }; +} + +const uniqueFrontmatterFields = new Map([ + ['title', new Map()], + ['description', new Map()], +]); + +for (const file of mdxFiles) { + const source = fs.readFileSync(file, 'utf8'); + const frontmatter = readFrontmatter(source, file); + if (!frontmatter) continue; + + for (const [fieldName, seenValues] of uniqueFrontmatterFields) { + const value = frontmatter.fields[fieldName]; + if (!value) { + errors.push(`${path.relative(repoRoot, file)} is missing frontmatter ${fieldName}`); + continue; + } + + const normalizedValue = value.toLocaleLowerCase('en-US'); + const firstFile = seenValues.get(normalizedValue); + if (firstFile) { + errors.push( + `Duplicate ${fieldName} "${value}": ${path.relative(repoRoot, firstFile)} and ${path.relative(repoRoot, file)}`, + ); + } else { + seenValues.set(normalizedValue, file); + } + } + + const description = frontmatter.fields.description; + if (description && frontmatter.body.includes(description)) { + errors.push(`${path.relative(repoRoot, file)} repeats its frontmatter description verbatim in the page body`); + } +} + +const redirects = Array.isArray(config.redirects) ? config.redirects : []; +const redirectSources = new Set(redirects.map(row => row.source)); +for (const redirect of redirects) { + const destination = String(redirect.destination || '').split(/[?#]/, 1)[0]; + if (!destination || destination.includes(':')) continue; + if (!routes.has(destination) && !redirectSources.has(destination)) { + errors.push(`Redirect destination is missing: ${redirect.source} -> ${destination}`); + } +} + const installRedirect = redirects.find(row => row.source === '/install'); -if (installRedirect?.destination !== 'https://proofable.me/install') { - errors.push('/install must route readers to the Proofable install guide'); +if (installRedirect?.destination !== '/mcp/setup') { + errors.push('/install must route readers directly to the public MCP setup docs'); } if (redirectSources.has('/integrations/server')) { errors.push('/integrations/server must remain directly reachable'); @@ -140,78 +140,78 @@ const quickstartRedirect = redirects.find(row => row.source === '/quickstart'); if (quickstartRedirect?.destination !== '/') { errors.push('/quickstart must redirect to Getting started'); } -for (const removedPage of [ - 'mcp/ide-plugin', - 'mcp/journeys', - 'brand-kit', - 'agents/a2a', - 'platform/environments', - 'quickstart', - 'cookbook/overview', - 'agents/overview', - 'verification/overview', - 'sdks/overview', -]) { - if (pageIds.includes(removedPage)) errors.push(`Removed page remains in navigation: ${removedPage}`); -} - -function normalizeDocsRoute(rawHref, sourceFile) { - const href = String(rawHref || '').trim(); - if (!href || href.startsWith('#') || /^[a-z][a-z0-9+.-]*:/i.test(href)) return null; - const withoutSuffix = href.split(/[?#]/, 1)[0]; - if (!withoutSuffix) return null; - if (withoutSuffix.startsWith('/')) return path.posix.normalize(withoutSuffix); - - const sourceRelative = path.relative(docsRoot, sourceFile).replaceAll(path.sep, '/'); - const sourceRouteDir = path.posix.dirname(`/${sourceRelative.replace(/\.mdx$/, '')}`); - return path.posix.normalize(path.posix.join(sourceRouteDir, withoutSuffix.replace(/\.mdx$/, ''))); -} - -const hrefPatterns = [/\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, /\bhref=["']([^"']+)["']/g]; -for (const file of mdxFiles) { - const source = fs.readFileSync(file, 'utf8'); - for (const pattern of hrefPatterns) { - for (const match of source.matchAll(pattern)) { - const route = normalizeDocsRoute(match[1], file); - if (!route || routes.has(route) || redirectSources.has(route)) continue; - - const relativeTarget = path.resolve(path.dirname(file), match[1].split(/[?#]/, 1)[0]); - if (fs.existsSync(relativeTarget)) continue; - errors.push(`${path.relative(repoRoot, file)} links to missing route ${route}`); - } - } -} - -for (const option of config.contextual?.options || []) { - if (!option || typeof option !== 'object') continue; - const href = String(option.href || ''); - if (href.startsWith('/') && !routes.has(href) && !redirectSources.has(href)) { - errors.push(`Contextual link is missing: ${href}`); - } -} - -if (config.appearance?.default !== 'light') { - errors.push('appearance.default must be light to match the product color-scheme default'); -} -if (config.appearance?.strict === true) { - errors.push('appearance.strict must be false so docs can toggle light/dark like the app'); -} -if (config.background?.color?.light !== '#F5F4F1') { - errors.push('background.color.light must be Bone #F5F4F1'); -} -if (config.background?.color?.dark !== '#050505') { - errors.push('background.color.dark must stay #050505'); -} -if (config.styling?.codeblocks !== 'system') { - errors.push('styling.codeblocks must be system so code follows the site theme'); -} -if (config.seo?.metatags?.['color-scheme'] !== 'light dark') { - errors.push('seo.metatags.color-scheme must advertise both light and dark'); -} - -if (errors.length > 0) { - console.error(`Docs route validation failed:\n- ${[...new Set(errors)].join('\n- ')}`); - process.exit(1); -} - -console.log(`Docs routes valid: ${pageIds.length} navigation pages, ${redirects.length} redirects, ${mdxFiles.length} MDX files.`); +for (const removedPage of [ + 'mcp/ide-plugin', + 'mcp/journeys', + 'brand-kit', + 'agents/a2a', + 'platform/environments', + 'quickstart', + 'cookbook/overview', + 'agents/overview', + 'verification/overview', + 'sdks/overview', +]) { + if (pageIds.includes(removedPage)) errors.push(`Removed page remains in navigation: ${removedPage}`); +} + +function normalizeDocsRoute(rawHref, sourceFile) { + const href = String(rawHref || '').trim(); + if (!href || href.startsWith('#') || /^[a-z][a-z0-9+.-]*:/i.test(href)) return null; + const withoutSuffix = href.split(/[?#]/, 1)[0]; + if (!withoutSuffix) return null; + if (withoutSuffix.startsWith('/')) return path.posix.normalize(withoutSuffix); + + const sourceRelative = path.relative(docsRoot, sourceFile).replaceAll(path.sep, '/'); + const sourceRouteDir = path.posix.dirname(`/${sourceRelative.replace(/\.mdx$/, '')}`); + return path.posix.normalize(path.posix.join(sourceRouteDir, withoutSuffix.replace(/\.mdx$/, ''))); +} + +const hrefPatterns = [/\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, /\bhref=["']([^"']+)["']/g]; +for (const file of mdxFiles) { + const source = fs.readFileSync(file, 'utf8'); + for (const pattern of hrefPatterns) { + for (const match of source.matchAll(pattern)) { + const route = normalizeDocsRoute(match[1], file); + if (!route || routes.has(route) || redirectSources.has(route)) continue; + + const relativeTarget = path.resolve(path.dirname(file), match[1].split(/[?#]/, 1)[0]); + if (fs.existsSync(relativeTarget)) continue; + errors.push(`${path.relative(repoRoot, file)} links to missing route ${route}`); + } + } +} + +for (const option of config.contextual?.options || []) { + if (!option || typeof option !== 'object') continue; + const href = String(option.href || ''); + if (href.startsWith('/') && !routes.has(href) && !redirectSources.has(href)) { + errors.push(`Contextual link is missing: ${href}`); + } +} + +if (config.appearance?.default !== 'light') { + errors.push('appearance.default must be light to match the product color-scheme default'); +} +if (config.appearance?.strict === true) { + errors.push('appearance.strict must be false so docs can toggle light/dark like the app'); +} +if (config.background?.color?.light !== '#F5F4F1') { + errors.push('background.color.light must be Bone #F5F4F1'); +} +if (config.background?.color?.dark !== '#050505') { + errors.push('background.color.dark must stay #050505'); +} +if (config.styling?.codeblocks !== 'system') { + errors.push('styling.codeblocks must be system so code follows the site theme'); +} +if (config.seo?.metatags?.['color-scheme'] !== 'light dark') { + errors.push('seo.metatags.color-scheme must advertise both light and dark'); +} + +if (errors.length > 0) { + console.error(`Docs route validation failed:\n- ${[...new Set(errors)].join('\n- ')}`); + process.exit(1); +} + +console.log(`Docs routes valid: ${pageIds.length} navigation pages, ${redirects.length} redirects, ${mdxFiles.length} MDX files.`); From 07069472a38e72b66ee70b6c2afac1b5f3448941 Mon Sep 17 00:00:00 2001 From: Chris Leal <184772742+chrisjleal@users.noreply.github.com> Date: Sun, 27 Sep 2026 21:38:06 -0500 Subject: [PATCH 3/6] Updated mintlify pages - Updated mcp/setup.mdx - Updated snippets/connect-hosts.mdx - Updated agents/import.mdx - Updated index.mdx Mintlify-Source: dashboard-editor --- agents/import.mdx | 4 ---- index.mdx | 2 +- mcp/setup.mdx | 24 ++++++++---------------- snippets/connect-hosts.mdx | 2 +- 4 files changed, 10 insertions(+), 22 deletions(-) diff --git a/agents/import.mdx b/agents/import.mdx index 0d5d541..a9352f5 100644 --- a/agents/import.mdx +++ b/agents/import.mdx @@ -7,10 +7,6 @@ icon: file-import Drop a package, pick a GitHub repo, or paste a card or runtime link. Proofable inspects the source, registers identity on your profile, and asks you to reconnect apps. It does not copy secrets or run the package. - -Help me bring my existing agent into Proofable. If this tool is not connected, follow https://docs.proofable.me/mcp/setup first. Ask for the repository, package, or runtime link, inspect it using the existing import flow, and let me review before registering it. Keep credentials out. Reuse existing connections and reconnect apps only with my approval. Then help me complete one useful task. - - Supported inputs include OpenClaw workspace files, Hermes config and skills, Agent Skills `SKILL.md` folders, A2A Agent Cards, MCP URLs, GitHub repositories, and zip or tar packages. ## From the product diff --git a/index.mdx b/index.mdx index 2b4a71c..c72e6cb 100644 --- a/index.mdx +++ b/index.mdx @@ -16,7 +16,7 @@ Use Proofable in your AI tool, or build it into your product. Verify a person or account, then let your server check the proof before it grants access. - Copy a setup prompt or open the install button for your tool. + Copy the existing setup skill or open the install link for your tool. Bring a repository, package, or runtime link and review the import. diff --git a/mcp/setup.mdx b/mcp/setup.mdx index 4f64061..56bcf5a 100644 --- a/mcp/setup.mdx +++ b/mcp/setup.mdx @@ -5,10 +5,10 @@ description: "Add the Proofable MCP endpoint to any MCP client, then connect and icon: plug --- -Paste this into your AI tool, or choose your client below. Your tool opens sign-in when it needs your account. +Give your AI the existing [Proofable setup skill](https://raw.githubusercontent.com/proofable/mcp/main/skills/proofable-setup/SKILL.md), or choose your client below. - -Set up Proofable in this tool using https://docs.proofable.me/mcp/setup. Use its supported plugin or one-click install when available. In a terminal-capable tool, use npx -y @proofable/sdk setup for the MCP server and workflow skill. Reuse an existing connection. Complete the sign-in this tool opens, then load my profile once. Ask what I want to do and help me finish one useful task, such as reviewing GitHub code or summarizing Gmail. Connect only the app that task needs, with my approval, or let me paste context. If I bring an existing agent, guide its import and let me review before registering it. Ask before granting access, sending, publishing, or spending. Report what actually worked and any action I still need to take. + +https://raw.githubusercontent.com/proofable/mcp/main/skills/proofable-setup/SKILL.md ## Connect your client @@ -17,19 +17,7 @@ import ConnectHosts from '/snippets/connect-hosts.mdx'; -## Do your first task - -Ask your AI for the outcome you want. It can guide the next connection or use context you paste. - -```text -Review my GitHub pull requests for bugs and risky changes. Help me connect GitHub if needed, with my approval, or let me paste a diff. -``` - -```text -Summarize Gmail messages that need my attention and draft replies for my review. Help me connect Gmail if needed. Do not send anything. -``` - -To bring an agent you already use, follow [Import an agent](/agents/import). To choose a starter, [create an agent on your profile](https://proofable.me/profile/agents/new). An agent identity is optional for using your profile directly. +To bring an agent you already use, follow [Import an agent](/agents/import). ## From a terminal @@ -41,6 +29,10 @@ The CLI registers the hosted server and installs the workflow skill for supporte ## Building an app + +https://raw.githubusercontent.com/proofable/mcp/main/skills/proofable-integrate/SKILL.md + + Use the [JavaScript SDK](/sdks/javascript), [HTTP API](/api/overview), or [hosted verification](/verification/hosted) for your product. Servers and CI use [access keys](/deployment/access-keys). ## Troubleshooting diff --git a/snippets/connect-hosts.mdx b/snippets/connect-hosts.mdx index a8487f1..6dce3f4 100644 --- a/snippets/connect-hosts.mdx +++ b/snippets/connect-hosts.mdx @@ -56,6 +56,6 @@ https://mcp.proofable.me/mcp ``` - For terminal clients, ask your agent to follow the setup prompt above. [Authentication](/mcp/auth) covers clients that require a server key. + For terminal clients, use the setup skill above. [Authentication](/mcp/auth) covers clients that require a server key. From c1861000427e4f7e82569af77915de94b5c0d0ae Mon Sep 17 00:00:00 2001 From: Chris Leal <184772742+chrisjleal@users.noreply.github.com> Date: Sun, 27 Sep 2026 21:39:35 -0500 Subject: [PATCH 4/6] Keep setup redirect validation diff limited to its two changed lines --- scripts/validate-doc-routes.mjs | 406 ++++++++++++++++---------------- 1 file changed, 203 insertions(+), 203 deletions(-) diff --git a/scripts/validate-doc-routes.mjs b/scripts/validate-doc-routes.mjs index de160e1..e900ad2 100644 --- a/scripts/validate-doc-routes.mjs +++ b/scripts/validate-doc-routes.mjs @@ -1,131 +1,131 @@ -#!/usr/bin/env node - -import fs from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); -const docsRoot = repoRoot; -const config = JSON.parse(fs.readFileSync(path.join(docsRoot, 'docs.json'), 'utf8')); -const errors = []; - -function walkFiles(directory) { - return fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => { - const target = path.join(directory, entry.name); - return entry.isDirectory() ? walkFiles(target) : [target]; - }); -} - -const pngSignature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); -for (const file of walkFiles(path.join(docsRoot, 'images')).filter(file => file.endsWith('.png'))) { - const signature = fs.readFileSync(file).subarray(0, pngSignature.length); - if (!signature.equals(pngSignature)) { - errors.push(`Invalid PNG asset: ${path.relative(repoRoot, file)}`); - } -} - -function collectPageIds(value, ids = []) { - if (Array.isArray(value)) { - for (const entry of value) collectPageIds(entry, ids); - return ids; - } - if (!value || typeof value !== 'object') return ids; - if (Array.isArray(value.pages)) { - for (const page of value.pages) { - if (typeof page === 'string') ids.push(page); - } - } - for (const child of Object.values(value)) collectPageIds(child, ids); - return ids; -} - -function pageRoute(pageId) { - const withoutIndex = pageId.replace(/\/index$/, ''); - return withoutIndex ? `/${withoutIndex}` : '/'; -} - -function pageFile(pageId) { - return path.join(docsRoot, `${pageId}.mdx`); -} - -const pageIds = [...new Set(collectPageIds(config.navigation))]; -const routes = new Set(['/']); -for (const pageId of pageIds) { - routes.add(pageRoute(pageId)); - if (!fs.existsSync(pageFile(pageId))) { - errors.push(`Navigation page is missing: ${pageId}.mdx`); - } -} - -const mdxFiles = walkFiles(docsRoot).filter( - file => file.endsWith('.mdx') && !file.startsWith(path.join(docsRoot, 'snippets', path.sep)), -); -for (const file of mdxFiles) { - const relative = path.relative(docsRoot, file).replaceAll(path.sep, '/').replace(/\.mdx$/, ''); - routes.add(pageRoute(relative)); -} - -function readFrontmatter(source, file) { - const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/); - if (!match) { - errors.push(`${path.relative(repoRoot, file)} is missing frontmatter`); - return null; - } - - const fields = {}; - for (const line of match[1].split(/\r?\n/)) { - const field = line.match(/^(title|description):\s*(.+?)\s*$/); - if (!field) continue; - fields[field[1]] = field[2].replace(/^(["'])([\s\S]*)\1$/, '$2').trim(); - } - - return { fields, body: source.slice(match[0].length) }; -} - -const uniqueFrontmatterFields = new Map([ - ['title', new Map()], - ['description', new Map()], -]); - -for (const file of mdxFiles) { - const source = fs.readFileSync(file, 'utf8'); - const frontmatter = readFrontmatter(source, file); - if (!frontmatter) continue; - - for (const [fieldName, seenValues] of uniqueFrontmatterFields) { - const value = frontmatter.fields[fieldName]; - if (!value) { - errors.push(`${path.relative(repoRoot, file)} is missing frontmatter ${fieldName}`); - continue; - } - - const normalizedValue = value.toLocaleLowerCase('en-US'); - const firstFile = seenValues.get(normalizedValue); - if (firstFile) { - errors.push( - `Duplicate ${fieldName} "${value}": ${path.relative(repoRoot, firstFile)} and ${path.relative(repoRoot, file)}`, - ); - } else { - seenValues.set(normalizedValue, file); - } - } - - const description = frontmatter.fields.description; - if (description && frontmatter.body.includes(description)) { - errors.push(`${path.relative(repoRoot, file)} repeats its frontmatter description verbatim in the page body`); - } -} - -const redirects = Array.isArray(config.redirects) ? config.redirects : []; -const redirectSources = new Set(redirects.map(row => row.source)); -for (const redirect of redirects) { - const destination = String(redirect.destination || '').split(/[?#]/, 1)[0]; - if (!destination || destination.includes(':')) continue; - if (!routes.has(destination) && !redirectSources.has(destination)) { - errors.push(`Redirect destination is missing: ${redirect.source} -> ${destination}`); - } -} - +#!/usr/bin/env node + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const docsRoot = repoRoot; +const config = JSON.parse(fs.readFileSync(path.join(docsRoot, 'docs.json'), 'utf8')); +const errors = []; + +function walkFiles(directory) { + return fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => { + const target = path.join(directory, entry.name); + return entry.isDirectory() ? walkFiles(target) : [target]; + }); +} + +const pngSignature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); +for (const file of walkFiles(path.join(docsRoot, 'images')).filter(file => file.endsWith('.png'))) { + const signature = fs.readFileSync(file).subarray(0, pngSignature.length); + if (!signature.equals(pngSignature)) { + errors.push(`Invalid PNG asset: ${path.relative(repoRoot, file)}`); + } +} + +function collectPageIds(value, ids = []) { + if (Array.isArray(value)) { + for (const entry of value) collectPageIds(entry, ids); + return ids; + } + if (!value || typeof value !== 'object') return ids; + if (Array.isArray(value.pages)) { + for (const page of value.pages) { + if (typeof page === 'string') ids.push(page); + } + } + for (const child of Object.values(value)) collectPageIds(child, ids); + return ids; +} + +function pageRoute(pageId) { + const withoutIndex = pageId.replace(/\/index$/, ''); + return withoutIndex ? `/${withoutIndex}` : '/'; +} + +function pageFile(pageId) { + return path.join(docsRoot, `${pageId}.mdx`); +} + +const pageIds = [...new Set(collectPageIds(config.navigation))]; +const routes = new Set(['/']); +for (const pageId of pageIds) { + routes.add(pageRoute(pageId)); + if (!fs.existsSync(pageFile(pageId))) { + errors.push(`Navigation page is missing: ${pageId}.mdx`); + } +} + +const mdxFiles = walkFiles(docsRoot).filter( + file => file.endsWith('.mdx') && !file.startsWith(path.join(docsRoot, 'snippets', path.sep)), +); +for (const file of mdxFiles) { + const relative = path.relative(docsRoot, file).replaceAll(path.sep, '/').replace(/\.mdx$/, ''); + routes.add(pageRoute(relative)); +} + +function readFrontmatter(source, file) { + const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/); + if (!match) { + errors.push(`${path.relative(repoRoot, file)} is missing frontmatter`); + return null; + } + + const fields = {}; + for (const line of match[1].split(/\r?\n/)) { + const field = line.match(/^(title|description):\s*(.+?)\s*$/); + if (!field) continue; + fields[field[1]] = field[2].replace(/^(["'])([\s\S]*)\1$/, '$2').trim(); + } + + return { fields, body: source.slice(match[0].length) }; +} + +const uniqueFrontmatterFields = new Map([ + ['title', new Map()], + ['description', new Map()], +]); + +for (const file of mdxFiles) { + const source = fs.readFileSync(file, 'utf8'); + const frontmatter = readFrontmatter(source, file); + if (!frontmatter) continue; + + for (const [fieldName, seenValues] of uniqueFrontmatterFields) { + const value = frontmatter.fields[fieldName]; + if (!value) { + errors.push(`${path.relative(repoRoot, file)} is missing frontmatter ${fieldName}`); + continue; + } + + const normalizedValue = value.toLocaleLowerCase('en-US'); + const firstFile = seenValues.get(normalizedValue); + if (firstFile) { + errors.push( + `Duplicate ${fieldName} "${value}": ${path.relative(repoRoot, firstFile)} and ${path.relative(repoRoot, file)}`, + ); + } else { + seenValues.set(normalizedValue, file); + } + } + + const description = frontmatter.fields.description; + if (description && frontmatter.body.includes(description)) { + errors.push(`${path.relative(repoRoot, file)} repeats its frontmatter description verbatim in the page body`); + } +} + +const redirects = Array.isArray(config.redirects) ? config.redirects : []; +const redirectSources = new Set(redirects.map(row => row.source)); +for (const redirect of redirects) { + const destination = String(redirect.destination || '').split(/[?#]/, 1)[0]; + if (!destination || destination.includes(':')) continue; + if (!routes.has(destination) && !redirectSources.has(destination)) { + errors.push(`Redirect destination is missing: ${redirect.source} -> ${destination}`); + } +} + const installRedirect = redirects.find(row => row.source === '/install'); if (installRedirect?.destination !== '/mcp/setup') { errors.push('/install must route readers directly to the public MCP setup docs'); @@ -140,78 +140,78 @@ const quickstartRedirect = redirects.find(row => row.source === '/quickstart'); if (quickstartRedirect?.destination !== '/') { errors.push('/quickstart must redirect to Getting started'); } -for (const removedPage of [ - 'mcp/ide-plugin', - 'mcp/journeys', - 'brand-kit', - 'agents/a2a', - 'platform/environments', - 'quickstart', - 'cookbook/overview', - 'agents/overview', - 'verification/overview', - 'sdks/overview', -]) { - if (pageIds.includes(removedPage)) errors.push(`Removed page remains in navigation: ${removedPage}`); -} - -function normalizeDocsRoute(rawHref, sourceFile) { - const href = String(rawHref || '').trim(); - if (!href || href.startsWith('#') || /^[a-z][a-z0-9+.-]*:/i.test(href)) return null; - const withoutSuffix = href.split(/[?#]/, 1)[0]; - if (!withoutSuffix) return null; - if (withoutSuffix.startsWith('/')) return path.posix.normalize(withoutSuffix); - - const sourceRelative = path.relative(docsRoot, sourceFile).replaceAll(path.sep, '/'); - const sourceRouteDir = path.posix.dirname(`/${sourceRelative.replace(/\.mdx$/, '')}`); - return path.posix.normalize(path.posix.join(sourceRouteDir, withoutSuffix.replace(/\.mdx$/, ''))); -} - -const hrefPatterns = [/\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, /\bhref=["']([^"']+)["']/g]; -for (const file of mdxFiles) { - const source = fs.readFileSync(file, 'utf8'); - for (const pattern of hrefPatterns) { - for (const match of source.matchAll(pattern)) { - const route = normalizeDocsRoute(match[1], file); - if (!route || routes.has(route) || redirectSources.has(route)) continue; - - const relativeTarget = path.resolve(path.dirname(file), match[1].split(/[?#]/, 1)[0]); - if (fs.existsSync(relativeTarget)) continue; - errors.push(`${path.relative(repoRoot, file)} links to missing route ${route}`); - } - } -} - -for (const option of config.contextual?.options || []) { - if (!option || typeof option !== 'object') continue; - const href = String(option.href || ''); - if (href.startsWith('/') && !routes.has(href) && !redirectSources.has(href)) { - errors.push(`Contextual link is missing: ${href}`); - } -} - -if (config.appearance?.default !== 'light') { - errors.push('appearance.default must be light to match the product color-scheme default'); -} -if (config.appearance?.strict === true) { - errors.push('appearance.strict must be false so docs can toggle light/dark like the app'); -} -if (config.background?.color?.light !== '#F5F4F1') { - errors.push('background.color.light must be Bone #F5F4F1'); -} -if (config.background?.color?.dark !== '#050505') { - errors.push('background.color.dark must stay #050505'); -} -if (config.styling?.codeblocks !== 'system') { - errors.push('styling.codeblocks must be system so code follows the site theme'); -} -if (config.seo?.metatags?.['color-scheme'] !== 'light dark') { - errors.push('seo.metatags.color-scheme must advertise both light and dark'); -} - -if (errors.length > 0) { - console.error(`Docs route validation failed:\n- ${[...new Set(errors)].join('\n- ')}`); - process.exit(1); -} - -console.log(`Docs routes valid: ${pageIds.length} navigation pages, ${redirects.length} redirects, ${mdxFiles.length} MDX files.`); +for (const removedPage of [ + 'mcp/ide-plugin', + 'mcp/journeys', + 'brand-kit', + 'agents/a2a', + 'platform/environments', + 'quickstart', + 'cookbook/overview', + 'agents/overview', + 'verification/overview', + 'sdks/overview', +]) { + if (pageIds.includes(removedPage)) errors.push(`Removed page remains in navigation: ${removedPage}`); +} + +function normalizeDocsRoute(rawHref, sourceFile) { + const href = String(rawHref || '').trim(); + if (!href || href.startsWith('#') || /^[a-z][a-z0-9+.-]*:/i.test(href)) return null; + const withoutSuffix = href.split(/[?#]/, 1)[0]; + if (!withoutSuffix) return null; + if (withoutSuffix.startsWith('/')) return path.posix.normalize(withoutSuffix); + + const sourceRelative = path.relative(docsRoot, sourceFile).replaceAll(path.sep, '/'); + const sourceRouteDir = path.posix.dirname(`/${sourceRelative.replace(/\.mdx$/, '')}`); + return path.posix.normalize(path.posix.join(sourceRouteDir, withoutSuffix.replace(/\.mdx$/, ''))); +} + +const hrefPatterns = [/\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, /\bhref=["']([^"']+)["']/g]; +for (const file of mdxFiles) { + const source = fs.readFileSync(file, 'utf8'); + for (const pattern of hrefPatterns) { + for (const match of source.matchAll(pattern)) { + const route = normalizeDocsRoute(match[1], file); + if (!route || routes.has(route) || redirectSources.has(route)) continue; + + const relativeTarget = path.resolve(path.dirname(file), match[1].split(/[?#]/, 1)[0]); + if (fs.existsSync(relativeTarget)) continue; + errors.push(`${path.relative(repoRoot, file)} links to missing route ${route}`); + } + } +} + +for (const option of config.contextual?.options || []) { + if (!option || typeof option !== 'object') continue; + const href = String(option.href || ''); + if (href.startsWith('/') && !routes.has(href) && !redirectSources.has(href)) { + errors.push(`Contextual link is missing: ${href}`); + } +} + +if (config.appearance?.default !== 'light') { + errors.push('appearance.default must be light to match the product color-scheme default'); +} +if (config.appearance?.strict === true) { + errors.push('appearance.strict must be false so docs can toggle light/dark like the app'); +} +if (config.background?.color?.light !== '#F5F4F1') { + errors.push('background.color.light must be Bone #F5F4F1'); +} +if (config.background?.color?.dark !== '#050505') { + errors.push('background.color.dark must stay #050505'); +} +if (config.styling?.codeblocks !== 'system') { + errors.push('styling.codeblocks must be system so code follows the site theme'); +} +if (config.seo?.metatags?.['color-scheme'] !== 'light dark') { + errors.push('seo.metatags.color-scheme must advertise both light and dark'); +} + +if (errors.length > 0) { + console.error(`Docs route validation failed:\n- ${[...new Set(errors)].join('\n- ')}`); + process.exit(1); +} + +console.log(`Docs routes valid: ${pageIds.length} navigation pages, ${redirects.length} redirects, ${mdxFiles.length} MDX files.`); From 5181c47656eab919e12b692e301df4f8c31df4ee Mon Sep 17 00:00:00 2001 From: Chris Leal <184772742+chrisjleal@users.noreply.github.com> Date: Sun, 27 Sep 2026 21:40:57 -0500 Subject: [PATCH 5/6] Reuse existing setup skills and consolidate client installation Mintlify-Source: dashboard-editor --- mcp/setup.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mcp/setup.mdx b/mcp/setup.mdx index 56bcf5a..1da9740 100644 --- a/mcp/setup.mdx +++ b/mcp/setup.mdx @@ -5,6 +5,8 @@ description: "Add the Proofable MCP endpoint to any MCP client, then connect and icon: plug --- +import ConnectHosts from '/snippets/connect-hosts.mdx'; + Give your AI the existing [Proofable setup skill](https://raw.githubusercontent.com/proofable/mcp/main/skills/proofable-setup/SKILL.md), or choose your client below. @@ -13,8 +15,6 @@ https://raw.githubusercontent.com/proofable/mcp/main/skills/proofable-setup/SKIL ## Connect your client -import ConnectHosts from '/snippets/connect-hosts.mdx'; - To bring an agent you already use, follow [Import an agent](/agents/import). From a35b68d4fd6cfdf9a6cdc23364eb8136b13c8f97 Mon Sep 17 00:00:00 2001 From: Chris Leal <184772742+chrisjleal@users.noreply.github.com> Date: Sun, 27 Sep 2026 21:41:10 -0500 Subject: [PATCH 6/6] Update the existing install redirect guard