Skip to content

Add Docs MCP Server page, fix get_page path resolution - #467

Draft
simonhamp wants to merge 1 commit into
mainfrom
mcp-server-setup-docs
Draft

Add Docs MCP Server page, fix get_page path resolution#467
simonhamp wants to merge 1 commit into
mainfrom
mcp-server-setup-docs

Conversation

@simonhamp

Copy link
Copy Markdown
Member

What

The docs MCP server has been live and completely undocumented — grepping resources/ for mcp/MCP/Model Context Protocol returned zero matches across both desktop and mobile docs. This adds a standalone page at /mcp, linked from the footer as "MCP".

The page covers per-agent setup (Claude Code CLI + .mcp.json, Cursor, VS Code's servers key, an mcp-remote bridge for stdio-only agents), what the four tools do, the raw-markdown URL trick, and the REST endpoints.

It's one standalone page rather than a page per docs version, so the content doesn't have to be duplicated and kept in sync across mobile/4 and desktop/2.

Two defects fixed along the way

Verifying every claim against the live endpoint (rather than describing intent) surfaced two real bugs that the page would otherwise have had to warn readers about.

search_docs returned ids that get_page could not resolve. The section was derived as basename(dirname($file)), so plugins/core/camera.md came back as mobile/4/core/camera — but getPage() rebuilt the path as {platform}/{version}/core/camera.md, which doesn't exist. Searching for "camera" and fetching the top hit returned Page not found. This hit every core plugin page in mobile v3 and v4, and broke exactly the search-then-fetch chain agents rely on.

Sections now carry their full path relative to the version directory, which also makes the ids match the public docs URLs. getPageByPath treats everything between the version and the slug as the section, so existing 4-part paths are unaffected. A new sanitizeSectionPath validates each segment separately so traversal can't hide behind a separator. The page-list cache key is bumped to v2 — without that, entries cached under the old shape would keep serving unresolvable paths for up to 24h after deploy even with correct code.

Removed /api/mcp/sse. SSE is a legitimate MCP transport (the 2024-11-05 HTTP+SSE revision), but this route didn't implement it: a compliant client's first expectation is an event: endpoint frame carrying the POST-back URI, and the stream emitted only data: lines — no event: field anywhere in it. It also returned JSON-RPC responses in the POST body rather than over the stream, which is Streamable HTTP behaviour. So it was a hybrid implementing neither transport, and any conforming client would connect and hang. It additionally held a PHP-FPM worker per connection in a while (true) keepalive loop. Nothing referenced it — no tests, no .mcp.json, no docs. Clients use the Streamable HTTP endpoint at /api/mcp/message, which is what the NativePHP Claude Code plugin already ships.

The REST page route is now a wildcard so nested pages resolve there too; existing 4-segment URLs are unchanged.

Verification

Checked against the running app: search_docs for camera in mobile v4 returns mobile/4/plugins/core/camera and get_page resolves it; flat paths, list_apis for mobile v2, and navigation section ordering (plugins/core still sorts directly after plugins) all still work; /api/mcp/sse returns 404.

Tests

tests/Feature/DocsMcpServerPageTest.php (8 tests) plus two new traversal cases in McpSecurityTest.

The notable one is every_search_result_path_can_be_fetched_by_get_page, which walks real search results and asserts each one resolves — it fails on the actual regression rather than on a hardcoded path. Also covered: the page renders with the endpoint, config snippets survive un-evaluated through Blade, the footer anchor matches both href and label, tools/list returns exactly the four documented tools, the mcp.sse route is gone from the route table, and a guard that no duplicate mcp-server.md reappears under resources/views/docs/.

Notes for review

  • The page content lives in resources/views/mcp-content.md rather than inline in the Blade file. Prettier's Blade plugin splits inline elements onto their own lines, which renders <code>query</code>, as query , — visible on /privacy-policy today. A markdown heredoc inside @php was worse: prettier-plugin-blade appends a stray ''; after the heredoc terminator on every run. A separate .md avoids both and gets Torchlight highlighting via the app's existing CommonMark pipeline.
  • Still outstanding, not touched here: /api/mcp/health always reports "pages": 0, because search('') tokenizes to nothing so every page scores 0 and is filtered out. The page documents health as a liveness check and doesn't mention the count.

🤖 Generated with Claude Code

Documents the docs MCP server on a standalone /mcp page linked from the
footer, covering per-agent setup, the tools, and the REST endpoints.

Also fixes two defects the page would otherwise have to warn about:

- search_docs returned ids get_page could not resolve. The section was
  derived from the parent directory basename, so a page nested in a
  subsection came back as mobile/4/core/camera while the file lives at
  plugins/core/camera.md. Sections now carry their full relative path,
  which also matches the public docs URLs. The page-list cache key is
  bumped so entries cached under the old shape are not reused.

- Removes /api/mcp/sse. It never sent the endpoint event the HTTP+SSE
  transport requires, so compliant clients connected and hung, and it
  held a PHP-FPM worker per connection in a keepalive loop. Clients use
  the Streamable HTTP endpoint at /api/mcp/message.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant