Перейти к основному содержимому

A read-only MCP (Model Context Protocol) server that exposes v10r's pattern library to coding agents. It is what makes "emulate, don't clone" (see the root README) executable rather than aspirational: an agent building a new project queries curated pattern cards — docs, code, tests, showcase proof, invariants — instead of grepping this repo and guessing which hits are load-bearing.

This doc explains why it's shaped the way it is. For running it, testing it, or adding a pattern, see mcp/README.md — that's the operational reference. The code lives in mcp/.

See it live: /showcases/mcp — registry stats, the pattern dependency graph, the container architecture, and the JSON-RPC handshake, all computed from the real registry.


Why this exists

Grep finds code. It doesn't find the tests that pin a pattern's behavior, the invariants that must survive being ported to a new project, or proof that the pattern actually works end to end. An agent pointed at a raw repo has to reconstruct all of that by reading broadly and inferring — slow, and easy to get wrong in ways that only surface later (a copied auth guard missing the invariant that made it safe, a copied job missing the retry semantics that made it correct).

The Pattern MCP closes that gap. Each entry in mcp/patterns.registry.json is a curated card: what the pattern is, when to reach for it, which docs to read, which files to model code on, which tests mirror its behavior, which showcase route proves it live, and — the part grep can never give you — the invariants that must hold when you emulate it. The server is a thin, deterministic query layer over that registry; the registry is the actual product.

Design

Registry-as-product

The server (server.ts, protocol.ts, tools.ts) is intentionally boring: JSON-RPC plumbing and six query functions over two JSON files (the pattern registry and the snippet rules). All the curation work — deciding what counts as a pattern, which invariants matter, which files are the canonical entry point — lives in the registry data, not in code. Adding a pattern means adding a record, not writing a handler.

A drift guard (validate-registry.ts, wired into bun run validate as mcp:validate) keeps the registry honest: every docs/code/tests/showcases path must exist on disk, depends_on must form a DAG (checked via the same Kahn toposort the server uses at query time), and IDs must be unique kebab-case. A registry that references a moved or deleted file fails the gate — unlike a stale doc, it can't silently rot.

The registry is also self-referential: it catalogs v10r's own documentation conventions as patterns (docs-nav-hubs, pattern-index) alongside code patterns (multi-client-core, layered-rag, jobs-scheduler, …). The root README's Pattern Index is described in the registry itself as "the human-readable twin of this MCP's registry" — same map, two audiences.

Curated cards over raw grep

