Skip to content

feat(ai): MCP connectors — expose CMS tools to external AI clients#109

Merged
DavidBabinec merged 23 commits into
mainfrom
feat/mcp-connectors
Jun 29, 2026
Merged

feat(ai): MCP connectors — expose CMS tools to external AI clients#109
DavidBabinec merged 23 commits into
mainfrom
feat/mcp-connectors

Conversation

@DavidBabinec

Copy link
Copy Markdown
Contributor

What

Adds MCP connectors: Instatic now exposes its CMS tool catalog to external AI clients (Claude Code, Codex, and any remote MCP agent) at /_instatic/mcp, authenticated by per-connector bearer tokens. A connected agent can read the site, build pages, and write content the same way the in-app AI panel does.

Why

Users want to drive their static site from the terminal agent they already use (Claude Code/Codex) instead of burning API tokens in the in-app panel. MCP is the standard wire for that.

How

Thin adapter over the existing AI tool engine — no parallel tool definitions:

  • Server — low-level @modelcontextprotocol/sdk Server + WebStandardStreamableHTTPServerTransport (native Bun Request/Response). TypeBox inputSchemas pass straight through as JSON Schema (no zod). Capability-scoped per connector.
  • Execution split — headless reads (content reads + site_read_styles/site_list_breakpoints/get_context) run in-process; all page editing relays to the connector owner's open editor via a live editor bridge (editorBridge.ts + useEditorMcpBridge in SitePage), reusing the chat-bridge machinery. The live editor store stays the single source of truth — no headless DB-mutating page-tree tool that would desync an open editor.
  • Auth — bearer tokens stored only as SHA-256 hashes; 401 carries an RFC 9728 WWW-Authenticate. CRUD handlers enforce a privilege floor + audit log.
  • Admin UI — MCP tab (create/list/revoke + client snippets) reusing the shared CapabilityPicker extracted from the Role dialog.
  • Tool naming — consistent snake_case with site_/content_ prefixes across the whole agent + MCP surface (49 tools).

Review-driven improvements (folded in)

  • get_context — orient before editing (editor connected? which templates wrap pages?).
  • site_read_styles (summary format) + site_list_breakpoints.
  • site_insert_html returns the created node id → module → class map (fixed a stale-snapshot regression where created was always empty).
  • site_render_snapshot scopes to a node subtree via iframe.contentDocument; importer no longer double-represents anchor content (text OR children, never both).
  • Editor bridge self-heals — never permanently stops on an auth blip / dev-server restart.

Developer impact

  • @modelcontextprotocol/sdk is now scoped, not banned: allowed only under server/ai/mcp/, still banned in drivers + browser (ai-driver-isolation.test.ts narrowed accordingly).
  • New migration 018_ai_mcp_connectors (both dialects).

Verification

  • bun run tsc -b — clean
  • bun test — full suite green (last full run 5785 pass / 0 fail; MCP + agent + htmlImport subset re-run here: 360 pass / 0 fail)
  • bun run build / bun run lint — green on last full run

Docs: docs/features/mcp-connectors.md, CLAUDE.md MCP bullet, docs/reference/architecture-tests.md, docs/features/audit-log.md.

🤖 Generated with Claude Code

DavidBabinec and others added 22 commits June 29, 2026 10:27
…rivers only

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…andler delegates

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…P transport mounted at /_instatic/mcp

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lege floor + audit

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…picker + client snippets

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tle gate

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t/call) over the real handler

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cker

The capabilities list collided with the AiPage two-column field grid. Switch the
dialog to the SiteCreateDialog form layout + the UsersPage capability-picker
styles + shared CAPABILITY_META, grouped Read/Write, for visual consistency
with the role management dialog.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ialogs

Move the role dialog's capability checklist into a shared, presentational
`@admin/shared/CapabilityPicker` (+ co-located CAPABILITY_META). RoleDialog and
the MCP connector dialog both consume it, so they stay visually consistent and
the markup/CSS lives in one place. UsersPage.module.css keeps only the role
identity grid; users/utils/capabilities.ts keeps only the role grouping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n open editor

Browser-execution editor tools (HTML/CSS authoring, design tokens, page
lifecycle, content CRUD, code assets, live-DOM reads) have no server impl —
they run in the editor app. This relays MCP calls for those tools to the
connector owner's open editor and awaits the result, reusing the chat bridge
machinery wholesale (createBridge / resolveBridgeToolResult / /tool-result).

- server/ai/mcp/editorBridge.ts: per-user bridge registry + NDJSON stream.
- GET /admin/api/ai/editor-bridge: the stream the editor holds open.
- MCP registry now exposes the full deduped catalog; server.ts routes browser
  tools to the live bridge (clear 'open the editor' error when none connected).
- src/admin/.../useEditorMcpBridge.ts: editor-side listener (mounted in SitePage)
  running executeAgentTool + postToolResult, with reconnect.
