Most tutorials hand you code to type. This one does the opposite: you will stand
up a FastMCP server on your Mac in which you never
write a line of application code. You describe behavior in plain English and
Cucumber/Gherkin scenarios, and Claude
Code (driven by subagents and slash commands
you configure once) writes and maintains server.py until every scenario
passes. Your entire job is to run make (or let CI run it).
The stack is Mac-native: uv for Python, make
as the control panel, behave (Python’s Cucumber) for executable specs, and a
launchd service for deployment. The novelty is the harness in .claude/
that turns a behavior description into working, tested code.
The idea
There are two kinds of files in this project:
- What you author: specifications and configuration — Gherkin
.featurefiles,CLAUDE.md, subagent and slash-command definitions, theMakefile, and CI. This is English, Gherkin, and YAML — never application logic. - What Claude Code authors:
server.py(and any new step wiring), generated to satisfy the specs and kept green.
The loop is: describe a tool → make add-tool (Claude writes spec + code) →
make test → make deploy. A human reads specs and output; Claude writes the
implementation.
Prerequisites
- macOS 13 (Ventura) or newer, with Homebrew (brew.sh).
- uv 0.5+ —
brew install uv; verifyuv --version. - Claude Code CLI, authenticated. Verify with
claude --version. Themake generateandmake add-tooltargets call it in headless mode (claude -p …). Install from claude.com/claude-code. - Xcode Command Line Tools (
xcode-select --install) formake. - Basic familiarity with Gherkin (
Given/When/Then). You will read and write it, but not much else.
Everything runs through uv and make. The only tool that needs credentials is
Claude Code itself.
Step 1: Scaffold and lock down hygiene
Create the workspace and a .gitignore first. Note it keeps .claude/settings.local.json
(personal overrides) out of git while keeping the shared harness in.
Create the files
mkdir -p claude-code-fastmcp-server-macos
cd claude-code-fastmcp-server-macos
touch .gitignore
Add the code: .gitignore
# Python
__pycache__/
*.py[cod]
.venv/
# uv
.uv/
# Tooling caches
.pytest_cache/
.behave_cache/
# Runtime / deploy artifacts
logs/
*.log
*.pid
# Claude Code local settings (keep shared config, ignore personal overrides)
.claude/settings.local.json
# OS / editor noise
.DS_Store
Detailed breakdown
.claude/settings.local.jsonis per-developer Claude Code state; the shared harness (.claude/agents/,.claude/commands/) is committed so every teammate and CI gets the same behavior.- The rest is the usual
uv/macOS hygiene: never commit.venv/, caches, logs, or Finder’s.DS_Store.
Step 2: Initialize the project with uv
Create the uv project and add the two dependencies: FastMCP (runtime) and
behave, the Python Cucumber runner (dev).
Create the files
uv init --name macmcp --no-workspace
rm -f main.py hello.py # remove the sample file uv scaffolds
uv add fastmcp
uv add --dev behave
Add the code: pyproject.toml
[project]
name = "macmcp"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"fastmcp>=3.4.3",
]
[dependency-groups]
dev = [
"behave>=1.3.3",
]
Detailed breakdown
behaveexecutes.featurefiles against Python step definitions — the Cucumber workflow for Python. It is the test runner Claude Code must satisfy.fastmcpis the only runtime dependency; the generated server imports it.
Step 3: Write the rules — CLAUDE.md
CLAUDE.md is the project’s constitution. Claude Code reads it automatically and
follows it. This is where you encode “specs are the source of truth” and “the
human only runs make.”
Create the file
touch CLAUDE.md
Add the code: CLAUDE.md
# CLAUDE.md
Guidance for Claude Code when working in this repository. **The human operator
does not write code by hand** — they describe behavior, run `make`, and review.
Claude Code writes every line of `server.py` and the step definitions.
## What this project is
A FastMCP server (`server.py`) whose behavior is defined by Cucumber/Gherkin
specs in `features/`. The workflow is spec-first: a feature file describes what a
tool does, and Claude Code implements the tool so the spec passes.
## Golden rules
1. **Specs are the source of truth.** Never change a tool's behavior without a
corresponding scenario in `features/*.feature`. If a request implies new
behavior, write the scenario first, then the implementation.
2. **The developer only runs `make`.** Every capability must be reachable through
a Makefile target. Do not tell the human to run raw `python`/`uv`/`behave`
commands — add or use a target instead.
3. **Use `uv` for everything.** `uv add` for dependencies, `uv run` to execute.
Never `pip install` or bare `python`.
4. **Keep `server.py` minimal and typed.** One `@mcp.tool` function per behavior,
with a docstring FastMCP can surface to clients. Preserve the runtime
transport switch (`MCP_TRANSPORT`).
5. **Always finish green.** After any change, run `make test` and ensure every
scenario passes before reporting done.
## Tools and agents available
- `/build` — regenerate `server.py` so every scenario in `features/` passes.
- `/add-tool "<description>"` — add a new tool: author a `.feature` scenario and
the step wiring, then implement it and run the suite.
- Subagent `bdd-author` — turns a plain-English tool description into Gherkin.
- Subagent `mcp-builder` — implements FastMCP tools to satisfy the specs.
## Definition of done
- `make test` passes (all Cucumber scenarios green).
- `make serve` starts the HTTP server without error.
- No hand-written TODOs or placeholders in `server.py`.
Detailed breakdown
- The golden rules are behavioral guardrails. “Specs are the source of truth”
forces spec-first work; “the developer only runs
make” keeps the human out of raw commands; “always finish green” makes Claude iterate untilbehavepasses rather than declaring victory early. - The “Tools and agents” section is documentation for Claude — it tells the model which slash commands and subagents exist so it uses them instead of improvising.
- Claude Code loads this file on every run in this directory, so these rules
apply to
make generate,make add-tool, and any interactive session.
Step 4: Author the executable specification
Behavior lives in Gherkin. This is the human-authored contract Claude Code must
satisfy. Each Scenario is a concrete example a client could run.
Create the file
mkdir -p features
touch features/tools.feature
Add the code: features/tools.feature
Feature: Text tools exposed over MCP
As an MCP client
I want to call the server's tools
So that I can process text through the model context protocol
Scenario: Count the words in a sentence
Given a running macmcp server
When I call "word_count" with text "one two three"
Then the numeric result is 3
Scenario: Slugify a title with punctuation
Given a running macmcp server
When I call "slugify" with text "Deploy FastMCP on macOS!"
Then the text result is "deploy-fastmcp-on-macos"
Scenario: Slugify collapses repeated separators
Given a running macmcp server
When I call "slugify" with text " A -- B "
Then the text result is "a-b"
Scenario: The server advertises its tools
Given a running macmcp server
When I list the available tools
Then the tool "word_count" is available
And the tool "slugify" is available
Detailed breakdown
- Each
Scenariois an executable example. “Count the words … Then the numeric result is 3” is both documentation and a test. Claude Code implementsword_countso this passes. - Edge cases are first-class. “collapses repeated separators” pins down
slugify’s tricky behavior so the generated code can’t quietly get it wrong. - The last scenario asserts discovery — that the server advertises the tools — which catches a tool that exists but was never registered.
- You write scenarios in the domain’s language; the wiring to code lives in the step definitions next.
Step 5: Wire the steps to the server
Step definitions translate each Gherkin line into a call against the server. They drive FastMCP’s in-memory client, so the suite is fast and needs no network or running process — ideal for CI. You author this once; Claude reuses the steps and only adds new ones when a scenario needs a genuinely new phrasing.
Create the files
mkdir -p features/steps
touch features/steps/tool_steps.py behave.ini
Add the code: features/steps/tool_steps.py
"""Step definitions for the MCP tool scenarios.
Each step drives the server through FastMCP's in-memory client, so the suite
runs fast and deterministically in CI with no network or subprocess.
"""
import asyncio
from behave import given, then, when
from fastmcp import Client
from server import mcp
def _run(coro):
"""Run an async coroutine from a synchronous behave step."""
return asyncio.run(coro)
async def _call(tool: str, arguments: dict):
async with Client(mcp) as client:
return (await client.call_tool(tool, arguments)).data
async def _tool_names():
async with Client(mcp) as client:
return [t.name for t in await client.list_tools()]
@given("a running macmcp server")
def step_server(context):
context.mcp = mcp
@when('I call "{tool}" with text "{text}"')
def step_call(context, tool, text):
context.result = _run(_call(tool, {"text": text}))
@when("I list the available tools")
def step_list(context):
context.tools = _run(_tool_names())
@then("the numeric result is {expected:d}")
def step_numeric(context, expected):
assert context.result == expected, f"expected {expected}, got {context.result!r}"
@then('the text result is "{expected}"')
def step_text(context, expected):
assert context.result == expected, f"expected {expected!r}, got {context.result!r}"
@then('the tool "{name}" is available')
def step_available(context, name):
assert name in context.tools, f"{name!r} not in {context.tools}"
Add the code: behave.ini
[behave]
show_timings = true
logging_level = WARNING
[behave.userdata]
# The in-memory MCP transport is used by the step definitions; no server URL
# is needed for `make test`.
Detailed breakdown
from server import mcpimports the FastMCP object that Claude Code will write in the next step. Passing it toClient(mcp)uses the in-memory transport — the client talks to the server object directly, with no socket._runbridges sync and async.behavesteps are synchronous, so each one drives the async client withasyncio.run.- The
{tool},{text},{expected:d}placeholders arebehave’s parse syntax;:dcoerces to an int so the numeric assertion is type-correct. behave.iniquiets FastMCP’s info logging to keep the test output readable. These steps are generic enough that most new tools reuse them verbatim — thebdd-authorsubagent is told to prefer them.
Step 6: Define the subagents
Now the harness. Two subagents split the work: bdd-author writes specs,
mcp-builder writes code. Each is a Markdown file with YAML frontmatter naming
it, describing when to use it, and limiting its tools.
Create the files
mkdir -p .claude/agents
touch .claude/agents/bdd-author.md .claude/agents/mcp-builder.md
Add the code: .claude/agents/bdd-author.md
---
name: bdd-author
description: Turns a plain-English description of an MCP tool into a Cucumber/Gherkin scenario and the matching behave step wiring. Use when adding or changing a tool's behavior.
tools: Read, Write, Edit, Glob, Grep
model: sonnet
---
You author executable specifications for a FastMCP server, in Gherkin.
When given a tool description:
1. Read `features/tools.feature` and `features/steps/tool_steps.py` to match the
existing style and reuse steps where possible.
2. Add one `Scenario` per observable behavior, including at least one edge case.
Use the existing step phrasings when they fit:
- `Given a running macmcp server`
- `When I call "<tool>" with text "<text>"`
- `Then the numeric result is <n>` / `Then the text result is "<s>"`
3. Only introduce a new step phrasing if no existing step expresses the check,
and then add its definition to `features/steps/tool_steps.py`.
4. Keep steps transport-free: they drive the server with FastMCP's in-memory
`Client(mcp)` so the suite stays fast and deterministic.
Do NOT implement the tool in `server.py` — that is `mcp-builder`'s job. Output
only the specs, then hand off.
Add the code: .claude/agents/mcp-builder.md
---
name: mcp-builder
description: Implements FastMCP tools in server.py so that every Cucumber scenario in features/ passes. Use after the specs exist to write or update the server code.
tools: Read, Write, Edit, Glob, Grep, Bash
model: sonnet
---
You implement a FastMCP server so its behavior matches the Gherkin specs.
Process:
1. Read every `features/*.feature` file and `features/steps/tool_steps.py` to
learn the required tools, inputs, and expected outputs.
2. Read the current `server.py`. Add or adjust one `@mcp.tool` function per
required behavior. Each tool must:
- have typed parameters and return type,
- carry a one-line docstring FastMCP can advertise,
- be pure and deterministic where possible.
3. Preserve the transport switch in `main()` (`MCP_TRANSPORT` selecting stdio vs
http) — never remove it.
4. Run `make test` with Bash and iterate until every scenario is green. Do not
stop on a red suite.
5. Keep the file minimal: no dead code, no placeholder TODOs.
Report the tools you added/changed and the final `make test` summary.
Detailed breakdown
- The frontmatter is the contract.
nameis how the agent is invoked;descriptionis what Claude Code reads to decide when to delegate;toolsscopes each agent (the spec author has noBash, so it can’t run or deploy anything, only read and write files);modelpicks the engine. - Separation of concerns is deliberate.
bdd-authoris forbidden from touchingserver.py, andmcp-builderis told to iterate againstmake test. Together they enforce spec-first development without a human in the loop. mcp-builderhasBashprecisely so it can runmake testand keep going until green — the “always finish green” rule fromCLAUDE.md, operationalized.
Step 7: Define the slash commands
Slash commands are the human-facing verbs. /build re-syncs code to specs;
/add-tool adds a behavior end to end. They orchestrate the subagents. The
Makefile calls these in headless mode.
Create the files
mkdir -p .claude/commands
touch .claude/commands/build.md .claude/commands/add-tool.md
Add the code: .claude/commands/build.md
---
description: Regenerate server.py so every Cucumber scenario in features/ passes
allowed-tools: Read, Write, Edit, Glob, Grep, Bash
---
Bring the server implementation in sync with the specs.
Current scenarios:
!`ls features/*.feature`
Steps:
1. Delegate to the `mcp-builder` subagent to implement or update `server.py` so
that every scenario in `features/` passes.
2. When it reports back, run `make test` yourself to confirm the suite is green.
3. If anything is red, iterate until it passes. Report the final `make test`
summary and the list of tools now exposed.
Do not ask the operator to run any commands — do it all and report the result.
Add the code: .claude/commands/add-tool.md
---
description: Add a new MCP tool from a plain-English description, spec-first
argument-hint: "<description of the tool>"
allowed-tools: Read, Write, Edit, Glob, Grep, Bash
---
Add a new tool to the server, specification-first. The tool to add:
**$ARGUMENTS**
Steps:
1. Delegate to the `bdd-author` subagent to add Gherkin scenarios for this tool
to `features/tools.feature` (plus any new step definitions), covering the
happy path and at least one edge case.
2. Delegate to the `mcp-builder` subagent to implement the tool in `server.py`.
3. Run `make test` and iterate until every scenario passes.
4. Report the scenarios added, the tool signature, and the final test summary.
The operator only described the behavior — you write all the code and specs.
Detailed breakdown
$ARGUMENTSinadd-tool.mdinterpolates whatever the operator passes after the command, somake add-tool DESC="reverse the text"becomes/add-tool reverse the text.!ls features/*.feature`` runs a shell command and injects its output into the prompt, so/buildalways sees the current spec files.allowed-toolspre-authorizes the tools these commands need, so they run unattended in headless mode without permission prompts.- The commands are thin orchestrators: they delegate the real work to the two
subagents and then verify with
make test.
Step 8: The server — written by Claude Code
Here is the twist made concrete. You do not write server.py; you ask
Claude Code to. The creation command is make generate (Step 9 wires it up),
which runs /build, which drives mcp-builder.
Create the file — by asking Claude Code
make generate # runs: claude -p "/build" --permission-mode acceptEdits
Claude Code reads the specs and writes server.py. The result is a small,
typed FastMCP server equivalent to the following (your generated version may
differ in comments or ordering, but it will satisfy every scenario):
Generated result: server.py
"""macmcp — a small FastMCP server.
Generated and maintained by Claude Code from the specs in features/ and
prompts/. You are not expected to edit this file by hand; run `make generate`
to (re)produce it from the behavior specs. Transport is chosen at runtime so
the same file serves local stdio dev and deployed HTTP.
"""
import os
from fastmcp import FastMCP
mcp = FastMCP("macmcp")
@mcp.tool
def word_count(text: str) -> int:
"""Count the whitespace-separated words in a block of text."""
return len(text.split())
@mcp.tool
def slugify(text: str) -> str:
"""Turn a title into a URL-safe, lowercase slug."""
cleaned = [c.lower() if c.isalnum() else "-" for c in text.strip()]
slug = "".join(cleaned)
while "--" in slug:
slug = slug.replace("--", "-")
return slug.strip("-")
def main() -> None:
if os.environ.get("MCP_TRANSPORT", "stdio").lower() == "http":
mcp.run(
transport="http",
host=os.environ.get("MCP_HOST", "127.0.0.1"),
port=int(os.environ.get("MCP_PORT", "8000")),
)
else:
mcp.run()
if __name__ == "__main__":
main()
Detailed breakdown
mcp = FastMCP("macmcp")is the server;@mcp.toolregisters each function as an MCP tool from its type hints and docstring — one per behavior in the specs.slugify’s loop is exactly what the “collapses repeated separators” scenario forced: repeated hyphens are squeezed and edges trimmed. The spec drove the implementation.main()keeps the transport switchCLAUDE.mdrequires: stdio for local dev, HTTP (for deployment) whenMCP_TRANSPORT=http.- Because this file is generated, treat the specs as what you edit. Change a
scenario, run
make generate, and Claude rewrites the code to match. Commit the generatedserver.pyso CI is deterministic.
Step 9: The Makefile — the only thing the human runs
Every capability is a make target, including the two that delegate to Claude
Code. Plain make prints help.
Create the file
touch Makefile
Add the code: Makefile
.DEFAULT_GOAL := help
# --- Deploy configuration (launchd user agent) --------------------------------
LABEL := com.macmcp.server
PLIST := $(HOME)/Library/LaunchAgents/$(LABEL).plist
UV := $(shell command -v uv)
DIR := $(shell pwd)
DOMAIN := gui/$(shell id -u)
# Description passed to `make add-tool DESC="..."`
DESC ?=
.PHONY: help install generate add-tool test serve smoke deploy undeploy status logs ci clean
help: ## Show this help screen
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
| awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-10s\033[0m %s\n", $$1, $$2}'
install: ## Sync runtime and dev dependencies
uv sync
generate: ## Have Claude Code (re)write server.py to satisfy the specs
claude -p "/build" --permission-mode acceptEdits
add-tool: ## Add a tool from a description: make add-tool DESC="reverse the text"
@test -n "$(DESC)" || (echo 'Usage: make add-tool DESC="what the tool does"' && exit 1)
claude -p "/add-tool $(DESC)" --permission-mode acceptEdits
test: ## Run the Cucumber (behave) suite
uv run behave
serve: ## Run the HTTP server in the foreground (Ctrl-C to stop)
MCP_TRANSPORT=http uv run python server.py
smoke: ## Call a tool against the running HTTP server
uv run python scripts/smoke.py
deploy: ## Render the launchd plist and start the service
@mkdir -p logs
@sed -e 's#@UV@#$(UV)#g' \
-e 's#@DIR@#$(DIR)#g' \
-e 's#@PATH@#$(dir $(UV)):/usr/bin:/bin#g' \
deploy/$(LABEL).plist.template > $(PLIST)
launchctl bootout $(DOMAIN) $(PLIST) 2>/dev/null || true
launchctl bootstrap $(DOMAIN) $(PLIST)
@echo "Deployed $(LABEL). Check: make status"
undeploy: ## Stop the service and remove the launchd plist
launchctl bootout $(DOMAIN) $(PLIST) 2>/dev/null || true
rm -f $(PLIST)
@echo "Removed $(LABEL)."
status: ## Show whether the service is registered and running
@launchctl print $(DOMAIN)/$(LABEL) 2>/dev/null \
| grep -E 'state =|pid =' || echo "$(LABEL) is not loaded."
logs: ## Tail the service log files
@tail -n 40 -f logs/server.out.log logs/server.err.log
ci: ## What CI runs: install then test (no Claude Code required)
uv sync
uv run behave
clean: ## Remove caches and local logs
rm -rf .pytest_cache logs __pycache__ features/steps/__pycache__
Detailed breakdown
generateandadd-toolcallclaude -p …— Claude Code’s headless mode.--permission-mode acceptEditslets it write files unattended. These are the only targets that need the Claude Code CLI.add-toolguardsDESCso a baremake add-toolprints usage instead of sending an empty prompt.cideliberately omits Claude Code. It runsuv sync+behaveagainst the committed, already-generated code, so continuous integration is deterministic and needs no credentials.deploy/status/logs/undeployare thelaunchdlifecycle from the companion deploy article;.DEFAULT_GOAL := helpmakes baremakeself-document.
Step 10: Deploy configuration and CI
Two more authored files: the launchd plist template (rendered by make deploy) and a CI workflow that runs the suite on every push. Also add the smoke
client the smoke target uses.
Create the files
mkdir -p deploy scripts .github/workflows
touch deploy/com.macmcp.server.plist.template scripts/smoke.py .github/workflows/ci.yml
Add the code: deploy/com.macmcp.server.plist.template
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.macmcp.server</string>
<key>ProgramArguments</key>
<array>
<string>@UV@</string>
<string>run</string>
<string>python</string>
<string>server.py</string>
</array>
<key>WorkingDirectory</key>
<string>@DIR@</string>
<key>EnvironmentVariables</key>
<dict>
<key>MCP_TRANSPORT</key>
<string>http</string>
<key>MCP_HOST</key>
<string>127.0.0.1</string>
<key>MCP_PORT</key>
<string>8000</string>
<key>PATH</key>
<string>@PATH@</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>@DIR@/logs/server.out.log</string>
<key>StandardErrorPath</key>
<string>@DIR@/logs/server.err.log</string>
</dict>
</plist>
Add the code: scripts/smoke.py
"""Connect to the running HTTP server and call a tool as a smoke test."""
import asyncio
import os
from fastmcp import Client
URL = os.environ.get("MCP_URL", "http://127.0.0.1:8000/mcp/")
async def main() -> None:
async with Client(URL) as client:
print("tools:", [t.name for t in await client.list_tools()])
result = await client.call_tool("slugify", {"text": "Built by Claude Code"})
print("slugify ->", result.data)
if __name__ == "__main__":
asyncio.run(main())
Add the code: .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
# The Cucumber suite runs on the committed (Claude-generated) code, so CI is
# deterministic and needs no Claude Code or API key.
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Run the Cucumber suite
run: make ci
Detailed breakdown
- The plist runs
uv run python server.pywithMCP_TRANSPORT=httpunderlaunchd, restarting on crash (KeepAlive) and logging tologs/.@UV@,@DIR@,@PATH@are filled in bymake deploybecauselaunchdruns with a bare environment. scripts/smoke.pyconnects over HTTP to the deployed server — the same code a real client uses — and is whatmake smokeruns.- CI runs
make ci, which tests the committed generated code. The comment states the deliberate choice: CI does not call Claude Code, so it is fast, free, and reproducible. Regeneration (make generate) is a local or credential-gated step, never a gate on merging.
Step 11: The workflow in action
With the harness in place, here is the entire developer experience.
Regenerate the server from specs and run the suite:
make generate # Claude Code writes server.py to satisfy features/
make test
Expected make test output:
1 feature passed, 0 failed, 0 skipped
4 scenarios passed, 0 failed, 0 skipped
13 steps passed, 0 failed, 0 skipped
Add a brand-new tool without writing code or specs yourself:
make add-tool DESC="reverse the characters in the text"
Claude Code delegates to bdd-author (which appends Gherkin scenarios for a
reverse tool, including an edge case), then to mcp-builder (which implements
reverse in server.py), then runs make test until green — and reports back.
You reviewed a description; it produced spec + code + passing tests.
Deploy and smoke-test the running service:
make deploy
make status # state = running, a live pid
make smoke
# tools: ['word_count', 'slugify']
# slugify -> built-by-claude-code
make undeploy
Let CI do it: on every push, GitHub Actions runs make ci — uv sync plus
the Cucumber suite — against the committed code. Merges are gated on green specs,
with no Claude Code credentials in CI.
Troubleshooting
make generate/make add-toolsaysclaude: command not found. Install and authenticate the Claude Code CLI; verify withclaude --version. Only these two targets need it —make test,deploy, andcido not.make testfails after a spec change. That is the system working: the code no longer matches the spec. Runmake generateto have Claude Code updateserver.py, thenmake testagain.- Claude Code edits the wrong file or asks for permission. Check that
CLAUDE.md,.claude/agents/, and.claude/commands/are committed at the repo root and thatallowed-toolsin each command lists the tools it needs. behavecan’t importserver. Run it viamake testfrom the project root so the root is on the import path; the step file doesfrom server import mcp.- CI fails but local passes. CI runs
make cion exactly the committed code. Make sure the generatedserver.pyis committed, not left as a local-only regeneration.
Recap
You built and deployed a FastMCP server on macOS in which the application code was written entirely by Claude Code:
- You authored specs and configuration — Gherkin
.featurefiles,CLAUDE.md, two subagents, two slash commands, theMakefile, and CI. - Claude Code authored
server.py, driven by/buildand/add-toolthrough themcp-builderandbdd-authorsubagents, iterating until the Cucumber suite passed. behaveturns Gherkin into executable tests that both specify behavior and gate merges.- The human only runs
make—generate,add-tool,test,deploy— and CI runsmake cion the committed code with no credentials.
Next improvements
- Add a
make regen-checktarget (and CI job, gated on a Claude Code token) that runsmake generateand fails if the committedserver.pydrifts from what the specs would produce. - Add a pre-commit hook that runs
make testso a red suite can never be committed. - Grow the harness: a
refactorsubagent, or a/reviewcommand that has Claude Code critique its own generated code againstCLAUDE.md. - Point Claude Desktop or Claude Code at the deployed server (see the companion Connect to a FastMCP Server from macOS) to use the generated tools from a real client.