Skip to content

feat(mcp): add MCP server tools (list-pages, get-page)#33

Merged
amondnet merged 2 commits into
mainfrom
feat/mcp-server-tools
May 28, 2026
Merged

feat(mcp): add MCP server tools (list-pages, get-page)#33
amondnet merged 2 commits into
mainfrom
feat/mcp-server-tools

Conversation

@amondnet
Copy link
Copy Markdown
Contributor

@amondnet amondnet commented May 28, 2026

Summary

Continues the docus-upstream-changes survey port after #27 by adding MCP server tools so MCP clients (Claude Desktop, etc.) can query our documentation directly.

Implements survey item #8 (docs/docus-upstream-changes.md). Ports upstream docus commits:

  • ec3b468c — initial MCP tool surface
  • 3eb54da4 — exclude .navigation paths
  • f7e42ebfapp.baseURL correctness
  • 6043beb1 — derive URLs from the request origin

What's added

packages/layer/server/mcp/tools/list-pages.ts — Zod-schema MCP tool exposing every page's title, path, description, locale, and absolute url. Optional locale input filter. .navigation paths are skipped. Per-collection query failures (missing i18n collections etc.) degrade gracefully instead of failing the whole listing.

packages/layer/server/mcp/tools/get-page.ts — Zod-schema MCP tool that returns a single page's title, path, description, content (markdown), and absolute url. The body is fetched via event.$fetch('/raw{path}.md'), reusing the existing route at server/routes/raw/[...slug].md.get.ts.

packages/layer/server/utils/content.ts — shared helpers (getAvailableLocales, getCollectionsToQuery, isNavigationPath, getCollectionFromPath). sitemap.xml.ts now consumes the first two so the helpers have a single owner.

