Files
mcp/README.md
T
Rene Nochebuena 262eade93d feat(mcp): scaffold tool, config/env conventions, and scaffold-hygiene rules
Major release to v1.0.0, aligned with the v1.0.0 framework. The MCP documented
the wiring conventions but not the config half of a project, and its example
main.go omitted the godotenv autoload — so an assistant starting a service from
zero still hand-rolled main.go and the launcher, and got config wrong. This adds
a first-class scaffold, completes the config/.env.example conventions, and adds
rules that catch the "mess in main" pattern.

internal/tools:
- New get_scaffold: returns the canonical minimum application scaffold as
  ready-to-write files (main.go with godotenv autoload + wire.Run(), wire.go,
  a composed config.go, a health hook, .env.example), with import paths filled
  from a `module` argument. Registered in tools.go.

internal/rules:
- Three new validate_snippet rules, appended in scaffold_rules.go: main.dirty
  (launcher/components built in main instead of internal/wire),
  main.no-godotenv-autoload (a wire-convention main that never loads .env), and
  config.raw-getenv (an EINHERJAR_* var read via os.Getenv instead of composing
  the component Config; EINHERJAR_LOG_* stays with logz.direct-env-read).
- scaffold_rules_test.go — internal/rules had no tests; asserts each new rule
  fires and that a clean main is not flagged.

internal/index (builtins):
- The synthetic wire module gains a Config section (compose the framework's
  component configs, load with caarlos0/env, APP_* app fields / EINHERJAR_*
  framework fields) and a Config & .env.example discipline (every env var the
  config reads is documented in .env.example, kept in lock-step).
- main.go now shows the `_ "github.com/joho/godotenv/autoload"` blank import,
  previously omitted. The assembly file is renamed launcher.go -> wire.go.
- Migrations and seeding removed from the documented scaffold — developer
  choices, not framework conventions. Re-synced against iron-dough-api / pei-api.

Version:
- Badge and serverVersion const were stale at v0.1.0; both now v1.0.0.

Docs:
- README (eleven tools, eleven validation rules) and CHANGELOG updated.

No new dependencies. The wire conventions are embedded at build time
(//go:embed builtins/README.md) and the new tool and rules are compiled in, so a
deployment must be rebuilt to serve them; a server still running the v0.2.0
binary keeps serving the old conventions until redeployed.
2026-08-07 12:21:00 -06:00

205 lines
8.5 KiB
Markdown

# einherjar/mcp
[![version](https://img.shields.io/badge/version-v1.0.0-5C4EE5?style=flat-square)](https://code.nochebuena.dev/einherjar/mcp)
[![license](https://img.shields.io/badge/license-AGPL--3.0-22863A?style=flat-square)](LICENSE)
[![go](https://img.shields.io/badge/Go-1.26+-00ADD8?style=flat-square&logo=go&logoColor=white)](https://go.dev)
> 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 **eleven** 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_scaffold` | The canonical **minimum application scaffold** as ready-to-write files — a clean `main.go`, `internal/wire/wire.go`, a composed `internal/config`, a health hook, and `.env.example`. Use it when starting a new Einherjar service |
| `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 **eleven** wiring-convention rules at v1.0.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`, `wire.route-specific-after-param`,
`main.dirty`, `main.no-godotenv-autoload`, and `config.raw-getenv`.
---
## 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
```bash
# 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
```bash
# 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
```bash
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 [`Dockerfile`](Dockerfile) at 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:
```bash
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/activation` and uses it when
present, falling back to `-addr` TCP binding otherwise. Same binary works in
both modes — no flag, no env var. Drop the binary into `/opt/<somewhere>/`
and write a `.socket` + `.service` pair 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.*