feat(ai): MCP connectors — expose CMS tools to external AI clients#109
Merged
Conversation
…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>
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.
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:
@modelcontextprotocol/sdkServer+WebStandardStreamableHTTPServerTransport(native BunRequest/Response). TypeBoxinputSchemas pass straight through as JSON Schema (no zod). Capability-scoped per connector.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+useEditorMcpBridgeinSitePage), 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.WWW-Authenticate. CRUD handlers enforce a privilege floor + audit log.CapabilityPickerextracted from the Role dialog.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_htmlreturns the created node id → module → class map (fixed a stale-snapshot regression wherecreatedwas always empty).site_render_snapshotscopes to a node subtree viaiframe.contentDocument; importer no longer double-represents anchor content (textOR children, never both).Developer impact
@modelcontextprotocol/sdkis now scoped, not banned: allowed only underserver/ai/mcp/, still banned in drivers + browser (ai-driver-isolation.test.tsnarrowed accordingly).018_ai_mcp_connectors(both dialects).Verification
bun run tsc -b— cleanbun 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 runDocs:
docs/features/mcp-connectors.md, CLAUDE.md MCP bullet,docs/reference/architecture-tests.md,docs/features/audit-log.md.🤖 Generated with Claude Code