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 .feature files, CLAUDE.md, subagent and slash-command definitions, the Makefile, 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 testmake 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; verify uv --version.
  • Claude Code CLI, authenticated. Verify with claude --version. The make generate and make add-tool targets call it in headless mode (claude -p …). Install from claude.com/claude-code.
  • Xcode Command Line Tools (xcode-select --install) for make.
  • 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.json is 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

  • behave executes .feature files against Python step definitions — the Cucumber workflow for Python. It is the test runner Claude Code must satisfy.
  • fastmcp is 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 until behave passes 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 Scenario is an executable example. “Count the words … Then the numeric result is 3” is both documentation and a test. Claude Code implements word_count so 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 mcp imports the FastMCP object that Claude Code will write in the next step. Passing it to Client(mcp) uses the in-memory transport — the client talks to the server object directly, with no socket.
  • _run bridges sync and async. behave steps are synchronous, so each one drives the async client with asyncio.run.
  • The {tool}, {text}, {expected:d} placeholders are behave’s parse syntax; :d coerces to an int so the numeric assertion is type-correct.
  • behave.ini quiets FastMCP’s info logging to keep the test output readable. These steps are generic enough that most new tools reuse them verbatim — the bdd-author subagent 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. name is how the agent is invoked; description is what Claude Code reads to decide when to delegate; tools scopes each agent (the spec author has no Bash, so it can’t run or deploy anything, only read and write files); model picks the engine.
  • Separation of concerns is deliberate. bdd-author is forbidden from touching server.py, and mcp-builder is told to iterate against make test. Together they enforce spec-first development without a human in the loop.
  • mcp-builder has Bash precisely so it can run make test and keep going until green — the “always finish green” rule from CLAUDE.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

  • $ARGUMENTS in add-tool.md interpolates whatever the operator passes after the command, so make 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 /build always sees the current spec files.
  • allowed-tools pre-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.tool registers 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 switch CLAUDE.md requires: stdio for local dev, HTTP (for deployment) when MCP_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 generated server.py so 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

  • generate and add-tool call claude -p … — Claude Code’s headless mode. --permission-mode acceptEdits lets it write files unattended. These are the only targets that need the Claude Code CLI.
  • add-tool guards DESC so a bare make add-tool prints usage instead of sending an empty prompt.
  • ci deliberately omits Claude Code. It runs uv sync + behave against the committed, already-generated code, so continuous integration is deterministic and needs no credentials.
  • deploy/status/logs/undeploy are the launchd lifecycle from the companion deploy article; .DEFAULT_GOAL := help makes bare make self-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.py with MCP_TRANSPORT=http under launchd, restarting on crash (KeepAlive) and logging to logs/. @UV@, @DIR@, @PATH@ are filled in by make deploy because launchd runs with a bare environment.
  • scripts/smoke.py connects over HTTP to the deployed server — the same code a real client uses — and is what make smoke runs.
  • 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 ciuv 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-tool says claude: command not found. Install and authenticate the Claude Code CLI; verify with claude --version. Only these two targets need it — make test, deploy, and ci do not.
  • make test fails after a spec change. That is the system working: the code no longer matches the spec. Run make generate to have Claude Code update server.py, then make test again.
  • 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 that allowed-tools in each command lists the tools it needs.
  • behave can’t import server. Run it via make test from the project root so the root is on the import path; the step file does from server import mcp.
  • CI fails but local passes. CI runs make ci on exactly the committed code. Make sure the generated server.py is 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 .feature files, CLAUDE.md, two subagents, two slash commands, the Makefile, and CI.
  • Claude Code authored server.py, driven by /build and /add-tool through the mcp-builder and bdd-author subagents, iterating until the Cucumber suite passed.
  • behave turns Gherkin into executable tests that both specify behavior and gate merges.
  • The human only runs makegenerate, add-tool, test, deploy — and CI runs make ci on the committed code with no credentials.

Next improvements

  • Add a make regen-check target (and CI job, gated on a Claude Code token) that runs make generate and fails if the committed server.py drifts from what the specs would produce.
  • Add a pre-commit hook that runs make test so a red suite can never be committed.
  • Grow the harness: a refactor subagent, or a /review command that has Claude Code critique its own generated code against CLAUDE.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.