Minor release. The indexer named composite types but could not describe their shape: every declaration was truncated at the first brace, so a struct symbol carried only its `type X struct` header and an interface symbol only its `type X interface` header. Field names, field types, and — most painfully — struct tags such as `env:"EINHERJAR_PG_HOST"` were dropped, as were the method sets of every port interface. An assistant could be told that `db-postgres` has a `Config` and a `Provider`, but not what env vars configure the one or what methods the other requires. This change captures both. internal/index (schema): - Symbol gains two optional fields. `fields` ([]Field) carries a struct's field set — name, type, raw struct tag (surrounding backticks stripped), doc comment, and an `embedded` marker. `methods` ([]Method) carries an interface's method set — name, signature without the leading `func`, and doc comment, including embedded interfaces. Both are omitempty and absent for every other kind. - SchemaVersion is deliberately unchanged. The two additions are additive and omitempty, so an older consumer parses the new index unchanged; per the existing rule the constant only bumps on a breaking format change. internal/index (builder): - collectSymbols now inspects each type's TypeSpec and, for a *ast.StructType or *ast.InterfaceType, fills the new Symbol members. A grouped field declaration (`x, y int`) yields one Field per name; an embedded field or interface yields an entry with an empty name. - New helpers: typeSpecType (underlying type expr of a lone type spec), extractFields, extractIfaceMethods, fieldDoc (doc comment or trailing line comment), and nodeString — a non-truncating printer used for field types, tags, and method signatures, distinct from formatNode which keeps truncating to produce the one-line header. internal/index (search): - matches() now also tests the query against struct field names, field types, and struct tags, and against interface method names and signatures. A query like an env-var key or a method name now resolves to the type that declares it. internal/tools: - get_symbol and search_symbols descriptions updated to advertise the new struct-field and interface-method coverage. No input/output schema change beyond the additive Symbol fields, which get_symbol already returns whole. internal/index (tests): - New builder_test.go — the package previously had no tests. Builds a temporary module fixture and asserts capture of struct fields (with tags and docs), embedded fields, interface methods (with signatures and docs), embedded interfaces, and discovery of a struct by one of its struct tags (the failure mode that motivated the change). Docs: - CHANGELOG.md gains an [Unreleased] entry; README.md tool table updated to state that get_symbol returns struct fields and interface methods and that search_symbols matches fields, tags, and methods. No new dependencies. The committed data/index.json placeholder is untouched — the index is regenerated at image build (Dockerfile runs cmd/indexer), so a deployment must be rebuilt to serve the richer index; a server still running the prior image keeps serving the older, member-less one. Reviewed-on: #1 Co-authored-by: Rene Nochebuena Guerrero <rene@nochebuena.dev> Co-committed-by: Rene Nochebuena Guerrero <rene@nochebuena.dev>
einherjar/mcp
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, module — or by a struct field/tag or an interface method |
get_symbol |
Full signature, doc comment, and source location for one symbol — plus struct fields (with tags) and interface method sets |
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.
Deployment
The server is a single self-contained static binary. There is no canonical hosting shape — pick whichever matches the rest of your infrastructure. Two patterns cover most cases:
-
Container. The
Dockerfileat the module root produces a distroless runtime image. Build from the einherjar repository root so the indexer can reach 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 -
Systemd / socket-activated binary. The server detects an inherited listener via
github.com/coreos/go-systemd/v22/activationand uses it when present, falling back to-addrTCP binding otherwise. Same binary works in both modes — no flag, no env var. Drop the binary into/opt/<somewhere>/and write a.socket+.servicepair that matches your conventions.
Whatever shape you pick, the public-facing reverse proxy must keep response
buffering off on the /mcp location. Streamable MCP delivers tool results
via Server-Sent Events; default nginx, Envoy, or Caddy buffering batches the
stream and breaks Claude's session before the first event arrives. For nginx
that means proxy_buffering off; proxy_cache off; proxy_request_buffering off; chunked_transfer_encoding on; plus an extended proxy_read_timeout for
long-lived sessions.
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.