Go MCP template with hexagonal architecture, cobra + viper, DI, and tool handlers grouped by concept.
Before making changes in this repository, load and apply these local skills:
.codex/skills/clean-code.codex/skills/golang-pro.codex/skills/sql-pro.codex/skills/tdd-workflows-tdd-cycle.codex/skills/tdd-workflows-tdd-red.codex/skills/tdd-workflows-tdd-green.codex/skills/tdd-workflows-tdd-refactor
Notes:
- The scaffold reads these skills from
template/skills/in this repository and copies them into.codex/skills/in the generated project. .claude/skillsand.opencode/skillsare symlinks to.codex/skillsin the generated project.
context-distill/
├── cmd/server/main.go
├── distill/
│ ├── application/
│ └── domain/
├── mcp/
│ ├── application/
│ └── domain/
├── platform/
│ ├── config/
│ ├── di/
│ ├── mcp/
│ │ ├── commands/
│ │ ├── server/
│ │ ├── tools/
│ └── openai/
└── shared/
├── ai/domain/
└── config/domain/
- Entry point:
cmd/server/main.go. - Command-line parameters are managed with
cobraand bound toviper. - MCP server wiring lives in
platform/mcp/server/server.go. - Tool definitions live in
platform/mcp/tools/and are registered from the command runner. - Default transport:
stdio.
Included example:
- Tools
distill_batchanddistill_watch. - New retrieval tool:
search_code.
- Never use hardcoded string literals for DI dependency names.
- Every
di.Def{Name: ...}must use a label constant defined inplatform/di/labels.go. - Every
ctn.Get(...)must use label constants too. - If a new dependency is added, add its label first and then use that constant in both definition and retrieval.
- Create a domain port under
mcp/domain/<feature>/if business logic is needed. - Add the use case in
mcp/application/<feature>/. - Implement infrastructure adapters in
platform/mcp/<feature>/. - Create the tool definition and handler in
platform/mcp/tools/<feature>.go. - Register dependencies in
platform/di/container.go. - Register the tool in the command runner before server startup.
platform -> shared + mcp/application + mcp/domain
mcp/application -> mcp/domain
cmd -> platform + shared
Do not allow shared or mcp/domain to import platform.
- Do not create giant monolithic services or repositories.
- Split responsibilities into multiple focused
.gofiles, followingclean-code(SRP, small units, clear naming). - Prefer composition of small components over one large file handling everything.
If a specific domain appears (example: post), place it at project root:
post/
├── application/
└── domain/
This is only a structural example; no post boilerplate is generated by default.
- New tools are registered in startup wiring.
- Flags are bound via
viperand support env overrides. - No
platformimports insidesharedormcp/domain. - OpenAI provider config is read via config repository.
-
go test ./...andgo vet ./...pass.
Use distill_batch for ANY command output before sending it to the LLM.
The only exception is when the raw output is trivially small (≤ 5–8 lines)
and you can already read it at a glance — in that case, skip the call.
If you are unsure whether to distill: distill. The cost of an unnecessary distill call is near zero; the cost of flooding the context with raw output is high.
-
Every
distill_batchcall MUST include an output contract inquestion. Tell the distiller exactly what format to return:- "Return only PASS or FAIL."
- "Return valid JSON with keys: severity, file, message."
- "Return only filenames, one per line."
-
One task per call. Don't combine unrelated questions.
-
Prefer machine-checkable formats (PASS/FAIL, JSON, one-item-per-line) so you can act on the result programmatically.
| Source command | question |
|---|---|
go test ./... |
"Did all tests pass? Return only PASS or FAIL. If FAIL, list failing test names, one per line." |
git diff |
"List only changed file paths, one per line." |
| CI / build logs | "Return valid JSON array. Each object has keys: severity, file, message." |
docker logs <ctr> |
"Summarise errors only. One bullet per distinct error." |
find / ls -lR |
"Return only paths that match *.go, one per line." |
When you have two snapshots of the same source, use distill_watch to extract
only what changed.
question |
previous_cycle |
current_cycle |
|---|---|---|
| "What changed in failure count? One short sentence." | snapshot T-1 | snapshot T |
| "Return only newly failing services, one per line." | status at T-1 | status at T |
Skip the call only when one of these is true:
- The output is ≤ 5–8 lines and already human-readable at a glance.
- You need the exact raw bytes for compliance, audit, or binary integrity.
- You are debugging an interactive terminal exchange where the precise character-by-character flow matters.
In every other case, distill first.
Before opening many files, use search_code when task is locating:
- symbol definitions/usages
- config loading points
- entrypoints
- provider-related code
search_code rules:
- Every call must include
questionwith explicit output contract. - Use one mode per call:
text | regex | symbol | path. - Keep output machine-checkable when possible (JSON,
file:line, one-per-line).
Examples:
context-distill search_code --query "provider_name" --mode text --question "Return only file:line, one per line."context-distill search_code --query "LoadDistillConfig" --mode symbol --question "Return likely definitions first as file:line, one per line."
Use Caveman Sill