packages/layer/nuxt.config.ts + packages/layer/package.json — adds @nuxtjs/mcp-toolkit@^0.17.2 (matches upstream docus pin) and registers it in the modules array alphabetically near the other @nuxtjs/* modules. Tools are auto-discovered from server/mcp/tools/*.ts.

Each tool sets cache: '1h' and the read-only annotation set (readOnlyHint, idempotentHint).

Behavior notes

  • URLs use the live request origin (getRequestURL(event).origin) with fallback to inferSiteURL(). app.baseURL is joined via ufo.joinURL.
  • The defineMcpTool, useEvent, useRuntimeConfig, getRequestURL, createError symbols are server-side auto-imports provided by @nuxtjs/mcp-toolkit / Nitro / Nuxt — no manual import needed.

Verification

  • bun lint — passes
  • bun typecheck (in packages/layer) — no new errors introduced by these files (pre-existing errors in unrelated files remain)
  • bun dev — starts cleanly, logs [@nuxtjs/mcp-toolkit] ✔ /mcp enabled with 2 tools. POST /mcp returns a proper MCP response; GET /mcp returns 405 as expected.
  • bun --filter @pleaseai/docs-site build with NITRO_PRESET=node-server — succeeds.

Known limitation: Cloudflare Pages build

Under the docs site's default cloudflare_pages preset, the toolkit's Cloudflare transport (providers/cloudflare.js) dynamically imports agents/mcp, which in turn lazy-loads ai. Both are optional peer dependencies of the toolkit, but the Cloudflare preset sets noExternals: true and Rollup fails to follow the dynamic-import chain at build time even when the packages are installed at the workspace root:

ERROR  Cannot resolve "ai" from "node_modules/agents/dist/client-QZa2Rq0l.js"
       and externals are not allowed!

This is independent of the new tool code (it reproduces on any project that just adds @nuxtjs/mcp-toolkit to a Cloudflare-targeted Nuxt app). Tracking this separately — likely needs an upstream toolkit fix (mark agents/ai as externals.inline in the toolkit's nitro hook when the CF preset is detected, or skip the CF provider when the user has no Durable Object streaming). The dev server and the node-server build are unaffected, and MCP functionality is fully exercised in dev.


Summary by cubic

Adds MCP server tools so clients can list and fetch documentation pages directly from apps like Claude Desktop. URLs are correct for prefixed and multi-host setups, responses are cached, and @nuxtjs/mcp-toolkit is registered; Cloudflare Pages builds still fail due to the toolkit’s dynamic imports (dev and node-server builds are fine).

  • New Features

    • list-pages: lists pages (title, path, description, locale, absolute url), optional locale filter, skips .navigation, tolerates missing collections.
    • get-page: returns title, path, description, and full markdown via /raw{path}.md, skips .navigation.
    • URLs derive from the live request origin plus app.baseURL; tools are read-only and cached for 1h.
  • Bug Fixes

    • list-pages: when locale is provided but unsupported on i18n sites, return no results instead of falling back to other languages (non-i18n sites still use docs).
    • get-page: build /raw.md from the canonical page.path to avoid 404s from casing/trailing slashes.
    • get-page: guard error type before reading statusCode to prevent TypeErrors.

Written for commit 287a92b. Summary will update on new commits.

Port @nuxtjs/mcp-toolkit MCP server tools from upstream docus so MCP
clients can query documentation pages directly.

Tools added (auto-discovered from `server/mcp/tools/*.ts`):

- `list-pages`: lists all pages with title, path, description, locale,
  and absolute URL. Optional `locale` filter. Skips `.navigation` paths.
- `get-page`: returns a single page by exact path, with the full
  markdown body fetched from the existing `/raw/{path}.md` route. Skips
  `.navigation` paths.

URL derivation prefers the live request origin (via `getRequestURL`),
then falls back to `inferSiteURL()`. `app.baseURL` is joined into the
URL via `ufo.joinURL` so prefixed deployments stay correct.

New shared helpers in `server/utils/content.ts`:
`getAvailableLocales`, `getCollectionsToQuery`, `isNavigationPath`, and
`getCollectionFromPath`. The existing `server/routes/sitemap.xml.ts`
now reuses `getAvailableLocales` and `isNavigationPath` from there
(behavior unchanged).

Also:
- Add `@nuxtjs/mcp-toolkit` (^0.17.2) to layer dependencies and
  register it in `nuxt.config.ts` modules.
- Tools are auto-imported via the toolkit's `defineMcpTool`,
  `useEvent`, `useRuntimeConfig`, `createError`, `getRequestURL`.
- Each tool caches its response for `'1h'`.

Tolerate per-collection query failures in `list-pages` (missing i18n
locale collections, etc.) so a single bad collection doesn't fail the
whole listing.

Verified: `bun lint` passes, `bun typecheck` introduces no new errors,
`bun --filter @pleaseai/docs-site build` succeeds with the
`node-server` preset, and `bun dev` starts cleanly with
`[@nuxtjs/mcp-toolkit] /mcp enabled with 2 tools`.

Note: under the Cloudflare Pages preset (the docs site's default
preset), the toolkit's Cloudflare transport dynamically imports
`agents/mcp`, which lazy-loads `ai`. Both are optional peers of the
toolkit, but `noExternals: true` on the Cloudflare preset breaks the
dynamic import chain at build time. Working through this is tracked
separately — the dev server and node-server build are unaffected.

Refs upstream docus commits:
- ec3b468c (initial)
- 3eb54da4 (exclude .navigation)
- f7e42ebf (baseURL fix)
- 6043beb1 (derive URLs from request origin)

Survey item docs/docus-upstream-changes.md#8.
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request integrates the @nuxtjs/mcp-toolkit module and introduces two new MCP tools, get-page and list-pages, to enable querying and retrieving documentation pages. It also refactors common content and locale resolution logic into a shared utility file (packages/layer/server/utils/content.ts). The review feedback highlights three main improvement opportunities: refining getCollectionsToQuery to return an empty array when an unsupported locale is requested on an i18n-enabled site, using the validated page.path instead of the raw user-input path when fetching raw markdown in get-page.ts, and safely checking the error object type before accessing its statusCode property in the catch block of get-page.ts.

Comment thread packages/layer/server/utils/content.ts
Comment thread packages/layer/server/mcp/tools/get-page.ts Outdated
Comment thread packages/layer/server/mcp/tools/get-page.ts
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 28, 2026

Deploying docs-please with  Cloudflare Pages  Cloudflare Pages

Latest commit: 287a92b
Status: ✅  Deploy successful!
Preview URL: https://8e4075c6.docs-please.pages.dev
Branch Preview URL: https://feat-mcp-server-tools.docs-please.pages.dev

View logs

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 7 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/layer/server/mcp/tools/get-page.ts">

<violation number="1" location="packages/layer/server/mcp/tools/get-page.ts:62">
P1: Raw markdown fetch path is not `app.baseURL`-aware, which can break `get-page` on subpath deployments.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant MC as MCP Client (Claude Desktop)
    participant Nuxt as Nuxt App (@nuxtjs/mcp-toolkit)
    participant Event as Nuxt Event (useEvent)
    participant Content as @nuxt/content (queryCollection)
    participant Raw as /raw/[...slug].md Route
    participant Config as Runtime Config
    participant Helpers as Server Utils (content.ts)

    Note over MC,Helpers: NEW: MCP Tool Discovery & Registration

    Nuxt->>Nuxt: Initialize @nuxtjs/mcp-toolkit module
    Nuxt->>Nuxt: Auto-discover server/mcp/tools/*.ts
    Nuxt-->>Nuxt: Register "list-pages" & "get-page" tools

    Note over MC,Raw: Happy Path: MCP Client queries list-pages

    MC->>Nuxt: POST /mcp (method: "tools/call", name: "list-pages", locale?)

    Nuxt->>Event: defineMcpTool handler invoked
    Event->>Config: useRuntimeConfig(event)
    Config-->>Event: runtime config (public, app.baseURL)
    Event->>Event: getRequestURL(event).origin || inferSiteURL()
    Event->>Helpers: getAvailableLocales(publicConfig)
    Helpers-->>Event: e.g., ["en", "fr"]
    Event->>Helpers: getCollectionsToQuery(locale, availableLocales)
    Helpers-->>Event: e.g., ["docs_en", "docs_fr"]

    loop For each collection name
        Event->>Content: queryCollection(event, collectionName).select(...).all()
        Content-->>Event: Array of pages
        Event->>Event: Filter out .navigation paths (isNavigationPath)
        Event->>Event: Map pages with locale & url (joinURL)
    end

    alt Collection query fails (e.g., missing i18n collection)
        Event->>Event: Catch error, return empty array []
    end

    Event-->>Nuxt: Flattened page list
    Nuxt-->>MC: MCP response (page metadata array)

    Note over MC,Raw: Happy Path: MCP Client queries get-page

    MC->>Nuxt: POST /mcp (method: "tools/call", name: "get-page", path: "/en/getting-started/installation")

    Nuxt->>Event: defineMcpTool handler invoked
    Event->>Event: isNavigationPath(path) → false, continue
    Event->>Config: useRuntimeConfig(event)
    Config-->>Event: runtime config
    Event->>Event: getRequestURL(event).origin || inferSiteURL()
    Event->>Helpers: getAvailableLocales(publicConfig)
    Event->>Helpers: getCollectionFromPath(path, availableLocales)
    Helpers-->>Event: e.g., "docs_en"

    Event->>Content: queryCollection(event, "docs_en").path(path).select(...).first()
    alt Page found
        Content-->>Event: { title, path, description }
        Event->>Raw: event.$fetch(`/raw${withLeadingSlash(path)}.md`)
        Raw-->>Event: Full markdown content
        Event->>Event: Build absolute url with joinURL(siteUrl, baseURL, path)
        Event-->>Nuxt: { title, path, description, content, url }
    else Page not found
        Content-->>Event: null
        Event->>Event: throw createError(404, "Page not found")
        Event-->>Nuxt: Error response
    end

    Nuxt-->>MC: MCP response (page content object)

    Note over Nuxt,Helpers: Error Handling & Edge Cases

    alt Navigation path requested
        Event->>Event: isNavigationPath(path) → true
        Event->>Event: throw createError(404, "Page not found")
        Event-->>Nuxt: Error response
        Nuxt-->>MC: MCP error
    end

    alt Server error during get-page
        Event->>Content: Query or fetch fails (non-404)
        Event->>Event: throw createError(500, "Failed to get page")
        Event-->>Nuxt: Error response
        Nuxt-->>MC: MCP error
    end
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/layer/server/mcp/tools/get-page.ts Outdated
Comment thread packages/layer/server/mcp/tools/get-page.ts Outdated
Comment thread packages/layer/server/utils/content.ts Outdated
- content.ts: return [] when locale is given but unsupported on i18n
  sites, so list-pages no longer silently falls back to every other
  language. Single-locale (non-i18n) sites still return ['docs'].
- get-page.ts: build the /raw<path>.md fetch URL from the canonical
  page.path rather than the raw user input, so trailing slashes or
  non-canonical casing don't produce a 404.
- get-page.ts: guard error type before reading .statusCode so a
  primitive thrown value cannot trigger a TypeError.

Addresses gemini-code-assist and cubic-dev-ai review threads on #33.
@amondnet amondnet self-assigned this May 28, 2026
@sonarqubecloud
Copy link
Copy Markdown

@amondnet amondnet merged commit f52dc38 into main May 28, 2026
3 checks passed
@amondnet amondnet deleted the feat/mcp-server-tools branch May 28, 2026 15:56
amondnet added a commit that referenced this pull request May 28, 2026
Resolve modules-array conflict in packages/layer/nuxt.config.ts (#33
landed @nuxtjs/mcp-toolkit; keep ./modules/markdown-rewrite from this
branch alongside it).
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