Files
mcp/README.md
Rene Nochebuena cc62906c6f feat(mcp): initial implementation — MCP server, framework indexer, 10 tools, 8 validation rules (v0.1.0)
Introduces code.nochebuena.dev/einherjar/mcp — the Einherjar Model Context Protocol
server. A remote, streamable-HTTP service that teaches AI assistants about every
other module of the framework: which package exposes which type, what each module
guarantees through its compliance tests, the canonical wiring shape for a service,
and whether a Go snippet follows the conventions. Indexes the framework on disk at
build time and ships a self-contained binary via go:embed; imports nothing from
other einherjar/* modules at compile time.

server (cmd/server):
- Streamable-HTTP MCP server built on github.com/modelcontextprotocol/go-sdk v1.0.0
- mcp.NewServer + mcp.NewStreamableHTTPHandler, served via net/http on EINHERJAR_MCP_ADDR
  (default :8080) and EINHERJAR_MCP_PATH (default /mcp)
- /healthz liveness endpoint; structured JSON logging via log/slog
- Loads the embedded data/index.json once at startup; in-memory for the process lifetime

indexer (cmd/indexer):
- Walks an Einherjar repository checkout (default ../), parses every sibling
  module's go.mod, README.md, CHANGELOG.md, docs/adr/ADR-*.md, doc.go package
  comments, every exported type/interface/func/method/const/var (via go/doc on
  go/parser ASTs), and compliance_test.go
- Captures module dependency edges by regex over each go.mod's require lines
  (einherjar/* paths only; self-reference filtered)
- Appends a synthetic "wire" module documenting canonical application wiring
  conventions, authored at internal/index/builtins/README.md and embedded via
  go:embed; participates in list_modules / get_module / get_example like a real module

internal/index:
- Schema einherjar.mcp/index/v1; types: Index, Module, SubPackage, Symbol, ADR,
  Example, Compliance, InterfaceAssert, ComplianceTest
- Build(repoRoot) → *Index walks the repo; BuildBuiltins() returns the synthetic
  wire module from the embedded markdown
- Load([]byte) → *Index validates the schema version on read
- FindModule, SearchSymbols helpers used by tools

internal/tools (10 tools):
- list_modules — enumerate every module with purpose + sub-packages
- get_module — package doc, dependencies, sub-packages, key symbols, ADRs,
  compliance counts; optional embedded README
- search_symbols — full-text across name, doc, sub-package, module; filterable by
  module and kind
- get_symbol — full signature, doc comment, source file:line for one symbol
- list_adrs — list ADRs across the framework or within one module
- get_adr — fetch one ADR's markdown body
- get_example — canonical usage snippets extracted from module READMEs and from
  the synthetic wire conventions
- get_compliance — interface assertions (var _ Iface = impl) and structural test
  names from a module's compliance_test.go
- get_changelog — full CHANGELOG.md markdown for one module
- validate_snippet — pattern-match a Go snippet against framework conventions

internal/rules (8 rules, registered via init() against a single registered slice):
- launcher.missing-run — launcher constructed but Run() never called
- launcher.no-components — launcher.New() called without any .Append(...)
- launcher.run-error-discarded — lc.Run() invoked as an ExprStmt (return ignored)
- logz.direct-env-read — os.Getenv("EINHERJAR_LOG_*") bypassing logz config
- web.server-not-appended — web/server constructed but not added to the launcher
- wire.hook-bad-signature — with<Feature>(...) first param is not launcher.Launcher
- wire.hook-outside-beforestart — repo/service/handler construction or route
  registration at the top level of a hook (outside lc.BeforeStart)
- wire.route-specific-after-param — /users/{id} registered before a sibling
  /users/me of the same length and method (chi would shadow the literal route)

Synthetic wire module (internal/index/builtins/README.md):
- Project layout (cmd/<app>/main.go + internal/wire/*.go + per-feature domain dirs)
- Canonical Run() shape: config → logger → infra (db, cache, pool, mc, srv) → cross-
  cutting (validator, permission provider) → launcher.New → lc.Append(infra...) →
  withMigrations / withSuperAdminSeed / withHealth / withFeature hooks → return lc.Run()
- Canonical with<Feature> hook shape: signature (launcher.Launcher first, server.Server
  second, deps last), single lc.BeforeStart closure containing all construction +
  route registration
- chi route ordering, srv.With(authz(...)) authorization, middleware helpers
  (authz / skipPublicPaths / skipMethodPath), tokenSignerAdapter pattern showing
  that the framework exposes Signer.Sign as a primitive and the application owns
  the access/refresh response shape

Packaging:
- Multi-stage Dockerfile that builds from the einherjar repository root
  (docker build -f mcp/Dockerfile .) so cmd/indexer can walk every sibling module
  at image-build time; runtime layer is gcr.io/distroless/static-debian12:nonroot
- 86-byte placeholder data/index.json committed once with `git add -f`; subsequent
  indexer runs overwrite it locally but the file is .gitignored
- .gitea/CODEOWNERS and pull_request_template.md mirror the sibling layout

Design notes:
- mcp depends on nothing in einherjar/* — it reads the framework via the filesystem
  at index time. This keeps mcp outside the framework dependency graph and lets it
  index any version of einherjar without versioning itself in lock-step.
- All structured-output tool responses initialise empty slices ([]Type{}) rather
  than relying on Go's nil-marshals-to-null default, so the SDK's JSON-schema
  output validator never rejects a tools/call result.
2026-05-29 18:12:45 +00:00

6.7 KiB

einherjar/mcp

version license go

Every warrior who knew the sagas had a skald nearby. This is yours.

code.nochebuena.dev/einherjar/mcp is the Einherjar Model Context Protocol server. It is a remote, streamable-HTTP service that teaches AI assistants about every other module of the framework: which package exposes which type, what each module promises via its compliance tests, the canonical wiring shape for a service, and whether a snippet of Go follows the conventions. Anyone who works in an Einherjar codebase can point their AI tools at one URL and get answers grounded in the actual source.


What Is Einherjar?

In Norse mythology, the Einherjar are the chosen warriors of Valhalla — selected not for glory, but to be ready for what comes after. They train. They prepare. They build the capability that others will rely on.

This framework is named for that purpose. Every module is a piece of that preparation: built carefully, documented for those who were never in the room, and designed to hold under pressure.


Commands

Command Purpose
cmd/server Streamable-HTTP MCP server. Embeds the framework index at build time and serves it over a single HTTP endpoint.
cmd/indexer Walks an Einherjar repository checkout and writes the framework index to data/index.json.

Tools

The server exposes ten tools to MCP-aware clients (Claude desktop, Claude Code, Cursor, Zed, and anything else that speaks MCP):

Tool Purpose
list_modules Enumerate every Einherjar module with its purpose and sub-packages
get_module Package doc, dependencies, sub-packages, key types, compliance counts; optional README
search_symbols Find a type, function, or interface by name, doc text, sub-package, or module
get_symbol Full signature, doc comment, and source location for one symbol
list_adrs List architectural decision records, optionally restricted to one module
get_adr Fetch a single ADR's markdown body
get_example Canonical usage snippet — pulled from module READMEs and from the synthetic wire conventions
get_compliance Interface assertions and structural test names from a module's compliance_test.go
get_changelog Full CHANGELOG.md markdown for one module
validate_snippet Pattern-match a Go snippet against framework conventions; returns findings with severity, hint, and line

validate_snippet ships eight wiring-convention rules at v0.1.0: launcher.missing-run, launcher.no-components, launcher.run-error-discarded, logz.direct-env-read, web.server-not-appended, wire.hook-bad-signature, wire.hook-outside-beforestart, and wire.route-specific-after-param.


Build Flow

                  build time                          runtime
   ┌──────────────────────────────┐      ┌──────────────────────────┐
   │ cmd/indexer ../              │      │ cmd/server               │
   │   walks every Einherjar      │      │   streamable-HTTP MCP    │
   │   module, parses Go pkgs,    │ ──▶  │   tools served from the  │
   │   reads READMEs + ADRs       │      │   embedded index.json    │
   │   ⇒ data/index.json (embed)  │      │                          │
   └──────────────────────────────┘      └──────────────────────────┘

The indexer is a separate command. It produces data/index.json which the server embeds via //go:embed, so the deployed binary is self-contained and reads nothing from disk at runtime.


Usage

Local run

# 1. Build the framework index from the sibling Einherjar modules
go run ./cmd/indexer ..

# 2. Build and run the server
go build -o bin/einherjar-mcp ./cmd/server
./bin/einherjar-mcp -addr :8080 -path /mcp

Container

# Build the image from the einherjar repo root so the indexer can walk every
# sibling module at image-build time.
docker build -f mcp/Dockerfile -t einherjar-mcp:0.1.0 .

docker run --rm -p 8080:8080 einherjar-mcp:0.1.0

Environment variables

Variable Default Effect
EINHERJAR_MCP_ADDR :8080 Listen address for the MCP server
EINHERJAR_MCP_PATH /mcp HTTP path served by the streamable-HTTP endpoint

Wiring Conventions (the synthetic wire module)

The MCP server ships a 15th, synthetic module called wire. It is not an Einherjar module — it documents the canonical application shape that uses Einherjar modules. The content lives at internal/index/builtins/README.md and is embedded at build time. AI assistants discover it via list_modules and read it via get_module and get_example the same way they read any real module.

The conventions captured: project layout (cmd/<app>/main.go, internal/wire/*.go, domain layout per feature), the fixed shape of Run(), the fixed shape of a with<Feature> hook (one lc.BeforeStart containing all construction and route registration), route-ordering rules for chi, the authz middleware helper, when to use skipPublicPaths vs skipMethodPath, and adapter patterns at the wire boundary.


Dependency Rules

contracts  (zero dependencies)
    ↑
  core, web, auth, …                  (every framework module)
    ↑
   mcp                                (reads framework source at index-time only)

mcp imports nothing from other Einherjar modules at compile time. The indexer parses the framework source on disk and writes a JSON blob; the server embeds that blob. This keeps mcp outside the framework dependency graph: it can index any version of einherjar without versioning itself in lock-step.


Verification

cd mcp/
go build ./...     # must compile clean
go vet ./...       # no warnings
go test ./...      # all tests pass
gofmt -l .         # no output

All four commands must produce clean output before a PR will be reviewed.


Architecture Decisions

No ADRs at v0.1.0. The structural decisions in this release (synthetic wire module, go:embed of the index, build-time-not-runtime knowledge model, primitives not response shapes) are captured in the framework-wide memory and in this README.


A blade is sharper when the warrior knows its name. This is what tells them.