Each card bundles five things a grep hit never gives you together: docs (what to read first), code (where the canonical implementation lives), tests (what pins its behavior), showcases (where it's proven live), and invariants (what must hold true after emulation, not just after copying). get_pattern returns the whole card; trace_capability walks concept → docs → code → tests → proof for a free-text query; search_patterns ranks cards by a weighted lexical match over title/keywords/capabilities/category/summary/invariants — no embeddings, no network call, because the container has neither.

Deterministic plan assembly, not inference

recommend_emulation_plan takes a list of desired capabilities and returns a dependency-ordered build plan — but it does no reasoning of its own. It matches capabilities to pattern records lexically, expands the selection through depends_on edges, and orders the result with the same Kahn toposort validate-registry.ts uses to check the registry for cycles. The assembly is 100% deterministic and inspectable; the agent supplies the judgment about how to adapt each step to the target project. Keeping the assembler dumb is deliberate — it's a query over curated facts, not a second opinion.

Ephemeral read-only container

MCP clients spawn the server as a throwaway container, not a long-lived process on the host:

podman run -i --rm --network=none -v <repo>:/v10r:ro docker.io/oven/bun:1.3.12 bun /v10r/mcp/server.ts

--rm means no state survives between sessions, --network=none means no exfiltration path exists even if something in the tool chain were compromised, and :ro means the mount can't be written to no matter what the server code does. This is kernel-enforced isolation, not a promise the application layer has to keep. security.ts adds a second, application-level layer on top — realpath-based path containment (rejects .. traversal and symlink escapes) and a secret-filename denylist (.env, *.pem, *.key, .git/, node_modules/, …) for get_file_excerpt — because client-supplied paths are advisory in MCP; the server is the trust boundary, not the client.

Zero-dependency protocol

server.ts and protocol.ts hand-roll JSON-RPC 2.0 framing over newline-delimited stdio (MCP spec 2025-11-25) with no SDK and no validation library. This isn't a style preference: the container is a bare oven/bun image with no node_modules, so any dependency would mean either baking a custom image or an bun install step on every spawn. Hand-rolling the ~150 lines of framing and a hand-written structural validator (registry.ts) keeps the spawn command a one-liner and the cold-start instant.

The initialize response also carries a hand-written instructions string (in server.ts) that tells the calling agent when to reach for this server versus plain Read/Grep — the same "emulate, don't clone" framing this doc opens with, but delivered as part of the protocol handshake instead of a doc the agent might not have read.

Client design constraints (the gotchas)

A handful of the server's behaviors exist only because live E2E testing against Claude Code surfaced non-obvious client bugs and limits. They're encoded as constraints, not comments, and regression-guarded by smoke.ts (a spawned-subprocess test, not a mock) where possible:

Constraint Why
Tool defs carry only name/description/inputSchema — never outputSchema, title, or annotations A live Claude Code bug silently drops a server's entire tool list if any one tool definition carries these fields.
Tool results are plain text/markdown — structuredContent is never returned E2E dogfooding showed that when a result has structuredContent, Claude Code shows the model only that payload and hides the text body — get_file_excerpt came through as bare metadata with no code.
stdout carries protocol frames only; all logging goes to stderr Any stray console.log corrupts the NDJSON stream the client is parsing.
The process exits on stdin EOF or SIGTERM/SIGINT Claude Code has a known issue leaving spawned child processes orphaned otherwise — the server terminates itself rather than trusting the client to clean up.
.mcp.json's mount uses ${V10R_REPO:-/home/ad/dev/velociraptor}, not ${CLAUDE_PROJECT_DIR} ${CLAUDE_PROJECT_DIR} does not expand inside .mcp.json args — an env-var-with-default is the only working substitution.
Every response stays well under Claude Code's 10k-token warning threshold Built in by construction: bounded excerpts (get_file_excerpt caps at 250 lines), capped result counts, no unbounded list dumps.

These aren't arbitrary style choices — each one maps to a specific failure mode observed by spawning the real server against a real client. Treat them as invariants of the MCP transport layer itself, the same way a pattern card's invariants field pins behavior for the patterns it describes.

Registry record shape

Each record in patterns.registry.json has id, title, category, summary, when_to_use, capabilities[], keywords[], depends_on[], and four reference lists — docs[], code[], tests[], showcases[] (each a { path, note?, kind? } ref, kind one of file | dir | route | anchor) — plus invariants[], emulation_notes[], and risk. See the file itself for the full shape and current entries; this doc won't re-derive what the schema already states plainly.

Hosted trust surfaces

Everything above describes the local stdio server a client spawns as an ephemeral container. The same pattern registry is now also served read-only over HTTP, at POST /api/mcp/public — no auth, same six tools, no mutation path to dispatch.

A second, unrelated surface, the private admin MCP at POST /api/mcp/admin, lives behind a bearer token and exposes a narrow set of demo-state tools over a small persistent domain — it shares only the transport plumbing with the public endpoint, not the registry or the trust level.

Full detail — trust-surface boundaries, auth, rate limits, the persistent demo state, the protected /admin/mcp page, env vars, and deployment caveats — lives in hosted-mcp.md.

Where to go next

  • See it live: /showcases/mcp — interactive dependency graph, architecture diagram, and protocol walkthrough
  • Run it, test it, register it, add a pattern: mcp/README.md
  • Read the code: mcp/server.ts (entry/lifecycle), protocol.ts (framing), registry.ts (types/validation/toposort), security.ts (containment), tools.ts (the six tools), snippet.ts (validate_snippet engine)
  • The pattern this MCP itself follows: multi-client-core.md — the registry's domain-shaped data plus a thin adapter is the same shape as every other pattern in this repo
← Back to Blueprint

Думаете, этот паттерн можно сделать лучше? Расскажите как.

Оставить отзыв