- MCP picker expanded to all tool-backed editing capabilities.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sh + sharper descriptions

Addresses live-use friction:
- read_styles: returns the design system as a CSS stylesheet (tokens + classes)
  read straight from the DB via the publisher emitters — headless, no editor,
  no snapshot. Replaces the snapshot-dependent list_tokens (excluded from MCP
  along with list_breakpoints, which silently returned nothing over MCP).
- MCP success with no payload now reads as {ok:true}, not the literal 'null',
  so mutating tools (deleteNode) report unambiguous success.
- The editor bridge flushes the draft save after a write tool, so a follow-up
  headless read sees the change instead of stale state (fixes the duplicatePage
  'looked like it failed' race).
- Sharpened read_page_tree/mutate_page_tree descriptions to steer the agent
  (headless-by-id vs editor HTML/CSS authoring; pointer to read_styles).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The MCP CallTool result emitted only text, discarding output.images — so
render_snapshot's screenshot never reached the client. Forward images as MCP
image content blocks. Adds an integration test relaying render_snapshot through
the editor bridge and asserting the PNG arrives as an image block.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… of truth

These MCP tools (added earlier this branch) edited the DB directly, creating a
second copy of each page with identical node ids. Mixed with the open editor's
browser tools they desynced, and the editor's autosave clobbered the headless
write (data loss the agent reproduced). Superseded by the live editor bridge:
ALL page editing now goes through the editor store (browser tools), persisted by
the existing save-flush. Removed the two tools + their registry wiring; tests
repointed to the headless content reads. treeService stays — the plugin RPC
(cms.content.tree.*) still uses it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…th blip

The reconnect loop set stopped=true on a 401/403, so a transient auth failure
during a dev-server restart silently killed the bridge for the whole session
('editor open but MCP can't see it'). Now it always retries (longer backoff for
auth failures) and logs on connect, so the bridge survives restarts and brief
session blips.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s summary, insertHtml created map

- list_breakpoints: headless (reads the site shell) — fixes 'guessed desktop';
  replaces the snapshot-based version that returned nothing over MCP.
- read_styles: format:'summary' returns a compact class catalog (selector +
  referenced token vars, no declarations) for cheap scanning before editing.
- insertHtml now returns 'created' — every inserted node { id, moduleId,
  classes } — so the agent can target nested nodes without re-dumping the tree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s wrap pages?)

Headless orientation tool: reports whether a live editor is connected (browser
tools need it) and which everywhere/post-type templates wrap pages — the two
things that silently confused live use. Reads bridge presence + template rows
from the DB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gent/MCP tools

Standardizes the whole agent toolset (shared by the built-in panel and MCP) on
snake_case with a domain prefix, so the tool list self-groups and humans+agents
can tell at a glance what's site vs content:
  insertHtml→site_insert_html, applyCss→site_apply_css, deleteNode→site_delete_node,
  addPage→site_add_page, render_snapshot→site_render_snapshot, read_styles→site_read_styles,
  create_document→content_create_document, list_collections→content_list_collections, …
get_context stays unprefixed (cross-cutting). The old site/content list_documents
name collision is resolved into distinct prefixed names.

Scoped strictly to agent tool-name references (server defs, both executors, the
panel row formatter, prompts, MCP registry/tools, SSOT gate, tests, docs) —
never the identically-spelled TreeOperation kinds or editor-store methods.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ble-prop

render_snapshot: the canvas renders each breakpoint inside an <iframe> (the
data-breakpoint-id is on the host wrapper, the nodes live in the iframe's
document). The capture searched the host frame, so scoped node lookups never
matched and full captures grabbed the empty wrapper. Now it resolves the
breakpoint's iframe and uses contentDocument as the node-lookup + screenshot
root, and defaults to the active breakpoint (not the first DOM frame, which is
mobile in a mobile-first layout). Scoped site_render_snapshot({nodeId}) and the
right-viewport full capture now work.

HTML importer: a node that recurses into child nodes no longer also keeps a
flattened 'text' prop — children are the source of truth. Fixes a loop <a>
wrapping spans/tokens that got BOTH a text prop and children (ambiguous,
double-render risk). Mirrors the conditional heading/label rules.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t snapshot)

runInsertHtml read activeDocumentNodes(store) from the snapshot captured BEFORE
insertImportedNodes ran, so the just-created nodes (and auto-created classes)
weren't in it — created came back []. Re-read fresh state after the insert.
Now created returns the full inserted subtree { id, moduleId, classes } with
resolved class names, so an agent can target nested nodes without re-reading the
tree. Regression test added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/__tests__/ai/mcpContextTool.test.ts Fixed
Comment thread src/__tests__/architecture/capability-picker-coverage.test.ts Fixed
@DavidBabinec DavidBabinec marked this pull request as ready for review June 29, 2026 15:07
@DavidBabinec DavidBabinec merged commit e554b0e into main Jun 29, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant