diff --git a/README.md b/README.md index 11028c6..c0d6153 100644 --- a/README.md +++ b/README.md @@ -7,20 +7,27 @@ This repo installs Baserow's MCP server and a short skill that teaches the agent ## Server URL - **baserow.io**: `https://api.baserow.io/mcp` -- **Self-hosted**: your backend URL followed by `/mcp`, for example `https://baserow.example.com/mcp`. Copy it from Settings → MCP server in Baserow. Sign-in needs `BASEROW_MCP_OAUTH_ENABLED=true` on the backend; it's off by default. Your reverse proxy must send `/mcp`, `/oauth/`, `/.well-known/oauth-authorization-server` and `/.well-known/oauth-protected-resource` to the backend. See [MCP OAuth sign-in for self-hosted Baserow](https://github.com/baserow/baserow/blob/develop/docs/installation/mcp-oauth.md). +- **Self-hosted**: your backend URL followed by `/mcp`, for example `https://baserow.example.com/mcp`. Copy it from My settings → MCP server in Baserow. The examples below use the baserow.io URL. On self-hosted Baserow, use your own URL instead. ## Quick start -This works for most agents: - ``` -npx add-mcp https://api.baserow.io/mcp -n baserow +npx add-mcp https://api.baserow.io/mcp -n baserow -g npx skills add baserow/baserow-mcp ``` -`add-mcp` finds the agents you have installed and adds the server to each. Add `-a ` to pick one, or `-g` to install globally. `skills add` copies the `baserow-basics` skill into your agents' skill folders. +`add-mcp` finds the agents you have installed and asks which ones to add Baserow to. Untick Claude Desktop if it's listed and add Baserow there as a [custom connector](#claudeai-and-claude-desktop) instead. `skills add` copies the `baserow-basics` skill into your agents' skill folders. On self-hosted Baserow, run `skills add` too, since only the plugins below include the skill. + +## Self-hosted requirements + +- A Baserow version newer than 2.4.1. Older versions don't have `/mcp` or sign-in; use an [endpoint key](#endpoint-keys) there. +- `BASEROW_MCP_OAUTH_ENABLED=true` on the backend. It's off by default. +- A public HTTPS backend URL, so hosted apps like Claude.ai and ChatGPT can reach it, and outbound HTTPS from the backend, which fetches the apps' client metadata. +- Your reverse proxy sends `/mcp` and paths under `/mcp/`, `/oauth/`, `/.well-known/oauth-authorization-server` and `/.well-known/oauth-protected-resource` to the backend. Match `/mcp` exactly, not as a prefix: the frontend serves the sign-in page at `/mcp-authorize/`. + +See [MCP OAuth sign-in for self-hosted Baserow](https://github.com/baserow/baserow/blob/develop/docs/installation/mcp-oauth.md). ## Install per agent @@ -29,14 +36,16 @@ npx skills add baserow/baserow-mcp | Claude Code | [Plugin](#claude-code) | Same plugin, enter your URL when asked | | Claude.ai, Claude Desktop | [Custom connector](#claudeai-and-claude-desktop) | Same, with your URL | | ChatGPT | [Custom MCP server](#chatgpt) | Same, with your URL | -| Codex, Cursor, VS Code | `npx plugins add baserow/baserow-mcp` | [Codex](#codex) or [config file](#config-files) | -| GitHub Copilot CLI | `npx plugins add baserow/baserow-mcp` | `copilot mcp add --transport http baserow ` | -| Gemini CLI | `gemini extensions install https://github.com/baserow/baserow-mcp` | `gemini mcp add -t http baserow ` | -| Goose | [Add to Goose](goose://extension?url=https%3A%2F%2Fapi.baserow.io%2Fmcp&type=streamable_http&id=baserow&name=Baserow) | [Quick start](#quick-start) with your URL | +| Codex | [Plugin](#codex) | `codex mcp add baserow --url ` | +| GitHub Copilot CLI | [Plugin](#github-copilot-cli) | [Server only](#github-copilot-cli) | +| Gemini CLI | `gemini extensions install https://github.com/baserow/baserow-mcp` | `gemini mcp add -s user -t http baserow ` | +| Cursor | [Add to Cursor](https://cursor.com/install-mcp?name=baserow&config=eyJ1cmwiOiJodHRwczovL2FwaS5iYXNlcm93LmlvL21jcCJ9) | [Config file](#config-files) | +| VS Code | [Add to VS Code](https://vscode.dev/redirect/mcp/install?name=baserow&config=%7B%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fapi.baserow.io%2Fmcp%22%7D) | [Config file](#config-files) | +| Goose | [Add to Goose](https://goose-docs.ai/extension?url=https%3A%2F%2Fapi.baserow.io%2Fmcp&type=streamable_http&id=baserow&name=Baserow) | [Quick start](#quick-start) with your URL | | OpenCode, Windsurf, Zed | [Config file](#config-files) | Same, with your URL | | Any other agent | [Quick start](#quick-start) | Same, with your URL | -The Codex, Cursor, VS Code, Copilot and Gemini plugins always point at baserow.io, so on self-hosted Baserow add only the server for those agents. +The Codex, Copilot and Gemini plugins and the Add to buttons always point at baserow.io, so on self-hosted Baserow add only the server for those agents. ### Claude Code @@ -45,7 +54,7 @@ The Codex, Cursor, VS Code, Copilot and Gemini plugins always point at baserow.i /plugin install baserow@baserow ``` -Keep the default URL when asked, or enter your self-hosted URL. To change it later, run `/plugin configure baserow@baserow` in Claude Code. Then run `/mcp`, pick `plugin:baserow:baserow` and sign in. +When it asks for the URL, press Esc to keep baserow.io, or enter your self-hosted URL. To change it later, run `/plugin configure baserow@baserow`. ### Claude.ai and Claude Desktop @@ -58,19 +67,29 @@ Open Plugins, select the plus button, then Add custom MCP server, and enter your ### Codex ``` -codex mcp add baserow --url https://api.baserow.io/mcp +codex plugin marketplace add baserow/baserow-mcp +codex plugin add baserow@baserow codex mcp login baserow ``` +### GitHub Copilot CLI + +``` +copilot plugin marketplace add baserow/baserow-mcp +copilot plugin install baserow@baserow +``` + +On self-hosted Baserow, add only the server: `copilot mcp add --transport http baserow `. If the plugin is installed, uninstall it first with `copilot plugin uninstall baserow`; its server replaces yours without a warning. + ### Config files To add only the server, without the plugin: -- Cursor: [Add to Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=baserow&config=eyJ1cmwiOiJodHRwczovL2FwaS5iYXNlcm93LmlvL21jcCJ9), or `~/.cursor/mcp.json`: +- Cursor, `~/.cursor/mcp.json`: ```json { "mcpServers": { "baserow": { "url": "https://api.baserow.io/mcp" } } } ``` -- VS Code: [Add to VS Code](vscode:mcp/install?%7B%22name%22%3A%22baserow%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fapi.baserow.io%2Fmcp%22%7D), or `.vscode/mcp.json`: +- VS Code, `.vscode/mcp.json`: ```json { "servers": { "baserow": { "type": "http", "url": "https://api.baserow.io/mcp" } } } ``` @@ -87,36 +106,41 @@ To add only the server, without the plugin: { "context_servers": { "baserow": { "url": "https://api.baserow.io/mcp" } } } ``` -The Add to Cursor and Add to VS Code links use the baserow.io URL. - ## Signing in -The first time your agent uses Baserow, it opens a Baserow page in your browser. Sign in, pick the workspace and tick the tools the agent may use. Read tools let the agent browse databases, tables and rows; write tools let it create, update and delete rows. Only the read tools are ticked on a first connection. To change the workspace or tools, reconnect the server from your agent's MCP settings. +Your agent opens a Baserow page in your browser. Sign in, pick the workspace and tick the tools the agent may use. Read tools let the agent browse databases, tables and rows; write tools let it create, update and delete rows. Only the read tools are ticked on a first connection. + +Where the sign-in starts: + +- Claude Code: run `/mcp`, pick `plugin:baserow:baserow` and authenticate. +- Codex: `codex mcp login baserow`. +- Gemini CLI: `/mcp auth baserow`. +- Claude.ai, ChatGPT, Cursor and VS Code: the Connect or Sign in button next to the server. -Your connected apps are listed under Settings → MCP server in Baserow. Disconnect one there to revoke its access. +To change the workspace or tools, sign in again: re-authenticate the server in your agent, or disconnect the app under My settings → MCP server in Baserow and connect again. A plain reconnect keeps the old sign-in. ## Troubleshooting -- **A tool is missing, or no databases are listed.** The connection only has the workspace and tools you picked at sign-in. Reconnect and pick again. -- **The sign-in never starts, or the client says the server doesn't support OAuth or client registration.** On self-hosted Baserow, check that `BASEROW_MCP_OAUTH_ENABLED=true` is set and that the proxy routes the OAuth paths in [Server URL](#server-url). Older Baserow versions don't support OAuth for MCP; update Baserow or use an [endpoint key](#endpoint-keys). -- **The connection fails with a URL you typed.** Copy the URL exactly as Settings → MCP server shows it. It ends in `/mcp`, with no trailing slash. -- **Two Baserow servers in Claude Code.** If you also added a `baserow` server by hand with the same URL, Claude Code shows only that one and hides the plugin's. Keep one: `claude mcp remove baserow` removes the hand-added server. +- **A tool is missing, or no databases are listed.** The connection only has the workspace and tools you picked at sign-in. Sign in again as described above. Tools that change tables or fields aren't available over MCP. +- **The sign-in never starts, or the client says the server doesn't support OAuth or client registration.** On self-hosted Baserow, check the [self-hosted requirements](#self-hosted-requirements). +- **The connection fails with a URL you typed.** Copy the URL exactly as My settings → MCP server shows it. It ends in `/mcp`, with no trailing slash. +- **Two Baserow servers in Claude Code.** If `claude mcp list` shows the plugin's `plugin:baserow:baserow` and a `baserow` server you added by hand, keep the one that connects. `claude plugin disable baserow@baserow` turns off the plugin's; `claude mcp remove baserow` removes yours. ### Endpoint keys -Older Baserow versions only accept an endpoint key, and the plugin doesn't set this up. Copy the endpoint URL from Settings → MCP server. It ends in `/sse` and contains your key. Add it by hand: +Baserow 2.4.1 and older only accept an endpoint key, and the plugin doesn't set this up. Copy the endpoint URL from My settings → MCP server. It ends in `/sse` and contains your key. Add it by hand to an agent that supports SSE servers, such as Claude Code, Cursor or VS Code; Codex and Goose don't: ``` -npx add-mcp "" -t sse -n baserow -g +npx add-mcp "" -t sse -n baserow -g -a claude-code ``` -Baserow versions with OAuth for MCP also accept the key as a header on `/mcp`, which is useful for scripts: +Newer versions also accept the key as a header on `/mcp`, which is useful for scripts: ``` -npx add-mcp https://baserow.example.com/mcp -n baserow -g -h "Authorization: Bearer " +npx add-mcp https://baserow.example.com/mcp -n baserow -g -a claude-code -h "Authorization: Bearer " ``` -The key gives full access to its workspace. `-g` writes it to your global agent config instead of the current folder, so it can't end up in a commit. +The key gives full access to its workspace. `-g` writes it to your user config instead of the current folder, so it can't end up in a commit. ## Privacy and support @@ -128,7 +152,7 @@ The key gives full access to its workspace. `-g` writes it to your global agent Run `npm install`, then `npm test` and `npm run check`. CI runs the same checks. -The Claude Code plugin declares its server in `.claude-plugin/plugin.json`, so it can use the URL you enter. `.mcp.json` holds the baserow.io URL for other tools that read Claude-format plugins, such as GitHub Copilot CLI. +The Claude Code plugin declares its server in `.claude-plugin/plugin.json`, so it can use the URL you enter. `.mcp.json` holds the baserow.io URL for other tools that read Claude-format plugins, such as GitHub Copilot CLI and Cursor. ## License diff --git a/SECURITY.md b/SECURITY.md index 18067b6..f67ed46 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -10,5 +10,5 @@ This repo holds manifests and a skill that point AI agents at Baserow's MCP serv - Each connection reaches one workspace and only the tools you tick at sign-in. Tick the read tools only unless the agent needs to change data. - Data in your tables reaches the agent as tool output. A row can contain text written to steer the agent (prompt injection), so review what the agent wants to change before you approve it. -- Disconnect apps you no longer use under Settings → MCP server in Baserow. +- Disconnect apps you no longer use under My settings → MCP server in Baserow. - Endpoint keys give full access to their workspace. Keep them out of repos; the README's `add-mcp` commands use `-g` for this reason. diff --git a/scripts/check-manifests.mjs b/scripts/check-manifests.mjs index ff42870..dac6338 100644 --- a/scripts/check-manifests.mjs +++ b/scripts/check-manifests.mjs @@ -134,20 +134,20 @@ export function checkSync(m) { return errors; } -// Returns the server URL an install link in the README points at. -function linkUrl(link) { - const parsed = new URL(link); - if (parsed.protocol === "cursor:") { - return JSON.parse(Buffer.from(parsed.searchParams.get("config") ?? "", "base64").toString()).url; - } - if (parsed.protocol === "vscode:") return JSON.parse(decodeURIComponent(parsed.search.slice(1))).url; - if (parsed.protocol === "goose:") return parsed.searchParams.get("url"); - return undefined; -} +// Install links in the README, and how to read the server URL out of each. +// GitHub strips cursor:, vscode: and goose: links, so the README uses the https forms. +const INSTALL_LINKS = [ + ["cursor.com", "/install-mcp", (p) => JSON.parse(Buffer.from(p.get("config") ?? "", "base64").toString()).url], + ["vscode.dev", "/redirect/mcp/install", (p) => JSON.parse(p.get("config") ?? "").url], + ["goose-docs.ai", "/extension", (p) => p.get("url")], +]; + +const installLink = (parsed) => + INSTALL_LINKS.find(([host, pathname]) => parsed.hostname === host && parsed.pathname.replace(/\/$/, "") === pathname); export function checkReadme(text, saasUrl) { const errors = []; - for (let [url] of text.matchAll(/https:\/\/[^\s"'`<>()\]]+/g)) { + for (let [url] of text.matchAll(/https?:\/\/[^\s"'`<>()\]|*!]+/g)) { url = url.replace(/[.,;:]+$/, ""); let parsed; try { @@ -155,19 +155,22 @@ export function checkReadme(text, saasUrl) { } catch { continue; } - if (/\/mcp(\/|$)/.test(parsed.pathname) && !parsed.hostname.endsWith("example.com") && url !== saasUrl) { + const link = installLink(parsed); + if (link) { + let target; + try { + target = link[2](parsed.searchParams); + } catch (e) { + errors.push(`${README}: can't read install link ${url} (${e.message})`); + continue; + } + if (target !== saasUrl) errors.push(`${README}: install link ${url} points at ${target}, not ${saasUrl}`); + } else if (!parsed.hostname.endsWith("example.com") && /\/mcp(\/|$)/.test(parsed.pathname) && url !== saasUrl) { errors.push(`${README}: ${url} differs from the SaaS URL ${saasUrl}`); } } for (const [, link] of text.matchAll(/\]\(((?:cursor|vscode|goose):[^)\s]+)\)/g)) { - let url; - try { - url = linkUrl(link); - } catch (e) { - errors.push(`${README}: can't read install link ${link} (${e.message})`); - continue; - } - if (url !== saasUrl) errors.push(`${README}: install link ${link} points at ${url}, not ${saasUrl}`); + errors.push(`${README}: ${link} isn't clickable on GitHub; use the https install link`); } return errors; } diff --git a/scripts/check-manifests.test.mjs b/scripts/check-manifests.test.mjs index bf9d42d..c86b3c2 100644 --- a/scripts/check-manifests.test.mjs +++ b/scripts/check-manifests.test.mjs @@ -226,16 +226,17 @@ test("malformed marketplace plugins are reported without crashing", () => { }); const cursorLink = (url) => - `cursor://anysphere.cursor-deeplink/mcp/install?name=baserow&config=${Buffer.from(JSON.stringify({ url })).toString("base64")}`; -const vscodeLink = (url) => `vscode:mcp/install?${encodeURIComponent(JSON.stringify({ name: "baserow", type: "http", url }))}`; -const gooseLink = (url) => `goose://extension?url=${encodeURIComponent(url)}&id=baserow`; + `https://cursor.com/install-mcp?name=baserow&config=${Buffer.from(JSON.stringify({ url })).toString("base64")}`; +const vscodeLink = (url) => + `https://vscode.dev/redirect/mcp/install?name=baserow&config=${encodeURIComponent(JSON.stringify({ type: "http", url }))}`; +const gooseLink = (url) => `https://goose-docs.ai/extension?url=${encodeURIComponent(url)}&id=baserow`; test("a README that matches the SaaS URL passes", () => { const readme = [ `Use \`${URL}\` or \`https://baserow.example.com/mcp\`.`, "See [the docs](https://github.com/baserow/baserow/blob/develop/docs/installation/mcp-oauth.md).", `[Add to Cursor](${cursorLink(URL)})`, - `[Add to VS Code](${vscodeLink(URL)})`, + `| [Add to VS Code](${vscodeLink(URL)}) |`, `[Add to Goose](${gooseLink(URL)})`, ].join("\n"); assert.deepEqual(checkReadme(readme, URL), []); @@ -243,9 +244,23 @@ test("a README that matches the SaaS URL passes", () => { test("a stale URL or install link in the README is reported", () => { const old = "https://old.baserow.io/mcp"; - const readme = [`{ "url": "${old}" }`, `${URL}/`, `[a](${cursorLink(old)})`, `[b](${vscodeLink(old)})`, `[c](${gooseLink(old)})`].join("\n"); + const readme = [ + `{ "url": "${old}" }`, + `${URL}/`, + `**${old}**`, + `| ${old}|`, + `http://api.baserow.io/mcp`, + `[a](${cursorLink(old)})`, + `[b](${vscodeLink(old)})`, + `[c](${gooseLink(old)})`, + ].join("\n"); const errors = checkReadme(readme, URL); - assert.equal(errors.length, 5, errors.join("\n")); + assert.equal(errors.length, 8, errors.join("\n")); +}); + +test("app-scheme install links are reported, since GitHub strips them", () => { + const errors = checkReadme("[Add to Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=baserow)", URL); + assert.match(errors.join("\n"), /isn't clickable on GitHub/); }); test("a skill with CRLF line endings, a BOM and a quoted name passes", async (t) => {