feat(mcp): MCP integrations Phase 1 — HTTP/SSE MCP servers, tool permissions, pinchy-mcp plugin#298
Open
clemenshelm wants to merge 58 commits into
Open
feat(mcp): MCP integrations Phase 1 — HTTP/SSE MCP servers, tool permissions, pinchy-mcp plugin#298clemenshelm wants to merge 58 commits into
clemenshelm wants to merge 58 commits into
Conversation
clemenshelm
pushed a commit
that referenced
this pull request
May 7, 2026
Six CI failures from PR #298, all rooted in three issues: 1. **PUT body shape regression.** The unified discriminated-union endpoint `[{kind:"odoo"|"mcp", connectionId, ...}]` replaced the legacy `{connectionId, permissions}` shape. The Odoo `setAgentPermissions` helper, the email E2E spec, and the odoo-agent-chat response parser were still on the old shape. Updated all three to the new shape. 2. **Sync eager-cascaded permissions instead of letting drift surface at GET time.** The MCP E2E spec explicitly asserts "permission row still exists — drift is detected at read time, not eagerly deleted by sync" but the implementation cascade-deleted in the sync transaction. Removed the cascade; drift detection in `agents/[id]/integrations` GET now finds the stale rows and reports them. Added a defensive filter in `openclaw-config/build.ts` so drifted permissions aren't emitted into the runtime config. Also wired up a failure-audit + 502 path for `listMcpTools` errors (review feedback). `toolPrefix` now resolves from the preset registry rather than a stale `data.toolPrefix` field. 3. **Test infrastructure misses for the new table + general E2E picking up MCP specs.** Added `agent_mcp_tool_permissions` to the integration-suite truncate list and excluded `e2e/mcp/**` from the general playwright config. Format-only: `prettier --write` on `mcp-gate.test.ts`. Verified locally: format check ✓, tsc ✓, lint 0 errors, 3881 vitest tests pass.
clemenshelm
pushed a commit
that referenced
this pull request
May 11, 2026
Six CI failures from PR #298, all rooted in three issues: 1. **PUT body shape regression.** The unified discriminated-union endpoint `[{kind:"odoo"|"mcp", connectionId, ...}]` replaced the legacy `{connectionId, permissions}` shape. The Odoo `setAgentPermissions` helper, the email E2E spec, and the odoo-agent-chat response parser were still on the old shape. Updated all three to the new shape. 2. **Sync eager-cascaded permissions instead of letting drift surface at GET time.** The MCP E2E spec explicitly asserts "permission row still exists — drift is detected at read time, not eagerly deleted by sync" but the implementation cascade-deleted in the sync transaction. Removed the cascade; drift detection in `agents/[id]/integrations` GET now finds the stale rows and reports them. Added a defensive filter in `openclaw-config/build.ts` so drifted permissions aren't emitted into the runtime config. Also wired up a failure-audit + 502 path for `listMcpTools` errors (review feedback). `toolPrefix` now resolves from the preset registry rather than a stale `data.toolPrefix` field. 3. **Test infrastructure misses for the new table + general E2E picking up MCP specs.** Added `agent_mcp_tool_permissions` to the integration-suite truncate list and excluded `e2e/mcp/**` from the general playwright config. Format-only: `prettier --write` on `mcp-gate.test.ts`. Verified locally: format check ✓, tsc ✓, lint 0 errors, 3881 vitest tests pass.
f42c279 to
ec08025
Compare
clemenshelm
pushed a commit
that referenced
this pull request
May 11, 2026
Six CI failures from PR #298, all rooted in three issues: 1. **PUT body shape regression.** The unified discriminated-union endpoint `[{kind:"odoo"|"mcp", connectionId, ...}]` replaced the legacy `{connectionId, permissions}` shape. The Odoo `setAgentPermissions` helper, the email E2E spec, and the odoo-agent-chat response parser were still on the old shape. Updated all three to the new shape. 2. **Sync eager-cascaded permissions instead of letting drift surface at GET time.** The MCP E2E spec explicitly asserts "permission row still exists — drift is detected at read time, not eagerly deleted by sync" but the implementation cascade-deleted in the sync transaction. Removed the cascade; drift detection in `agents/[id]/integrations` GET now finds the stale rows and reports them. Added a defensive filter in `openclaw-config/build.ts` so drifted permissions aren't emitted into the runtime config. Also wired up a failure-audit + 502 path for `listMcpTools` errors (review feedback). `toolPrefix` now resolves from the preset registry rather than a stale `data.toolPrefix` field. 3. **Test infrastructure misses for the new table + general E2E picking up MCP specs.** Added `agent_mcp_tool_permissions` to the integration-suite truncate list and excluded `e2e/mcp/**` from the general playwright config. Format-only: `prettier --write` on `mcp-gate.test.ts`. Verified locally: format check ✓, tsc ✓, lint 0 errors, 3881 vitest tests pass.
ec08025 to
eb2aafb
Compare
This was referenced May 11, 2026
clemenshelm
pushed a commit
that referenced
this pull request
May 12, 2026
Six CI failures from PR #298, all rooted in three issues: 1. **PUT body shape regression.** The unified discriminated-union endpoint `[{kind:"odoo"|"mcp", connectionId, ...}]` replaced the legacy `{connectionId, permissions}` shape. The Odoo `setAgentPermissions` helper, the email E2E spec, and the odoo-agent-chat response parser were still on the old shape. Updated all three to the new shape. 2. **Sync eager-cascaded permissions instead of letting drift surface at GET time.** The MCP E2E spec explicitly asserts "permission row still exists — drift is detected at read time, not eagerly deleted by sync" but the implementation cascade-deleted in the sync transaction. Removed the cascade; drift detection in `agents/[id]/integrations` GET now finds the stale rows and reports them. Added a defensive filter in `openclaw-config/build.ts` so drifted permissions aren't emitted into the runtime config. Also wired up a failure-audit + 502 path for `listMcpTools` errors (review feedback). `toolPrefix` now resolves from the preset registry rather than a stale `data.toolPrefix` field. 3. **Test infrastructure misses for the new table + general E2E picking up MCP specs.** Added `agent_mcp_tool_permissions` to the integration-suite truncate list and excluded `e2e/mcp/**` from the general playwright config. Format-only: `prettier --write` on `mcp-gate.test.ts`. Verified locally: format check ✓, tsc ✓, lint 0 errors, 3881 vitest tests pass.
eb2aafb to
2ccd808
Compare
clemenshelm
pushed a commit
that referenced
this pull request
May 12, 2026
PR #298's UI refactor split the integration type picker out of the modal into a dedicated `/settings/integrations/new` page (commit 82475f1). The "Add Integration" button became a `<Link>`, not a modal trigger — but the Odoo wizard E2E tests still expected a dialog to open directly on click. The happy-path test timed out after 2 minutes waiting for a dialog that won't appear at the old URL. Update both Odoo wizard tests to follow the new flow: 1. Click the "Add Integration" link (now navigates) 2. Land on /settings/integrations/new 3. Click the Odoo tile — opens the wizard dialog with `initialType=odoo` so the connect step renders straight away 4. Resume the existing wizard flow Other E2E specs were checked — only odoo-wizard.spec.ts uses the Add-Integration click path; MCP / Email / Web Search specs all seed via REST so they're unaffected.
clemenshelm
pushed a commit
that referenced
this pull request
May 13, 2026
Six CI failures from PR #298, all rooted in three issues: 1. **PUT body shape regression.** The unified discriminated-union endpoint `[{kind:"odoo"|"mcp", connectionId, ...}]` replaced the legacy `{connectionId, permissions}` shape. The Odoo `setAgentPermissions` helper, the email E2E spec, and the odoo-agent-chat response parser were still on the old shape. Updated all three to the new shape. 2. **Sync eager-cascaded permissions instead of letting drift surface at GET time.** The MCP E2E spec explicitly asserts "permission row still exists — drift is detected at read time, not eagerly deleted by sync" but the implementation cascade-deleted in the sync transaction. Removed the cascade; drift detection in `agents/[id]/integrations` GET now finds the stale rows and reports them. Added a defensive filter in `openclaw-config/build.ts` so drifted permissions aren't emitted into the runtime config. Also wired up a failure-audit + 502 path for `listMcpTools` errors (review feedback). `toolPrefix` now resolves from the preset registry rather than a stale `data.toolPrefix` field. 3. **Test infrastructure misses for the new table + general E2E picking up MCP specs.** Added `agent_mcp_tool_permissions` to the integration-suite truncate list and excluded `e2e/mcp/**` from the general playwright config. Format-only: `prettier --write` on `mcp-gate.test.ts`. Verified locally: format check ✓, tsc ✓, lint 0 errors, 3881 vitest tests pass.
65ee7d2 to
f2354aa
Compare
clemenshelm
pushed a commit
that referenced
this pull request
May 13, 2026
PR #298's UI refactor split the integration type picker out of the modal into a dedicated `/settings/integrations/new` page (commit 82475f1). The "Add Integration" button became a `<Link>`, not a modal trigger — but the Odoo wizard E2E tests still expected a dialog to open directly on click. The happy-path test timed out after 2 minutes waiting for a dialog that won't appear at the old URL. Update both Odoo wizard tests to follow the new flow: 1. Click the "Add Integration" link (now navigates) 2. Land on /settings/integrations/new 3. Click the Odoo tile — opens the wizard dialog with `initialType=odoo` so the connect step renders straight away 4. Resume the existing wizard flow Other E2E specs were checked — only odoo-wizard.spec.ts uses the Add-Integration click path; MCP / Email / Web Search specs all seed via REST so they're unaffected.
clemenshelm
pushed a commit
that referenced
this pull request
May 13, 2026
Six CI failures from PR #298, all rooted in three issues: 1. **PUT body shape regression.** The unified discriminated-union endpoint `[{kind:"odoo"|"mcp", connectionId, ...}]` replaced the legacy `{connectionId, permissions}` shape. The Odoo `setAgentPermissions` helper, the email E2E spec, and the odoo-agent-chat response parser were still on the old shape. Updated all three to the new shape. 2. **Sync eager-cascaded permissions instead of letting drift surface at GET time.** The MCP E2E spec explicitly asserts "permission row still exists — drift is detected at read time, not eagerly deleted by sync" but the implementation cascade-deleted in the sync transaction. Removed the cascade; drift detection in `agents/[id]/integrations` GET now finds the stale rows and reports them. Added a defensive filter in `openclaw-config/build.ts` so drifted permissions aren't emitted into the runtime config. Also wired up a failure-audit + 502 path for `listMcpTools` errors (review feedback). `toolPrefix` now resolves from the preset registry rather than a stale `data.toolPrefix` field. 3. **Test infrastructure misses for the new table + general E2E picking up MCP specs.** Added `agent_mcp_tool_permissions` to the integration-suite truncate list and excluded `e2e/mcp/**` from the general playwright config. Format-only: `prettier --write` on `mcp-gate.test.ts`. Verified locally: format check ✓, tsc ✓, lint 0 errors, 3881 vitest tests pass.
f2354aa to
7bca2f4
Compare
clemenshelm
pushed a commit
that referenced
this pull request
May 13, 2026
PR #298's UI refactor split the integration type picker out of the modal into a dedicated `/settings/integrations/new` page (commit 82475f1). The "Add Integration" button became a `<Link>`, not a modal trigger — but the Odoo wizard E2E tests still expected a dialog to open directly on click. The happy-path test timed out after 2 minutes waiting for a dialog that won't appear at the old URL. Update both Odoo wizard tests to follow the new flow: 1. Click the "Add Integration" link (now navigates) 2. Land on /settings/integrations/new 3. Click the Odoo tile — opens the wizard dialog with `initialType=odoo` so the connect step renders straight away 4. Resume the existing wizard flow Other E2E specs were checked — only odoo-wizard.spec.ts uses the Add-Integration click path; MCP / Email / Web Search specs all seed via REST so they're unaffected.
clemenshelm
pushed a commit
that referenced
this pull request
May 13, 2026
Six CI failures from PR #298, all rooted in three issues: 1. **PUT body shape regression.** The unified discriminated-union endpoint `[{kind:"odoo"|"mcp", connectionId, ...}]` replaced the legacy `{connectionId, permissions}` shape. The Odoo `setAgentPermissions` helper, the email E2E spec, and the odoo-agent-chat response parser were still on the old shape. Updated all three to the new shape. 2. **Sync eager-cascaded permissions instead of letting drift surface at GET time.** The MCP E2E spec explicitly asserts "permission row still exists — drift is detected at read time, not eagerly deleted by sync" but the implementation cascade-deleted in the sync transaction. Removed the cascade; drift detection in `agents/[id]/integrations` GET now finds the stale rows and reports them. Added a defensive filter in `openclaw-config/build.ts` so drifted permissions aren't emitted into the runtime config. Also wired up a failure-audit + 502 path for `listMcpTools` errors (review feedback). `toolPrefix` now resolves from the preset registry rather than a stale `data.toolPrefix` field. 3. **Test infrastructure misses for the new table + general E2E picking up MCP specs.** Added `agent_mcp_tool_permissions` to the integration-suite truncate list and excluded `e2e/mcp/**` from the general playwright config. Format-only: `prettier --write` on `mcp-gate.test.ts`. Verified locally: format check ✓, tsc ✓, lint 0 errors, 3881 vitest tests pass.
7bca2f4 to
9b78870
Compare
clemenshelm
pushed a commit
that referenced
this pull request
May 13, 2026
PR #298's UI refactor split the integration type picker out of the modal into a dedicated `/settings/integrations/new` page (commit 82475f1). The "Add Integration" button became a `<Link>`, not a modal trigger — but the Odoo wizard E2E tests still expected a dialog to open directly on click. The happy-path test timed out after 2 minutes waiting for a dialog that won't appear at the old URL. Update both Odoo wizard tests to follow the new flow: 1. Click the "Add Integration" link (now navigates) 2. Land on /settings/integrations/new 3. Click the Odoo tile — opens the wizard dialog with `initialType=odoo` so the connect step renders straight away 4. Resume the existing wizard flow Other E2E specs were checked — only odoo-wizard.spec.ts uses the Add-Integration click path; MCP / Email / Web Search specs all seed via REST so they're unaffected.
clemenshelm
pushed a commit
that referenced
this pull request
May 13, 2026
Six CI failures from PR #298, all rooted in three issues: 1. **PUT body shape regression.** The unified discriminated-union endpoint `[{kind:"odoo"|"mcp", connectionId, ...}]` replaced the legacy `{connectionId, permissions}` shape. The Odoo `setAgentPermissions` helper, the email E2E spec, and the odoo-agent-chat response parser were still on the old shape. Updated all three to the new shape. 2. **Sync eager-cascaded permissions instead of letting drift surface at GET time.** The MCP E2E spec explicitly asserts "permission row still exists — drift is detected at read time, not eagerly deleted by sync" but the implementation cascade-deleted in the sync transaction. Removed the cascade; drift detection in `agents/[id]/integrations` GET now finds the stale rows and reports them. Added a defensive filter in `openclaw-config/build.ts` so drifted permissions aren't emitted into the runtime config. Also wired up a failure-audit + 502 path for `listMcpTools` errors (review feedback). `toolPrefix` now resolves from the preset registry rather than a stale `data.toolPrefix` field. 3. **Test infrastructure misses for the new table + general E2E picking up MCP specs.** Added `agent_mcp_tool_permissions` to the integration-suite truncate list and excluded `e2e/mcp/**` from the general playwright config. Format-only: `prettier --write` on `mcp-gate.test.ts`. Verified locally: format check ✓, tsc ✓, lint 0 errors, 3881 vitest tests pass.
clemenshelm
pushed a commit
that referenced
this pull request
May 13, 2026
PR #298's UI refactor split the integration type picker out of the modal into a dedicated `/settings/integrations/new` page (commit 82475f1). The "Add Integration" button became a `<Link>`, not a modal trigger — but the Odoo wizard E2E tests still expected a dialog to open directly on click. The happy-path test timed out after 2 minutes waiting for a dialog that won't appear at the old URL. Update both Odoo wizard tests to follow the new flow: 1. Click the "Add Integration" link (now navigates) 2. Land on /settings/integrations/new 3. Click the Odoo tile — opens the wizard dialog with `initialType=odoo` so the connect step renders straight away 4. Resume the existing wizard flow Other E2E specs were checked — only odoo-wizard.spec.ts uses the Add-Integration click path; MCP / Email / Web Search specs all seed via REST so they're unaffected.
9b78870 to
aff36a5
Compare
clemenshelm
pushed a commit
that referenced
this pull request
May 18, 2026
Six CI failures from PR #298, all rooted in three issues: 1. **PUT body shape regression.** The unified discriminated-union endpoint `[{kind:"odoo"|"mcp", connectionId, ...}]` replaced the legacy `{connectionId, permissions}` shape. The Odoo `setAgentPermissions` helper, the email E2E spec, and the odoo-agent-chat response parser were still on the old shape. Updated all three to the new shape. 2. **Sync eager-cascaded permissions instead of letting drift surface at GET time.** The MCP E2E spec explicitly asserts "permission row still exists — drift is detected at read time, not eagerly deleted by sync" but the implementation cascade-deleted in the sync transaction. Removed the cascade; drift detection in `agents/[id]/integrations` GET now finds the stale rows and reports them. Added a defensive filter in `openclaw-config/build.ts` so drifted permissions aren't emitted into the runtime config. Also wired up a failure-audit + 502 path for `listMcpTools` errors (review feedback). `toolPrefix` now resolves from the preset registry rather than a stale `data.toolPrefix` field. 3. **Test infrastructure misses for the new table + general E2E picking up MCP specs.** Added `agent_mcp_tool_permissions` to the integration-suite truncate list and excluded `e2e/mcp/**` from the general playwright config. Format-only: `prettier --write` on `mcp-gate.test.ts`. Verified locally: format check ✓, tsc ✓, lint 0 errors, 3881 vitest tests pass.
clemenshelm
pushed a commit
that referenced
this pull request
May 18, 2026
PR #298's UI refactor split the integration type picker out of the modal into a dedicated `/settings/integrations/new` page (commit 82475f1). The "Add Integration" button became a `<Link>`, not a modal trigger — but the Odoo wizard E2E tests still expected a dialog to open directly on click. The happy-path test timed out after 2 minutes waiting for a dialog that won't appear at the old URL. Update both Odoo wizard tests to follow the new flow: 1. Click the "Add Integration" link (now navigates) 2. Land on /settings/integrations/new 3. Click the Odoo tile — opens the wizard dialog with `initialType=odoo` so the connect step renders straight away 4. Resume the existing wizard flow Other E2E specs were checked — only odoo-wizard.spec.ts uses the Add-Integration click path; MCP / Email / Web Search specs all seed via REST so they're unaffected.
aff36a5 to
0acd70c
Compare
clemenshelm
pushed a commit
that referenced
this pull request
May 18, 2026
Main added a new email-dispatch E2E test (line 265+) during the
merge gap that sends the legacy `{connectionId, permissions}` PUT
body. PR #298 changed the route to require the unified array shape
with `kind: "odoo"|"mcp"` discriminator, so the test was getting a
400 on the permissions grant.
Update the dispatch probe's beforeAll to use the new shape — same
pattern the other email + odoo E2E specs already follow:
[{ kind: "odoo", connectionId, entries: [...] }]
Email/Google connections share the agentConnectionPermissions table
with Odoo so they ride the same discriminator.
clemenshelm
pushed a commit
that referenced
this pull request
May 19, 2026
Six CI failures from PR #298, all rooted in three issues: 1. **PUT body shape regression.** The unified discriminated-union endpoint `[{kind:"odoo"|"mcp", connectionId, ...}]` replaced the legacy `{connectionId, permissions}` shape. The Odoo `setAgentPermissions` helper, the email E2E spec, and the odoo-agent-chat response parser were still on the old shape. Updated all three to the new shape. 2. **Sync eager-cascaded permissions instead of letting drift surface at GET time.** The MCP E2E spec explicitly asserts "permission row still exists — drift is detected at read time, not eagerly deleted by sync" but the implementation cascade-deleted in the sync transaction. Removed the cascade; drift detection in `agents/[id]/integrations` GET now finds the stale rows and reports them. Added a defensive filter in `openclaw-config/build.ts` so drifted permissions aren't emitted into the runtime config. Also wired up a failure-audit + 502 path for `listMcpTools` errors (review feedback). `toolPrefix` now resolves from the preset registry rather than a stale `data.toolPrefix` field. 3. **Test infrastructure misses for the new table + general E2E picking up MCP specs.** Added `agent_mcp_tool_permissions` to the integration-suite truncate list and excluded `e2e/mcp/**` from the general playwright config. Format-only: `prettier --write` on `mcp-gate.test.ts`. Verified locally: format check ✓, tsc ✓, lint 0 errors, 3881 vitest tests pass.
fb5510c to
a41a9cb
Compare
clemenshelm
pushed a commit
that referenced
this pull request
May 19, 2026
PR #298's UI refactor split the integration type picker out of the modal into a dedicated `/settings/integrations/new` page (commit 82475f1). The "Add Integration" button became a `<Link>`, not a modal trigger — but the Odoo wizard E2E tests still expected a dialog to open directly on click. The happy-path test timed out after 2 minutes waiting for a dialog that won't appear at the old URL. Update both Odoo wizard tests to follow the new flow: 1. Click the "Add Integration" link (now navigates) 2. Land on /settings/integrations/new 3. Click the Odoo tile — opens the wizard dialog with `initialType=odoo` so the connect step renders straight away 4. Resume the existing wizard flow Other E2E specs were checked — only odoo-wizard.spec.ts uses the Add-Integration click path; MCP / Email / Web Search specs all seed via REST so they're unaffected.
clemenshelm
pushed a commit
that referenced
this pull request
May 19, 2026
Main added a new email-dispatch E2E test (line 265+) during the
merge gap that sends the legacy `{connectionId, permissions}` PUT
body. PR #298 changed the route to require the unified array shape
with `kind: "odoo"|"mcp"` discriminator, so the test was getting a
400 on the permissions grant.
Update the dispatch probe's beforeAll to use the new shape — same
pattern the other email + odoo E2E specs already follow:
[{ kind: "odoo", connectionId, entries: [...] }]
Email/Google connections share the agentConnectionPermissions table
with Odoo so they ride the same discriminator.
added 30 commits
June 23, 2026 12:32
Six CI failures from PR #298, all rooted in three issues: 1. **PUT body shape regression.** The unified discriminated-union endpoint `[{kind:"odoo"|"mcp", connectionId, ...}]` replaced the legacy `{connectionId, permissions}` shape. The Odoo `setAgentPermissions` helper, the email E2E spec, and the odoo-agent-chat response parser were still on the old shape. Updated all three to the new shape. 2. **Sync eager-cascaded permissions instead of letting drift surface at GET time.** The MCP E2E spec explicitly asserts "permission row still exists — drift is detected at read time, not eagerly deleted by sync" but the implementation cascade-deleted in the sync transaction. Removed the cascade; drift detection in `agents/[id]/integrations` GET now finds the stale rows and reports them. Added a defensive filter in `openclaw-config/build.ts` so drifted permissions aren't emitted into the runtime config. Also wired up a failure-audit + 502 path for `listMcpTools` errors (review feedback). `toolPrefix` now resolves from the preset registry rather than a stale `data.toolPrefix` field. 3. **Test infrastructure misses for the new table + general E2E picking up MCP specs.** Added `agent_mcp_tool_permissions` to the integration-suite truncate list and excluded `e2e/mcp/**` from the general playwright config. Format-only: `prettier --write` on `mcp-gate.test.ts`. Verified locally: format check ✓, tsc ✓, lint 0 errors, 3881 vitest tests pass.
Replace the single "Generic MCP" entry plus internal preset dropdown with four first-class cards in the Add Integration dialog: GitHub, Notion, Linear, and Custom MCP server. The named-preset cards lock in their preset, URL, and transport so the user only sees the token field plus a Test connection button — the URL never varies for these providers. The Custom card preserves the original flow (preset selector + URL + transport + token) for arbitrary MCP servers. Why: the previous UX leaked the technical abstraction — discovering that GitHub support lives behind "Generic MCP" required clicking into the dialog and finding a dropdown. The new layout matches how end-users mentally model integrations: one card per provider. Other changes: - Add monochrome brand SVGs for GitHub, Notion, Linear from Simple Icons (CC0). Update Google to its monochrome mark so the row stays visually coherent now that there are seven cards. - Lift "Test connection" out of the Generic-only branch — it's useful for every MCP flow as a pre-save smoke check. - Backend untouched: the POST body still ships `type: "mcp"` + `preset: github|notion|linear|generic`, audit log, permission card, and pinchy-mcp plugin all unchanged.
Lift the integration picker out of the modal and into its own page, mirroring the New-Agent template selector pattern. The 7-tile list was already overflowing the modal vertically; once we add Slack, Jira, and other Phase-2 integrations the modal will be unusable. A dedicated page scales linearly and matches how every comparable platform (Zapier, Make, n8n, Vercel) handles their integration directories above the ~10-item modal-pain threshold. Layout: - /settings/integrations/new is the picker entry point - 2-col mobile / 3-col sm+ responsive grid via the same Card pattern the New-Agent picker uses - Custom MCP server tile is visually separated below a divider as the advanced catch-all option - AddIntegrationDialog opens as an overlay on top of the picker with the selected type pre-filled, preserving the existing per-type connect flow with zero changes to its internals - On success the dialog router-pushes back to ?tab=integrations Refactor: - INTEGRATION_TYPES + isMcpType + MCP_TYPE_TO_PRESET extracted from add-integration-dialog.tsx into integration-types.tsx so the picker and the dialog share one source of truth - McpIcon moved to integration-icons.tsx alongside the other tile icons - AddIntegrationDialog.initialType widened from "google" to IntegrationTypeId so the picker can pre-select any tile - Settings "Add Integration" button becomes a Link to the new page Search and category filters intentionally deferred — they're best practice above ~12 items but the picker would just be empty chrome today. The layout leaves a header slot so they're trivial to add later.
Three independent bugs reported on the new GitHub integration flow: 1. The PAT URL `github.com/settings/tokens?type=beta` 301-redirects to `github.com/settings/personal-access-tokens` as of late 2025. Update the GitHub preset to the canonical URL. Also widen the suggested scopes to include Pull requests R/W, which the agent typically needs once it can read code. 2. `tokenInstructions` is markdown but was rendered as plain text — users saw literal asterisks and bracket-link syntax. Render via react-markdown + remark-gfm with tailwind-style overrides for links, bold, inline code, and ordered/unordered lists. External links open in a new tab so the dialog isn't navigated away mid- setup. 3. The connect-step dialog had no max-height — GitHub's MCP server discovers ~50 tools, which after Test Connection blew the dialog past the viewport in both directions. Add `max-h-[85vh] overflow-y-auto` on DialogContent so the dialog scrolls within the viewport, and cap the test-discovery tool list at `max-h-48 overflow-y-auto` so it doesn't dominate the dialog by itself. Adds react-markdown@10 as a direct dependency (was transient via @assistant-ui/react-markdown).
…evel Six new first-class MCP integrations, all with vendor-maintained production-grade servers and static-token authentication. Each ships with an up-to-date 2026 setup walkthrough that's been verified against the current vendor docs (the old GitHub `?type=beta` URL bug taught us that link-rot in tokenInstructions is its own quiet failure mode, so the preset-registry test now asserts every named preset still points at a canonical credential page). - **Atlassian** (Jira + Confluence) — one card, one token, both products. `mcp.atlassian.com/v1/mcp` with API-Token-Basic. Note the admin-enable requirement: the Atlassian org admin has to flip "API token" auth on in Rovo settings; the instruction copy calls this out up front. - **GitLab** — `gitlab.com/api/v4/mcp` for SaaS, `glpat-` PAT with `api` scope. Self-managed GitLab users route through the Custom MCP tile (different host). - **Stripe** — `mcp.stripe.com` with `rk_live_`/`rk_test_` restricted keys; the docs steer users to test mode first. - **Cloudflare** — `mcp.cloudflare.com/mcp` with an API token. Flag the "no IP filter" requirement because the MCP server rejects filtered tokens. - **Intercom** — `mcp.intercom.com/mcp` with a Developer Hub access token. Note US-workspaces-only restriction prominently. - **HighLevel** — `services.leadconnectorhq.com/mcp/` with a `pit-` Private Integration Token. Static-only by design — the rare enterprise SaaS where OAuth isn't even an option. Deferred to Phase 2 (OAuth-only on the MCP endpoint, can't ship as a static-token integration yet): HubSpot, Asana, Salesforce, Slack, Sentry cloud. Zendesk has no official MCP server. Plumbing changes: - 6 new brand SVG icons (Simple Icons CC0; HighLevel uses a generic megaphone glyph until they publish brand assets). - `mcpFormSchema` and the API route's `mcpBodySchema` both extend to cover the new preset enum values. - Plaintext scanner gets six new patterns: `glpat-`, `glptt-`, `rk_live_`/`rk_test_`, `sk_live_`/`sk_test_`, `pit-`. Atlassian/ Cloudflare/Intercom tokens are opaque (no recognizable prefix) — Pattern-B fetch contract + manifest `additionalProperties: false` remain the primary defenses there. Tests cover: every new preset id present in the registry, toolPrefix uniqueness across all presets, canonical token URLs in instructions, representative card flows (Atlassian admin-enable note, Stripe submit shape, HighLevel PIT phrasing), all 10 picker tiles visible when the MCP flag is on, all hidden when off. 4169 unit tests pass, tsc clean, lint 0 errors.
Major correction round after browser testing surfaced a confused Notion flow and a deeper validation pass found four more presets in trouble. - **Notion** — hosted MCP at `mcp.notion.com/mcp` is OAuth-only; our preset pointed at the wrong URL (`api.notion.com/mcp/`) AND tried to Bearer-authenticate a token type that endpoint rejects. The Installation Access Token (`ntn_…`) users see in Notion's UI is real but only works against the REST API or the local stdio server. Tracked in #339 — will ship as a non-MCP `pinchy-notion` plugin (n8n's pattern). - **GitLab** — hosted MCP at `gitlab.com/api/v4/mcp` is OAuth-only as of May 2026. PAT support is an open feature request (GitLab issue #586184) but not shipped. Tracked in #340 — will land when either upstream PAT support arrives or our Phase-2 OAuth ships. - **Linear** — URL migrated from `/sse` to `/mcp` (Streamable HTTP) in Feb 2026; the SSE endpoint is deprecated. Updated to `https://mcp.linear.app/mcp` with `transport: "http"`. - **Atlassian** — clarified that the Bearer flow only works for service-account API keys (personal user tokens require Basic auth, which Phase 1 doesn't speak). Token instructions now spell out both prerequisites: admin enables API-token auth + provision a service account. - **GitHub** — relaxed the "fine-grained only" wording. Both classic (`ghp_`) and fine-grained (`github_pat_`) PATs work; the docs now say so. - **Cloudflare** — append `?codemode=false` to the URL. OpenClaw's MCP client doesn't speak Cloudflare's experimental Code Mode, so we opt out at the endpoint level rather than getting a surprise JS-sandbox response. HighLevel's MCP server requires a `locationId` header (Sub-Account ID) alongside `Authorization: Bearer pit-…`. Single-token form wasn't enough, so the dialog now captures `locationId` as a conditional second field and threads it through as a generic `extraHeaders: Record<string, string>` map across: - `McpFormValues` zod schema (with a `refine` that requires locationId when preset is highlevel) - POST `/api/integrations` body + storage in `connection.data.extraHeaders` - `/api/integrations/test` + sync route — both forward extraHeaders to `listMcpTools` - `listMcpTools` in `mcp-client.ts` — sets HTTP + SSE headers, with a reserved-header guard (Authorization / Content-Type / Accept stay client-owned) - `regenerateOpenClawConfig` — emits `extraHeaders` only when present - `pinchy-mcp` plugin manifest + runtime — reads `extraHeaders` per connection, sends them on every `tools/call` (with the same reserved- header guard) Generic-by-design so future presets can hang headers on the same hook without another schema bump. Updated suites: preset registry (now 8 entries instead of 10, plus canonical-URL freshness check), picker (no Notion/GitLab tiles + new guard test against accidental re-introduction), dialog (Atlassian copy assertions, GitHub label wording), openclaw-config-mcp (notion replaced by linear in the multi-connection test + new HighLevel extraHeaders emission test). 4171 unit tests pass, tsc clean, lint 0 errors, format clean. Plaintext scanner keeps the Notion (`secret_`) and GitLab (`glpat-`) patterns — they're defensive coverage that'll be useful when those integrations land in Phase 2.
PR #298's UI refactor split the integration type picker out of the modal into a dedicated `/settings/integrations/new` page (commit 82475f1). The "Add Integration" button became a `<Link>`, not a modal trigger — but the Odoo wizard E2E tests still expected a dialog to open directly on click. The happy-path test timed out after 2 minutes waiting for a dialog that won't appear at the old URL. Update both Odoo wizard tests to follow the new flow: 1. Click the "Add Integration" link (now navigates) 2. Land on /settings/integrations/new 3. Click the Odoo tile — opens the wizard dialog with `initialType=odoo` so the connect step renders straight away 4. Resume the existing wizard flow Other E2E specs were checked — only odoo-wizard.spec.ts uses the Add-Integration click path; MCP / Email / Web Search specs all seed via REST so they're unaffected.
…tion The picker's TypeCard renders the tile as a single `role=button` with the `<h3>` name and `<p>` description as siblings inside, so the computed accessible name is "Odoo Connect your Odoo ERP to query …" not just "Odoo". My previous anchored `^Odoo$` selector never matched and the click timed out. Switch to `^Odoo\b` — anchored at the start (so we don't accidentally match a card whose description happens to mention Odoo) but the rest of the name is free to follow. No other tile mentions Odoo, so this remains unambiguous. Same pattern the unit tests use in `integration-type-picker.test.tsx` (loose `/Odoo/` match), just slightly tightened for the wizard's E2E.
Main introduced a strict test that requires every registered template (except `custom`) to map to a category. The MCP starter templates landed before that test, so they're missing entries — the rebase exposed the gap. Map them to the closest existing buckets: - github-pr-reviewer → operations (engineering work) - linear-triage → operations (issue/project tracking) - notion-knowledge-keeper → knowledge-compliance (docs/wiki cluster) No new categories — wait for a clear demand signal before adding a dedicated "Engineering" bucket.
Main added a new email-dispatch E2E test (line 265+) during the
merge gap that sends the legacy `{connectionId, permissions}` PUT
body. PR #298 changed the route to require the unified array shape
with `kind: "odoo"|"mcp"` discriminator, so the test was getting a
400 on the permissions grant.
Update the dispatch probe's beforeAll to use the new shape — same
pattern the other email + odoo E2E specs already follow:
[{ kind: "odoo", connectionId, entries: [...] }]
Email/Google connections share the agentConnectionPermissions table
with Odoo so they ride the same discriminator.
Main's new email-dispatch E2E (`email_list dispatches via fake-LLM and
writes audit entry`) was failing on this PR because my PUT
`/api/agents/:id/integrations` always called `regenerateOpenClawConfig`,
while the pre-PR route never did. The follow-up PATCH
`/api/agents/:id { allowedTools: [...] }` *also* regens — two
config.apply calls within ~1 s hit OpenClaw's `~3/45s` rate limit,
the second one returned `UNAVAILABLE: retry after 13s`, the agent
never made it into OC's view, and the subsequent chat dispatch
failed with `unknown agent id`.
Only call `regenerateOpenClawConfig` when MCP entries are touched.
Odoo / email / web-search plugins fetch credentials AND per-agent
permissions lazily at runtime (Pattern B from AGENTS.md § Secret
Handling), so granting/revoking those permissions doesn't change
the emitted plugin config and doesn't need a regen.
This restores the pre-PR behaviour for the dominant Odoo/email path
while keeping the new behaviour for MCP — whose `agentTools` map *is*
in the emitted plugin config and genuinely needs the regen.
Test updates:
- `route.test.ts` "pure-Odoo round-trip" now asserts regen was NOT
called and explains why (the gap log keeps the next reader from
re-introducing the call).
- Pure-MCP and mixed Odoo+MCP cases still assert the regen runs.
Diagnosis from run 26042864847: `[ws] ⇄ res ✗ config.apply 2ms
errorCode=UNAVAILABLE errorMessage=rate limit exceeded for
config.apply; retry after 13s` followed 30s later by
`[ws] ⇄ res ✗ agent ... unknown agent id "2f203e28..."`.
Eight inline findings, all in test code, all in commits this PR
landed first. Two categories:
1. Unanchored host regex (5 findings):
- `mcp-presets.test.ts` checks `tokenInstructions` markdown
contains the provider's canonical token URL. Anchored each
pattern on `https://` so an attacker-controlled prefix like
`evil.com/github.com/...` couldn't false-match.
- `add-integration-dialog.test.tsx` matched a link by its
accessible name with a host regex. The link's visible text
IS the URL substring (markdown displays the URL as label),
so prefixing with `https://` would break the match. Switch
to an exact-string `name` lookup plus an explicit
`toHaveAttribute("href", "...")` assertion — both stricter
than the original regex and CodeQL-clean.
2. Dead code in `pinchy-mcp/src/index.test.ts` (3 findings):
- Drop unused `vi` import.
- Drop the `mcpResponseOverride` mutable + its dead branch —
never assigned a non-null value anywhere.
- Drop the unused `let callNumber = 0;` in the 401-retry test.
5191 unit tests pass + plugin tests pass, prettier clean.
My previous attempt anchored each URL pattern with the literal `https://` prefix, but CodeQL's `js/regex/missing-regexp-anchor` rule requires `^` or `\b` to consider a regex "anchored" — a literal scheme prefix doesn't count. CI re-flagged the same lines. The assertion is structurally a substring check on a known markdown string, not a regex match. Switch to `expect(...).toContain(...)` with a plain string per entry — stricter than the regex (exact URL match), shorter to read, and removes the CodeQL rule from this spot entirely. 5191 vitest pass, prettier clean.
After rebasing onto main, the merged sync/route.ts emits an
`integration.synced` audit with a `tools` field that the new
audit-detail union (main shipped `integration.synced` for Odoo)
does not list. Add the optional `tools` shape so MCP sync stays
typed instead of widening the whole detail to `unknown`.
The deleted-integration handler builds the detail as a plain
record, which loses the `name: string` shape `DeleteDetail`
requires for the `integration.deleted` event. Narrow the type
so MCP metadata (`mcp.{preset,transport,url}`) still attaches
cleanly via an optional field.
Drizzle prettified the new 0036 snapshot + journal entry.
The rebase landed main's auth-state-aware sync flow on top of the
MCP branch. The MCP-side tests still encoded the pre-merge contract
(`action: "integration_mcp_synced"` discriminator, 502-on-discovery-
throw with failure audit, implicit 400 for unsupported types). Update
them to match the new shared contract:
- audit event is `integration.synced` for both MCP and Odoo;
MCP carries an optional `tools` summary on the detail (added in
audit.ts in the previous commit).
- generic discovery failures now return 200 + `success:false` + error
message (matches main's Odoo behaviour); auth failures take a
separate branch and flip the connection to `auth_failed`.
- non-mcp, non-odoo connection types are explicitly 400'd in the
route rather than silently falling into the Odoo path (where they
would 200-with-failure on schema-parse mismatch, a worse signal).
- mcp-delete.test.ts stub for `odooCredentialsSchema` exposes a
`.partial()` shim so the PATCH-handler module load succeeds even
in DELETE-only test contexts.
- openclaw-config-mcp.test.ts adds the `setSetting` mock that
regenerateOpenClawConfig walks via getOrCreate{EncryptionKey,
PluginSecret}.
- template-integration-coverage drift-guard classifies pinchy-mcp
as NO_SCHEMA_REQUIRED — tool allow-list is per-agent
(agentMcpToolPermissions), not a synced schema.
4621 unit tests pass, prettier + tsc clean.
The 119-commit rebase onto main introduced three drift points:
- New migration index: main shipped `0036_models_table.sql` between my
previous rebase base and HEAD. Drop the stale `0036_lively_prima.sql`
and regenerate the MCP `agent_mcp_tool_permissions` migration fresh
as `0037_mcp_tool_permissions.sql` so the snapshot chain stays
contiguous.
- Capability audit: the `agent-templates-audit` test on main now
requires every non-text-only template to declare both `vision` and
`documents` capabilities. The three MCP templates (github-pr-reviewer,
notion-knowledge-keeper, linear-triage) had only `["tools"]` — extend
to `["tools", "vision", "documents"]` so users can paste screenshots
/ PDFs into chat without auto-default falling back to a text-only
model.
- openclaw-config-mcp.test.ts: main's `regenerateOpenClawConfig` now
calls `ensureModelCapabilityCacheLoaded()` for PDF/image model
selection. Its `db.select().from(models)` would otherwise consume
the first slot in this file's hand-counted db-select mock chain and
shift every fixture by one, so `activeMcpConnections` ended up
bound to the permission rows. Stub the cache module to a no-op so
the existing counter still maps correctly.
Rebase also dropped the obsolete `fix(drizzle): restore 0034 snapshot`
commit ("patch contents already upstream") — main shipped the snapshot
itself.
better-sqlite3@12.10.0 (transitive via better-auth) ships prebuilt
binaries for linux/amd64 but not linux/arm64. On Apple-Silicon dev
hosts the dev image rebuild falls through to node-gyp, which fails
on node:22-slim because Python and the build toolchain are absent:
.../better-sqlite3 install: gyp ERR! find Python
.../better-sqlite3 install: gyp ERR! stack Error: Could not find
any Python installation to use
ELIFECYCLE Command failed with exit code 1.
Add the minimal toolchain (python3, make, g++) so the rebuild
succeeds. Production Dockerfile.pinchy is left alone — it assumes
linux/amd64 where prebuilds work.
… 127.0.0.1 Dev-mode pages froze at their server-rendered shell (setup wizard stuck on "Checking infrastructure...") because Next 16's dev hydration waits on the HMR WebSocket, and that socket could never connect. Two independent defects, invisible to CI because production builds have no HMR: 1. Next.js never registered its HMR upgrade listener on our custom server. NextCustomServer only self-attaches its router-server upgradeHandler when the http server is passed via the `httpServer` option to next() — our server.ts didn't pass it, so HMR upgrade requests on /_next/webpack-hmr hung unanswered forever. (The previously documented assumption that "other upgrade requests are left for Next.js to handle" was wrong; app.getUpgradeHandler() is also not a fix — it resolves to NextNodeServer.handleUpgrade, which is an explicit no-op.) Restructure server.ts to create the http server before next() and hand it over via `httpServer`. The upgrade listener is extracted to src/server/upgrade-router.ts: /api/ws stays Pinchy-owned, every other path is left untouched for Next's own listener (which deliberately skips paths it doesn't own). 2. allowedDevOrigins lacked "127.0.0.1". Next's implicit dev-origin allowance covers localhost but not 127.0.0.1, so dev assets and the HMR endpoint were blocked with "Blocked cross-origin request" when the app was opened via http://127.0.0.1:7777 (the address printed in docs/run instructions). The Caddy domain local.heypinchy.com was already allow-listed, which is why this never surfaced before. Covered by src/__tests__/server/upgrade-router.test.ts: unit tests for the /api/ws routing invariants plus source-level drift guards pinning the `httpServer:` option and the 127.0.0.1 dev origin — the same pattern auth-config-consistency.test.ts uses, chosen because no CI job exercises dev-mode HMR.
Users pick "GitHub" in the integrations picker — MCP is the transport, an implementation detail that must not leak onto the integrations card as "GitHub MCP". The generic preset keeps its "Generic MCP" displayName: whoever picks Custom MCP knows what MCP is.
…lback The card's icon switch only knew google and web-search and fell back to OdooIcon for everything else — a GitHub MCP connection showed the Odoo logo. Extract the mapping into getConnectionIcon() (integration-types): MCP connections resolve their data.preset to the provider's brand icon, unknown types/presets get the neutral McpIcon instead of borrowing another vendor's logo.
A successful test used to dump the full tool list with descriptions into
the dialog — a wall of text for users who just want to connect GitHub.
Success is now one line ("Connected — 44 tools available.") with the
list one click away behind a Show-tools collapsible, where it remains
useful for debugging Custom MCP servers. Tool selection itself lives in
the agent permission UI, not in the connect flow.
"MCP server returned 401 Unauthorized" violated PERSONALITY.md § Error Messages: users clicked "GitHub" in the picker — that we speak MCP underneath is an implementation detail they should never have to decode, and the message offered no recovery path. - The test and create routes now ship a stable `code` (unauthorized | server_error | schema | network) derived from the typed client errors via mcpErrorCodeFromError(). - The dialog maps the code onto preset-aware copy through the new client-safe mcp-error-messages.ts: "GitHub rejected this token. Check that it hasn't expired and has the permissions listed above, then paste a fresh one." - Custom MCP servers additionally keep the raw error as a monospace detail line — their admins run the server themselves, so the raw response is genuinely useful for debugging. - Connect-submit failures go through the same translation as test-connection failures (one path, no drift).
…ability Main shipped 0038_drop_dead_capability_columns (documents/audio/video capabilities removed — PDFs route via OpenClaw's pdf tool now, so requiring "documents" made templates uninstantiable on stacks whose models lack native PDF input). Two knock-on fixes: - Regenerate the MCP migration as 0039_mcp_tool_permissions on top of main's chain (same table, same indexes — only the slot moved). - MCP templates drop "documents" from their modelHint capabilities, matching every other template and the updated capability audit.
The MCP E2E spec hardcoded the dev DB password ("pinchy_dev") as its
seedSetup() fallback. Main commit 4f37b35 introduced stackDbUrl(), which
reads the real DB_PASSWORD the E2E stack initializes Postgres with (the
server fail-closes on the default password per #156), and migrated every
other host-side spec (odoo, email, web, telegram, usage-tracking) to it.
mcp.spec.ts predated that refactor, so its direct postgres() connection
hit "password authentication failed for user pinchy" and aborted the
whole suite before the first test could run. Adopt stackDbUrl(5434),
matching the established sibling pattern.
The mcp-e2e job was the only E2E job without a job-level DB_PASSWORD.
docker-compose.yml interpolates ${DB_PASSWORD:-pinchy_dev} into BOTH
POSTGRES_PASSWORD and the server's DATABASE_URL, and the host-side
seedSetup() reads the same value via stackDbUrl(). With the var unset
only on this job, the stack and the Playwright seed disagreed and the
seed hit "password authentication failed for user pinchy", aborting the
suite before its first test (8 tests "did not run"). Pin it to
pinchy-ci-smoke-pass, matching odoo/email/web/telegram/usage-tracking.
Main shipped its own 0039 (0039_faithful_tyger_tiger, lossless token accounting #483), colliding with my 0039_mcp_tool_permissions. Regenerate the MCP migration fresh on top of main's chain → lands as 0040 (same table, indexes, FKs — only the slot moved). Also repair a rebase artifact in pnpm-lock.yaml: main's OSV-clearing bump (#14a561b8) moved vite to 8.0.16, but the rebased importer still pinned the vitest peer string to vite@8.0.14 with no matching package entry — frozen install broke with ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY. Re-resolved so the importer references the 8.0.16 entry that exists.
The pinchy-mcp feature adds an `activeMcpConnections` db.select() to build.ts (slot 4, between web-search and channelLinks). main's #508 upgrade-path test "PURGES a stale session.identityLinks on regenerate" hand-counts db.select calls and expected channel_links at callCount 4 — now slot 4 is the MCP query (returns [], so allMcpPerms never runs) and channel_links lands at slot 5. Bump the magic number to 5 with a comment documenting the full call order so the next query addition is obvious.
…tch-up Main shipped 0041_steady_stryfe + 0042_silly_revanche (chat_session_errors store for the durable "paused" banner). Regenerate the MCP migration fresh on main's chain → lands as 0043 (same table, indexes, FKs). Conflict resolutions this round combined additive entries: - agent-templates/types.ts: keep both defaultSkills (#543) and recommendedTools - template-icons.ts: keep Newspaper (main) + GitPullRequest/BookOpen/ListTodo (mine) - registry.ts: keep both WEB_TEMPLATES (main) and MCP_TEMPLATES spreads - entrypoint.sh: EXPECTED_PLUGINS lists pinchy-transcript + pinchy-mcp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements Phase 1 / MVP-Lite of MCP (Model Context Protocol) integrations for Pinchy. Admins can now register HTTP/SSE MCP servers, discover their tools, and grant tools to agents under Pinchy's existing permission, audit, and governance layer.
Feature is gated behind
PINCHY_MCP_ENABLED=1(must be explicitly enabled).What's included
Database
agent_mcp_tool_permissionstable (separate fromagentConnectionPermissions— see design doc §3.2 for rationale)IntegrationDatadiscriminated union withMcpIntegrationDatatypeMCP Client (packages/web)
mcp-client.ts— HTTP/SSE tool discovery with typed errors (McpAuthError,McpServerError,McpSchemaError), 10s timeout, SSRF protection viavalidateExternalUrl()mcp-tool-diff.ts— pure diff utility for tool lists (used at sync and in audit)mcp-presets.ts— registry for GitHub, Notion, Linear, and Generic presetspinchy-mcp OpenClaw Plugin
/api/internal/integrations/:id/credentials, 5-minute TTL cache with 401-triggered refreshKNOWN_PINCHY_PLUGINSandEXTERNAL_INTEGRATION_PLUGINSAPI Routes
POST /api/integrations— creates MCP connections with synchronous discoveryPOST /api/integrations/[id]/sync— re-syncs tools, cascades deletions of stale permissions, diffs in audit logPOST /api/integrations/test— admin-only test-connection (no DB write)GET/PUT /api/agents/[id]/integrations— unified discriminated-union endpoint for Odoo + MCP permissions; PUT validates tool availability (409 on stale tool) and replaces both tables atomicallymcp.preset,url,transport)OpenClaw Config
regenerateOpenClawConfig()emitspinchy-mcpplugin entry withconnections[].agentTools(no credentials in emitted config)ghp_,github_pat_,gho_), Notion (secret_), and Linear (lin_api_) token prefixesUX
Templates
recommendedToolswish-list on agent templates (missing tools silently skipped)E2E
config/mcp-mock/— zero-dependency Node.js MCP server with/control/{health,reset,seed,toggle-tool,calls}admin surfacepackages/web/e2e/mcp/mcp.spec.ts— full admin flow specdocker-compose.mcp-test.ymloverlay,playwright.mcp.config.ts,pnpm test:e2e:mcpscript,mcp-e2eCI jobDocs
docs/integrations/mcp/connect-github.md— How-to guidedocs/integrations/mcp/connect-generic.md— How-to guidedocs/integrations/mcp/reference.md— Reference (data model, audit events, security boundary, feature flag)docs/integrations/index.md— Integrations landing page updatedOut of scope (deferred)
Test plan
tsc --noEmitcleanpnpm -C packages/web lint— 0 errorscd docs && pnpm build— 42 pages, cleanKNOWN_PINCHY_PLUGINS, manifest, andconfig-schema.test.tsall referencepinchy-mcpEXTERNAL_INTEGRATION_PLUGINSincludespinchy-mcpand CI matrix hasmcp-e2ejobPINCHY_MCP_ENABLED=0(default) — MCP routes return 404, Odoo unaffectedPINCHY_MCP_ENABLED=1, connect a GitHub MCP server, grant tools to an agent, verify~/.openclaw/openclaw.jsoncontains no plaintext tokensRelated design doc:
docs/plans/2026-04-16-mcp-integrations-design.md(gitignored — local only)