Skip to content

feat(mcp): MCP integrations Phase 1 — HTTP/SSE MCP servers, tool permissions, pinchy-mcp plugin#298

Open
clemenshelm wants to merge 58 commits into
mainfrom
claude/cranky-aryabhata-563cc1
Open

feat(mcp): MCP integrations Phase 1 — HTTP/SSE MCP servers, tool permissions, pinchy-mcp plugin#298
clemenshelm wants to merge 58 commits into
mainfrom
claude/cranky-aryabhata-563cc1

Conversation

@clemenshelm

Copy link
Copy Markdown
Contributor

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

  • New agent_mcp_tool_permissions table (separate from agentConnectionPermissions — see design doc §3.2 for rationale)
  • Extended IntegrationData discriminated union with McpIntegrationData type

MCP Client (packages/web)

  • mcp-client.ts — HTTP/SSE tool discovery with typed errors (McpAuthError, McpServerError, McpSchemaError), 10s timeout, SSRF protection via validateExternalUrl()
  • 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 presets

pinchy-mcp OpenClaw Plugin

  • Pattern B secret handling: plugin fetches credentials lazily via /api/internal/integrations/:id/credentials, 5-minute TTL cache with 401-triggered refresh
  • Per-agent allow-list enforcement (tool calls blocked if not in agent's granted set)
  • Registered in KNOWN_PINCHY_PLUGINS and EXTERNAL_INTEGRATION_PLUGINS

API Routes

  • POST /api/integrations — creates MCP connections with synchronous discovery
  • POST /api/integrations/[id]/sync — re-syncs tools, cascades deletions of stale permissions, diffs in audit log
  • POST /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 atomically
  • MCP-aware audit detail on connection delete (snapshots mcp.preset, url, transport)

OpenClaw Config

  • regenerateOpenClawConfig() emits pinchy-mcp plugin entry with connections[].agentTools (no credentials in emitted config)
  • Plaintext scanner extended with GitHub (ghp_, github_pat_, gho_), Notion (secret_), and Linear (lin_api_) token prefixes

UX

  • "Add Integration" dialog: preset picker (GitHub, Notion, Linear, Generic MCP), pre-filled URL/transport, token instructions markdown, "Test connection" button for Generic
  • Agent Settings → Permissions: MCP tool checkboxes per connection card (no separate "MCP Tools" section)
  • Tool drift toast: one-shot notification when a re-sync removes a previously-granted tool

Templates

  • recommendedTools wish-list on agent templates (missing tools silently skipped)
  • GitHub PR Reviewer, Notion Knowledge Keeper, and Linear Triage starter templates

E2E

  • config/mcp-mock/ — zero-dependency Node.js MCP server with /control/{health,reset,seed,toggle-tool,calls} admin surface
  • packages/web/e2e/mcp/mcp.spec.ts — full admin flow spec
  • docker-compose.mcp-test.yml overlay, playwright.mcp.config.ts, pnpm test:e2e:mcp script, mcp-e2e CI job

Docs

  • docs/integrations/mcp/connect-github.md — How-to guide
  • docs/integrations/mcp/connect-generic.md — How-to guide
  • docs/integrations/mcp/reference.md — Reference (data model, audit events, security boundary, feature flag)
  • docs/integrations/index.md — Integrations landing page updated

Out of scope (deferred)

  • OAuth transport (Phase 2 — reuses email-branch infrastructure)
  • Auto-resync scheduler (Phase 2)
  • per-user MCP tokens (Phase 3)
  • stdio transport (Phase 3)
  • Slack preset (Phase 2)

Test plan

  • 3880 unit/integration tests pass locally (0 failures)
  • tsc --noEmit clean
  • pnpm -C packages/web lint — 0 errors
  • cd docs && pnpm build — 42 pages, clean
  • Plaintext scanner patterns verified (no real tokens in commits)
  • KNOWN_PINCHY_PLUGINS, manifest, and config-schema.test.ts all reference pinchy-mcp
  • EXTERNAL_INTEGRATION_PLUGINS includes pinchy-mcp and CI matrix has mcp-e2e job
  • PINCHY_MCP_ENABLED=0 (default) — MCP routes return 404, Odoo unaffected
  • Manual: boot dev stack with PINCHY_MCP_ENABLED=1, connect a GitHub MCP server, grant tools to an agent, verify ~/.openclaw/openclaw.json contains no plaintext tokens

Related design doc: docs/plans/2026-04-16-mcp-integrations-design.md (gitignored — local only)

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.
@clemenshelm clemenshelm force-pushed the claude/cranky-aryabhata-563cc1 branch from f42c279 to ec08025 Compare May 11, 2026 06:50
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.
@clemenshelm clemenshelm force-pushed the claude/cranky-aryabhata-563cc1 branch from ec08025 to eb2aafb Compare May 11, 2026 07:23
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.
@clemenshelm clemenshelm force-pushed the claude/cranky-aryabhata-563cc1 branch from eb2aafb to 2ccd808 Compare May 12, 2026 11:31
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.
@clemenshelm clemenshelm force-pushed the claude/cranky-aryabhata-563cc1 branch from 65ee7d2 to f2354aa Compare May 13, 2026 04:24
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 clemenshelm force-pushed the claude/cranky-aryabhata-563cc1 branch from f2354aa to 7bca2f4 Compare May 13, 2026 04:45
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 clemenshelm force-pushed the claude/cranky-aryabhata-563cc1 branch from 7bca2f4 to 9b78870 Compare May 13, 2026 07:58
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.
@clemenshelm clemenshelm force-pushed the claude/cranky-aryabhata-563cc1 branch from 9b78870 to aff36a5 Compare May 13, 2026 18:01
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.
@clemenshelm clemenshelm force-pushed the claude/cranky-aryabhata-563cc1 branch from aff36a5 to 0acd70c Compare May 18, 2026 15:12
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.
@clemenshelm clemenshelm force-pushed the claude/cranky-aryabhata-563cc1 branch from fb5510c to a41a9cb Compare May 19, 2026 18:51
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.
Clemens Helm 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants