eng-discipline-mcp
v1.0.0
Published
MCP server that enforces a structured, senior-engineer workflow on AI coding assistants: requirements, architecture, data model, and security review must all pass before any code is written.
Downloads
162
Maintainers
Readme
🛡️ eng-discipline-mcp
The MCP server that makes AI assistants behave like senior engineers — before a single line of code is written.
Turn "vibe coding" into engineered coding.
An MCP (Model Context Protocol) server that forces AI coding assistants through a structured, senior-engineer workflow before they are allowed to touch your codebase.
Instead of letting an assistant jump straight from "build me an app" to a wall of code,
eng-discipline-mcp manages a per-project session with sequential, gated phases. Each phase
must be completed and accepted before the next one unlocks — and the ordering is enforced in
TypeScript logic, not prompt text. Call a tool out of order and it returns an explicit error
naming exactly what's missing and which tool to call first.
The original Master Software Engineering Prompt is embedded verbatim and injected into every phase tool response, so the model always operates from the canonical instructions — never a paraphrase.
🗺️ The pipeline
┌────────┐ ┌──────────────┐ ┌───────────────┐ ┌────────────┐ ┌────────────────┐ ┌─────────────┐
│ INTAKE │ ───▶ │ REQUIREMENTS │ ───▶ │ ARCHITECTURE │ ───▶ │ DATA_MODEL │ ───▶ │ SECURITY_REVIEW │ ───▶ │ READY_FOR_CODE │
└────────┘ └──────────────┘ └───────────────┘ └────────────┘ └────────────────┘ └─────────────┘
start_project submit_req. propose_arch. design_data_model review_security get_full_spec| Phase | Completing tool | What must be provided |
| --- | --- | --- |
| INTAKE | start_project | name, description, target users, problem, expected scale, deployment target, preferred stack |
| REQUIREMENTS | submit_requirements | functional + non-functional requirements (10 categories), constraints, explicit assumptions |
| ARCHITECTURE | propose_architecture | components with purpose / rationale / dependencies / failure modes; complexity-justification rule enforced |
| DATA_MODEL | design_data_model | entities with fields, types, keys, relationships, constraints, indexes, audit fields |
| SECURITY_REVIEW | review_security | trust boundaries, server-side validation, what is exposed to untrusted clients |
| READY_FOR_CODE | get_full_spec | final gate — emits the assembled spec as structured markdown |
Only when the last gate passes is the assistant allowed to write code — and the spec it returns becomes the source of truth for the implementation.
🚦 The enforced rules (code, not vibes)
🧱 Phase gating. submit_requirements, propose_architecture, design_data_model,
review_security, and get_full_spec each require the project to be in the matching phase.
Calling one early returns an isError result structured like:
Tool call rejected: 'propose_architecture'.
Project: 'invoices-cli' (id=9f0a…)
Phase gating: 'propose_architecture' requires the project to be in phase 'ARCHITECTURE',
but it is in phase 'REQUIREMENTS'.
Missing:
- Functional requirements have not been recorded.
- Non-functional requirements have not been recorded.
- Constraints have not been recorded.
- Assumptions have not been recorded.
Call this tool first: submit_requirements📋 Completeness. submit_requirements rejects any submission where functional requirements,
non-functional requirements, constraints, or assumptions are empty.
⚖️ Over-engineering / complexity justification. During propose_architecture, any component
that adds high operational complexity (queues, message brokers, Redis, Kubernetes, microservices,
event-driven infrastructure, serverless, …) must include a complexity_justification. Missing
justification → hard reject. A justification that contradicts a small stated scale → warning flag,
stored on the project and surfaced in status and the final spec. Dangling component dependencies
and unaddressed architecture concerns (auth, database, monitoring, deployment, …) are also flagged
as warnings.
🧬 Data model integrity. design_data_model rejects primary keys, unique keys, indexes, or
relationships that reference fields/entities that do not exist in the model.
🧰 Tools
| Tool | Purpose |
| --- | --- |
| start_project | Create a project session and record intake. Returns project_id. |
| submit_requirements | Record requirements; advances to ARCHITECTURE. |
| propose_architecture | Record architecture; advances to DATA_MODEL. |
| design_data_model | Record data model; advances to SECURITY_REVIEW. |
| review_security | Record trust boundaries; advances to READY_FOR_CODE. |
| get_project_status | Current phase, completed phases, missing items, next tool to call. |
| get_full_spec | Assembles requirements + architecture + data model + security review as markdown. Only when READY_FOR_CODE. |
Each phase tool also appends the original corresponding section(s) of the Master Engineering Prompt to its response, so the calling model always sees the canonical instructions:
| Phase tool | Original prompt sections embedded |
| --- | --- |
| start_project | §1 PROJECT CONTEXT |
| submit_requirements | §3 REQUIREMENTS ANALYSIS |
| propose_architecture | §2 YOUR FIRST RULE + §4 ARCHITECTURE FIRST |
| design_data_model | §6 DATA MODEL |
| review_security | §5 SYSTEM BOUNDARIES + §8 AUTH & AUTHORIZATION + §9 SECURITY ENGINEERING + §10 INPUT VALIDATION |
src/master-prompt.ts is generated from MASTER SOFTWARE ENGINEERING PROMPT.md (repo root) by
npm run generate:master-prompt. It holds the prompt verbatim as named constants —
MASTER_PROMPT_FULL, SECTION_PREAMBLE, one SECTION_* per numbered section (1–41),
SECTION_FINAL_INSTRUCTION, and the SECTION_FOR_PHASE mapping above. The generator asserts a
byte-exact round-trip before writing; regenerate it whenever you edit the source .md.
📦 Resources
| Resource | Description |
| --- | --- |
| eng-discipline://master-prompt | The full Master Software Engineering Prompt, verbatim. |
| eng-discipline://project/{projectId}/status | Live project phase/status as JSON — read without a tool call. |
🚀 Quick start
git clone <this-repo> && cd eng-discipline-mcp
npm install
npm run build # regenerates src/master-prompt.ts, then compiles to dist/
npm run smoke # end-to-end test: drives a full project through all 6 phases over real stdioData is persisted as one JSON file per project under data/ (created automatically). Set
ENG_DISCIPLINE_DATA_DIR to store projects elsewhere. Writes are atomic (tmp-file + rename) so a
crash never leaves a half-written project file.
Run directly:
npx . --help # from the repo
npx eng-discipline-mcp # after publishing to npm🔌 Connect from your AI client
eng-discipline-mcp speaks stdio MCP (it launches as a local subprocess and talks over
stdin/stdout). That's the shape nearly every AI coding client supports out of the box — no remote
URL, tokens, or hosting needed. Just register it with your client's standard local server config
and point it at npx -y eng-discipline-mcp.
Not published yet? Swap the command below for a local checkout: use
node /absolute/path/to/eng-discipline-mcp/dist/index.jsinstead ofnpx -y eng-discipline-mcp(adjust the array/args shape per client). On Windows, escape backslashes (C:\\path\\to\\...) in JSON configs.
Quick reference
| Client | Config file | Top-level key | Transport field |
| --- | --- | --- | --- |
| Claude Code | .mcp.json or claude mcp add | mcpServers | inferred from command |
| Claude Desktop | claude_desktop_config.json | mcpServers | inferred from command |
| OpenCode | opencode.json | mcp | explicit type: "local" |
| Cursor | .cursor/mcp.json | mcpServers | inferred from command |
| Windsurf | ~/.codeium/windsurf/mcp_config.json | mcpServers | inferred from command |
| VS Code (Copilot) | .vscode/mcp.json | servers | explicit type: "stdio" |
| Zed | settings.json | context_servers | inferred from command |
🤖 Claude Code
Add to .mcp.json (project scope) or your user config:
{
"mcpServers": {
"eng-discipline": {
"command": "npx",
"args": ["-y", "eng-discipline-mcp"],
"env": {
"ENG_DISCIPLINE_DATA_DIR": "/absolute/path/to/eng-discipline-data"
}
}
}
}Or via the CLI (local is the default scope; use --scope user for all projects):
claude mcp add eng-discipline -- npx -y eng-discipline-mcpRestart Claude Code and confirm with claude mcp list.
💬 Claude Desktop
Edit claude_desktop_config.json (Claude → Settings → Developer → Edit Config):
{
"mcpServers": {
"eng-discipline": {
"command": "npx",
"args": ["-y", "eng-discipline-mcp"]
}
}
}Windows: %APPDATA%\Claude\claude_desktop_config.json · macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
⚡ OpenCode
Add to opencode.json (or opencode.jsonc):
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"eng-discipline": {
"type": "local",
"command": ["npx", "-y", "eng-discipline-mcp"],
"enabled": true,
"environment": {
"ENG_DISCIPLINE_DATA_DIR": "/absolute/path/to/eng-discipline-data"
}
}
}
}OpenCode is the one client that requires an explicit type: "local" and an array-form command.
cwd and environment are supported if you need to pin the working directory or env vars.
🖱️ Cursor
Add to .cursor/mcp.json (per-project) or ~/.cursor/mcp.json (global):
{
"mcpServers": {
"eng-discipline": {
"command": "npx",
"args": ["-y", "eng-discipline-mcp"]
}
}
}🌊 Windsurf
Add to ~/.codeium/windsurf/mcp_config.json:
{
"mcpServers": {
"eng-discipline": {
"command": "npx",
"args": ["-y", "eng-discipline-mcp"],
"env": {
"ENG_DISCIPLINE_DATA_DIR": "/absolute/path/to/eng-discipline-data"
}
}
}
}📝 VS Code (via Copilot / MCP extension)
Add to .vscode/mcp.json — note the top-level key is servers, not mcpServers:
{
"servers": {
"eng-discipline": {
"type": "stdio",
"command": "npx",
"args": ["-y", "eng-discipline-mcp"]
}
}
}🧑💻 Zed
Add to your settings.json — Zed nests the command under a command object:
{
"context_servers": {
"eng-discipline": {
"command": {
"path": "npx",
"args": ["-y", "eng-discipline-mcp"]
}
}
}
}⚠️ The differences to watch for
- Top-level key:
mcpServers(Claude Code, Claude Desktop, Cursor, Windsurf) vsmcp(OpenCode) vsservers(VS Code) vscontext_servers(Zed). - Transport field: most clients infer stdio from a
command; OpenCode and VS Code demand an explicittype("local"/"stdio"). - Command shape: OpenCode wants an array (
["npx", "-y", "…"]); everyone else wantscommand+args. - Env vars: Claude Code, Claude Desktop, Windsurf, and OpenCode pass them via
env/environmentso you can pinENG_DISCIPLINE_DATA_DIR.
💾 Persistence & environment
| Variable | Default | Description |
| --- | --- | --- |
| ENG_DISCIPLINE_DATA_DIR | <cwd>/data | Where project sessions are stored (one JSON file per project). Point this at a stable path so sessions survive across tools and machines. |
Project files are written atomically (tmp-file + rename), so a crash never leaves a half-written
session. A corrupt file surfaces a descriptive CorruptProjectError telling you exactly which file
to inspect.
🎬 End-to-end walkthrough
A full session proving every phase gate, from intake to the final spec:
1. Intake
start_project(name="Invoices CLI", description="...", target_users="...",
problem="...", scale="single operator, tens of invoices", deployment_target="local laptop",
preferred_stack="Node.js, SQLite")
→ INTAKE RECORDED … Phase advanced: INTAKE → REQUIREMENTS2. Requirements
submit_requirements(project_id, functional=[...], non_functional={performance: "..."},
constraints={budget: "..."}, assumptions=[...])
→ REQUIREMENTS RECORDED … Phase advanced: REQUIREMENTS → ARCHITECTURE3. Trying to skip ahead is rejected
design_data_model(project_id, entities=[...])
→ Phase gating: 'design_data_model' requires phase 'DATA_MODEL', but it is in phase 'ARCHITECTURE'.
Missing: The architecture has not been proposed. Call this tool first: propose_architecture4. Architecture (with the complexity rule in action)
propose_architecture(project_id, components={
backend: { purpose: "...", rationale: "...", dependencies: [], failure_modes: ["..."], complexity_justification: "..." },
redis_cache: { purpose: "...", rationale: "...", dependencies: ["backend"], failure_modes: ["..."],
complexity_justification: "single operator — not justified" }
})
→ ARCHITECTURE RECORDED … warning flag raised: component 'redis_cache' adds complexity
(redis) but the stated scale reads as small. Review before writing code.
Phase advanced: ARCHITECTURE → DATA_MODELOmitting complexity_justification on redis_cache would instead reject the whole submission
with an OverengineeringError until it is justified or removed.
5. Data model
design_data_model(project_id, entities=[{ name: "Invoice", fields: [...], keys: {primary_key: "id"},
relationships: [], constraints: ["total >= 0"], audit_fields: ["created_at", "updated_at", "deleted_at"] }])
→ DATA MODEL RECORDED … Phase advanced: DATA_MODEL → SECURITY_REVIEW6. Security review
review_security(project_id, trust_boundaries={ summary: "...", client_side: {...},
server_side: { validated: ["..."] }, exposed_to_clients: [...], trust_assumptions: [...] })
→ ALL PHASES COMPLETE — project is now READY_FOR_CODE.7. Final gate
get_project_status(project_id)
→ phase: READY_FOR_CODE, ready_for_code: true, next_tool: get_full_spec
get_full_spec(project_id)
→ # Engineering Specification: Invoices CLI … full markdown spec.Only now is the assistant allowed to write code — and the spec it returns is the source of truth for the implementation.
🗂️ Project layout
MASTER SOFTWARE ENGINEERING PROMPT.md # the source prompt; src/master-prompt.ts is generated from it
src/
index.ts # MCP server wiring + stdio entry point
master-prompt.ts # GENERATED — Master Engineering Prompt, verbatim, as named string constants
constants.ts # phases, tool↔phase mapping, complexity keywords
schemas/toolSchemas.ts # zod schemas + inferred input types for every tool
storage/types.ts # Project, phase-section record types
storage/store.ts # JSON file store, atomic writes
domain/phases.ts # phase machine + hard gate enforcement
domain/completeness.ts # requirements completeness gate
domain/complexity.ts # over-engineering / scale-justification gate
domain/dataModel.ts # data-model integrity gate
domain/status.ts # status assembly + rendering
domain/spec.ts # get_full_spec markdown assembly
tools/ # one file per MCP tool + registry
resources/ # master-prompt + live project-status resources
scripts/
generate-master-prompt.mjs # splits the .md into SECTION_* constants (verbatim, round-trip checked)
smoke-test.mjs # end-to-end proof: full project through all 6 phases over stdio