Skip to content

Authenticated MCP read route + read resources + llms.txt (#18)#53

Merged
CuriouslyCory merged 2 commits into
mainfrom
feat/mcp-read-route
May 31, 2026
Merged

Authenticated MCP read route + read resources + llms.txt (#18)#53
CuriouslyCory merged 2 commits into
mainfrom
feat/mcp-read-route

Conversation

@CuriouslyCory

@CuriouslyCory CuriouslyCory commented May 31, 2026

Copy link
Copy Markdown
Owner

Closes #18.

Joins #17's API tokens and #15's deterministic markdown serializer into an authenticated MCP server: an agent presents a bearer token and reads the architecture as deterministic markdown, scoped to its owner's projects. Read-only by design.

What's here

  • Token → Actor (resolveActorFromToken, token.service.ts): re-derives the keyed HMAC (ADR-0020), looks the token up by tokenHash, and resolves it to an Actor{via:"token"}. Missing / unknown / revoked / expired all collapse to one null → a single 401 — which check failed is never disclosed (ADR-0002).
  • Owner-gated read (export.service.ts): refactored into a three-layer split that refines ADR-0017 — pure serializeGraph → shared serializeProjectScope → two front doors: the slug-grant exportMarkdown (web, unchanged) and the new owner-gated exportMarkdownForActor (MCP, addressed by projectId, authorized via assertCanRead). The two grant postures share fetch, not authz.
  • MCP module (src/server/mcp/): a data-driven resource catalog (index / project / subtree — the MCP-addressable face of the serializer's three modes), SDK registration with an owner-scoped resources/list (reuses listProjects), the withMcpAuth verifier, and a not-found≡forbidden error mapper.
  • Route + discovery: /api/mcp over Streamable HTTP via mcp-handler (SSE disabled → no Redis, no new env var; runtime="nodejs"). Generated /llms.txt rendered from the same catalog so it can't drift from resources/list.
  • Docs travel with the slice: ADR-0022; CONTEXT.md future→present flips + new MCP path / MCP resource / llms.txt / Agent entries.

Acceptance criteria

  • MCP route speaks Streamable HTTP and resolves a bearer token to an Actor
  • Unauthenticated / revoked / expired rejected; no anonymous access
  • An Actor can only read its owner's projects (no resource accepts a user id)
  • Read resources return markdown for project, subtree, and index via the existing serializer
  • llms.txt served describing endpoint, auth, and resources
  • Read authorization tested + MCP Inspector-style round-trip reads a project as markdown

Validation

  • pnpm check clean (eslint + tsc).
  • pnpm test: 235/235 — includes the ADR-0017 golden byte-equality test, so the export refactor is provably byte-identical.
  • Live round-trip (what pnpm check can't catch): /llms.txt→200; tokenless/bogus→401; initialize→capabilities; resources/list→owner's projects only; resources/read of project & subtree→correct markdown; inaccessible project→non-disclosing "not found". Synthetic seed cleaned up after.

Scope boundaries (held by design)

New dependencies

mcp-handler@1.1.0 + @modelcontextprotocol/sdk@1.26.0 (the SDK is mcp-handler's required peer). Chosen over hand-rolling Streamable HTTP for protocol/Inspector compatibility; the resolver and registry are transport-agnostic so swapping to the SDK transport directly would be localized.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added an authenticated MCP read surface with token-based access and owner-gated resource reads.
    • Added a dynamic /llms.txt discovery endpoint listing available MCP resources.
    • "Copy as markdown" now uses the MCP-backed export path (owner-gated for MCP; web path remains slug-readable).
  • Documentation

    • Updated glossary and ADR describing MCP vocabulary, token→Actor behavior, and the Markdown export contract.

Joins #17's API tokens and #15's deterministic serializer into an
authenticated MCP server: an agent presents a bearer token, reads the
architecture as deterministic markdown, scoped to its owner's projects.

- resolveActorFromToken: re-derives the keyed HMAC (ADR-0020) and resolves
  a token to an Actor{via:"token"}; missing/unknown/revoked/expired all
  collapse to one null -> 401 (non-disclosure, ADR-0002).
- export.service refactored into a three-layer split (ADR-0017 refined):
  pure serializeGraph -> shared serializeProjectScope -> two front doors,
  the slug-grant exportMarkdown (web) and owner-gated exportMarkdownForActor
  (MCP). Golden byte-equality test unchanged.
- src/server/mcp/: resource catalog (index/project/subtree, additive for
  #38), SDK registration with owner-scoped resources/list, withMcpAuth
  verifier, and a not-found-equals-forbidden error mapper.
- Route at /api/mcp (Streamable HTTP via mcp-handler, SSE disabled so no
  Redis/config, runtime=nodejs); generated /llms.txt rendered from the
  catalog.
- 13 service tests (resolver, owner-gated read incl. cross-owner, list
  isolation). ADR-0022; CONTEXT.md future->present + MCP path/resource/
  llms.txt/Agent entries.

Read-only: write tools (#19/#20), Flow resources (#38), and scope
enforcement (ADR-0021) stay out of scope by design.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented May 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
infinite-docs Ready Ready Preview, Comment May 31, 2026 11:35pm

Request Review

@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 687a677b-036d-4b68-a484-dd2e287c87cc

📥 Commits

Reviewing files that changed from the base of the PR and between 764a7e6 and ccbb7f7.

📒 Files selected for processing (1)
  • docs/adr/0022-authenticated-mcp-read-surface.md
✅ Files skipped from review due to trivial changes (1)
  • docs/adr/0022-authenticated-mcp-read-surface.md

📝 Walkthrough

Walkthrough

Implements an authenticated MCP read surface: token→Actor resolution, owner-gated MCP resources (index/project/subtree), shared serializeProjectScope core for deterministic markdown, Next.js route wiring with Node runtime and SSE disabled, dynamic /llms.txt discovery, and tests for auth and read isolation.

Changes

Authenticated MCP Read Surface

Layer / File(s) Summary
Design Documentation and Architecture Decisions
docs/adr/0022-authenticated-mcp-read-surface.md, CONTEXT.md
ADR-0022 and updated glossary describe the authenticated MCP surface, token→Actor behavior, three-layer read split, Streamable HTTP transport, and the two-front-door markdown export contract.
Token Authentication and Actor Resolution
src/server/architecture/token.service.ts, src/server/mcp/auth.ts
Adds resolveActorFromToken (token hash lookup, reject missing/unknown/revoked/expired) and MCP auth helpers (makeVerifyMcpToken, actorFromAuthInfo) producing AuthInfo/Actor.
Markdown Export Refactoring for Dual Paths
src/server/architecture/export.service.ts
Refactors export into serializeProjectScope core (delegates to serializeGraph), threads ResolvedProject ({projectId,projectTitle}), and updates slug and actor entrypoints to call the shared core; SQL now filters by resolved projectId.
MCP Read Input Schema
src/lib/schemas.ts
Adds mcpReadInput Zod schema and McpReadInput type using projectId, optional canvasNodeId (null default), and defaulted mode.
MCP Resource Catalog and Descriptors
src/server/mcp/catalog.ts
Defines RESOURCE_SCHEME, MARKDOWN_MIME, descriptor interface, and READ_RESOURCES (index,project,subtree) with URI templates and toInput mappers.
MCP Error Mapping
src/server/mcp/errors.ts
Adds toMcpReadError to normalize service/Zod/unknown errors into MCP McpError, collapsing not-found/forbidden/conflict into a non-disclosing InvalidParams.
MCP Handler and Resource Registration
src/server/mcp/handler.ts, src/server/mcp/resources.ts
Creates createArchitectureMcpHandler() (builds handler, registers resources, disables SSE, wraps with withMcpAuth) and registerArchitectureResources (per-resource list/read handlers scoped to the calling Actor and returning markdown via exportMarkdownForActor).
HTTP Routes and Runtime Dependencies
package.json, src/app/api/[transport]/route.ts, src/app/llms.txt/route.ts
Adds @modelcontextprotocol/sdk and mcp-handler. Exposes MCP handler via Next.js Node runtime API route for GET/POST/DELETE and serves dynamic /llms.txt describing the MCP endpoint and resources.
Authentication and Authorization Testing
src/server/architecture/__tests__/mcp-read.service.test.ts
Vitest suite checks token resolution/rejection cases, export rendering and mode differences, cross-user access control, soft-delete mapping to NotFound, subtree isolation, and owner-scoped project listing.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • #18: Authenticated MCP route + read resources + llms.txt — This PR implements the token→Actor resolution, owner-gated read resources, and llms.txt discovery described in issue #18.

Possibly related PRs

"🐰 A bearer token, neatly penned,
Resolves to an Actor, friend,
Markdown marches out on cue,
Owner-only views—hop, hop—it's true! ✨"

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: authenticated MCP read route, read resources, and llms.txt generation, matching the core objectives.
Linked Issues check ✅ Passed All acceptance criteria from issue #18 are met: MCP route with Streamable HTTP, bearer token resolution, rejection of invalid tokens, owner-only project access, markdown resources (project/subtree/index), llms.txt discovery, and authorization tests.
Out of Scope Changes check ✅ Passed All changes align with issue #18 scope: authentication, token resolution, read resources, and discovery. Write tools, Flow resources, scope enforcement, and session auth remain explicitly out of scope as documented.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mcp-read-route

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/adr/0022-authenticated-mcp-read-surface.md`:
- Around line 16-17: The two occurrences that begin with the literal tokens
`#17` and `#18` are being interpreted as ATX headings by Markdown linters;
update the ADR text so those references are escaped or rephrased (for example
use backticks, add a non-heading-safe prefix like "issue " or enclose in
parentheses) wherever the document currently starts a line with `#17` or `#18`
to avoid MD018 lint failures; search for the exact tokens `#17` and `#18` in the
ADR content and replace them with the escaped or reworded forms.
- Around line 82-84: Update the sentence that says
`withMcpAuth(resolveActorFromToken, { required: true })` to reflect that the
actual adapter seam wraps the resolver via `makeVerifyMcpToken(db)` before
wiring to `withMcpAuth`; i.e., mention that `resolveActorFromToken` is invoked
through the `makeVerifyMcpToken` verifier (used by `createMcpHandler`) rather
than being passed directly to `withMcpAuth`, so the doc matches the real call
chain.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 02666434-5818-449c-8d77-537980e75026

📥 Commits

Reviewing files that changed from the base of the PR and between b2deb17 and 764a7e6.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (14)
  • CONTEXT.md
  • docs/adr/0022-authenticated-mcp-read-surface.md
  • package.json
  • src/app/api/[transport]/route.ts
  • src/app/llms.txt/route.ts
  • src/lib/schemas.ts
  • src/server/architecture/__tests__/mcp-read.service.test.ts
  • src/server/architecture/export.service.ts
  • src/server/architecture/token.service.ts
  • src/server/mcp/auth.ts
  • src/server/mcp/catalog.ts
  • src/server/mcp/errors.ts
  • src/server/mcp/handler.ts
  • src/server/mcp/resources.ts

Comment thread docs/adr/0022-authenticated-mcp-read-surface.md Outdated
Comment thread docs/adr/0022-authenticated-mcp-read-surface.md
ADR-0022 doc accuracy: escape #17/#18 line starts (MD018) and correct the
withMcpAuth wiring to reflect the makeVerifyMcpToken(db) verifier seam.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@CuriouslyCory

Copy link
Copy Markdown
Owner Author

Fixes Applied Successfully

Fixed 1 file based on 2 CodeRabbit feedback item(s).

Files modified:

  • docs/adr/0022-authenticated-mcp-read-surface.md

Changes:

  • Escaped the #17 / #18 line-start issue references so they aren't parsed as ATX headings (MD018).
  • Corrected the transport auth-wiring sentence to match the real seam — withMcpAuth(handler, makeVerifyMcpToken(db), { required: true }), where the verifier wraps resolveActorFromToken.

Commit: ccbb7f7

The latest autofix changes are on the feat/mcp-read-route branch.

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.

Authenticated MCP route + read resources + llms.txt

1 participant