MCP Factory

The test count is verifiable below (python -m pytest tests/ โ 233 passed, 6 skipped) and enforced in CI by scripts/check_readme_counts.py, which fails the build if this README's counts drift from the live suite.
60-Second Quickstart
From PyPI (registry users):
pip install jaimenbell-mcp-factory
mcp-factory-hub --serve
From a git checkout (contributors): see the python hub_server.py ... examples throughout this README โ hub_server.py at the repo root is a backward-compat wrapper around the same mcp_factory.cli module the console script runs, so behavior is identical either way.
The manifest-driven engine behind the MCP Integration Sprint. Write one mcp.yaml for a bot repo and the factory generates the server stub and the ~/.claude.json entry; run the hub and it serves every bot's tools through a single MCP endpoint.
The SDK wrapper is the easy part. What makes an MCP server safe to put in front of a real internal tool โ scoped auth/env, fail-soft error handling, validated manifests, a collision-safe registry, and a real test suite โ is the engineering this engine is built around. That same production layer is hand-built per engagement; the factory scaffolds it, it doesn't fake it.
Browse before you reply
This repo is public so you can verify the discipline instead of taking my word for it. Every claim below maps to a file you can open:
| Claim | Where it lives | What to look for |
|---|
| Validated, env-scoped manifests | mcp_factory/manifest.py | strict from_dict validation (raises on missing/invalid fields); the env_required / env model that scopes which secrets a server may see |
| Fail-soft subprocess proxying | mcp_factory/runtime/subprocess_adapter.py | typed SubprocessError, lazy start, JSON-RPC error surfacing, timeout/OSError-guarded teardown + atexit cleanup โ a dead bot returns a clean error, it doesn't crash the hub |
| Collision-safe, manifest-driven registry | mcp_factory/runtime/registry.py ยท registry.json | CollisionError on duplicate <bot>.<tool> names; the registry is built from manifests, not hand-maintained |
| Tested on a clean checkout | tests/ | 233 passed, 6 skipped, 0 failed (Python 3.12); the 6 skips are real integration tests that no-op when external resources are absent |
Honesty rails: 233 is the real, reproducible count on a clean checkout โ the same number the public CI run produces and gates on. mcp-factory generates the scaffold and runs the hub โ it does not "generate the production server" or carry any client/CI claims. The hardened production layer (per-tool auth boundaries, the full failure set, two-axis version-pinning) is built per engagement on top of this engine. That applies to both Python scaffold styles below โ see "Two Python styles" for exactly what the fastmcp variant does and doesn't add on top of that baseline.
Quick Start
Factory mode (generate config from one manifest)
python hub_server.py --manifest examples/fleet_health.yaml
python hub_server.py --manifest my_bot/mcp.yaml --output-dir my_bot/
python hub_server.py --manifest my_bot/mcp.yaml --dry-run
python hub_server.py --manifest examples/fleet_health.yaml --verify ~/.claude.json
Output always goes to ~/.claude.json.factory-test by default โ never to the live ~/.claude.json. Copy entries manually after review.
Scan mode (batch-register all bots)
python hub_server.py --scan C:\path\to\projects
python hub_server.py --scan C:\path\to\projects --apply
python hub_server.py --scan C:\path\to\projects --apply --force
--scan discovers all projects/*/mcp.yaml files, validates each, and diffs them against the current ~/.claude.json. Default root is C:\path\to\projects. With --apply, a timestamped backup is created at ~/.claude.json.scan-backup-<timestamp> before writing.
Skip logic: manifests whose name already exists in ~/.claude.json are skipped unless --force is passed. This prevents accidentally overwriting manually-crafted entries.
Serve mode (runtime hub โ single MCP for all bots)
python hub_server.py --serve
python hub_server.py --serve \
--scan-root C:\path\to\projects \
--scan-root C:\path\to\Claude
python hub_server.py --register \
--scan-root C:\path\to\projects \
--scan-root C:\path\to\Claude
The hub scans all mcp.yaml manifests under each --scan-root at startup, then exposes every bot's tools under the <bot>.<tool> namespace (e.g., fleet-health.fleet_status, my-bot.run_scan). Tools are proxied to per-bot subprocess MCP servers with lazy startup.
Hub meta-tool: _hub.list_bots returns the registered bots and their subprocess status.
Hub is pre-registered in ~/.claude.json as mcp-factory-hub (see scripts/register_hub.py).
Node.js template
Factory generates Node.js stubs when runtime.type: node is set in mcp.yaml:
runtime:
type: node
command: "node"
output: "path/to/server.js"
Generated stubs use @modelcontextprotocol/sdk with stdio transport and zod for argument validation. See examples/node_example.yaml for a working demo.
Two Python styles: raw SDK vs. FastMCP
For runtime.type: python, the factory can scaffold either of two styles from the exact same manifest:
runtime:
type: python
command: "python"
style: raw
Both styles read the same tools: / env_required: fields and produce a server that speaks the same stdio JSON-RPC wire protocol โ the runtime hub's SubprocessAdapter proxies either one without any adapter changes (see tests/test_fastmcp_template.py::TestFastmcpServeSmoke for a live generate-and-call test).
| style: raw (python_server.py.j2) | style: fastmcp (python_fastmcp.j2) |
|---|
| SDK | official mcp package, mcp.server.Server | fastmcp (pinned exact fastmcp==3.4.2 โ 4.0.0b1 is a beta that breaks sampling/roots, do not float) |
| Tool registration | manual @server.list_tools() / @server.call_tool() dispatch | one @mcp.tool(...)-decorated function per tool |
| Arg schema | hand-built JSON Schema dict per arg | Annotated[type, Field(description=...)] on real Python parameters โ FastMCP derives the JSON Schema, including required/optional, from the signature |
| Tool body | # TODO: implement stub | same stub, wrapped in try/except Exception โ a runtime error in a filled-in implementation returns a structured {"status": "error", ...} instead of crashing the process |
env_required | not enforced at scaffold level | rendered into a _check_required_env() startup check that warns to stderr if a declared var is missing โ a presence check, not credential validation |
Gaps, stated honestly: neither style implements per-tool authorization, rate limiting, or the "full failure set" the hub-level subprocess_adapter.py gives you for free (typed errors, lazy start, atexit cleanup) โ that's still a per-engagement build on top of either scaffold. The fastmcp template's fail-soft wrapper and env-presence check are new, real code (read mcp_factory/templates/python_fastmcp.j2), not a marketing claim about auth โ they were added because FastMCP's decorator model made them cheap to include cleanly; they have not (yet) been backported to the raw template, which is why the two styles differ slightly in what ships out of the box. If your engagement needs FastMCP-specific features beyond this (resources, prompts, HTTP/SSE transport, middleware-based auth), the generated file is a normal FastMCP app โ extend it directly.
See examples/fastmcp_example.yaml for a working demo manifest.
mcp.yaml Schema
name: my-bot
description: >
What this bot does and when to use it.
runtime:
type: python
command: "C:\\Python314\\python.exe"
script: "path/to/server.py"
output: "path/to/out.py"
style: raw
tools:
- name: tool_name
description: >
What this tool does.
args:
- name: arg_name
type: string
required: true
description: "..."
env_required:
- MY_API_KEY
env:
MY_API_KEY: ""
tags: [trading, health]
priority: high
Key rules
runtime.script + existing file โ factory references it, skips scaffold
runtime.script + missing file โ validation error (use runtime.output for new scaffolds)
runtime.output โ explicit path for generated stub (absolute recommended)
- Neither
script nor output โ error at config-write step
How to Add a New MCP
- Write
mcp.yaml at your bot repo root (or in examples/)
- Run the factory:
python hub_server.py --manifest path/to/mcp.yaml
- Review
~/.claude.json.factory-test โ confirm the entry looks correct
- Copy the entry into
~/.claude.json under mcpServers
- Restart Claude Code
If the bot has no existing server, the factory generates a stub at generated/<name>_server.py. Fill in the # TODO: implement sections and set runtime.script to the stub path for future runs.
Runtime Hub Architecture
hub_server.py --serve
โโโ mcp_factory/runtime/
โโโ hub.py async MCP server (lists + routes all tools)
โโโ registry.py maps <bot>.<tool> โ manifest + adapter
โโโ subprocess_adapter.py spawns per-bot MCP server, proxies JSON-RPC
Subprocess lifecycle:
- Adapters start lazily on first tool call (no upfront spawn)
- Keep-alive for the hub session (one process per bot)
_hub.list_bots() reports status: idle (not yet started) or running
- All adapters stopped via
atexit on hub exit; stop() kills if needed after 5 s
Tool naming: <bot-name>.<tool-name> โ hyphens preserved, dots as separator.
Example: fleet-health.fleet_status, my-bot.get_alerts.
Day 4 โ workflow_runner.py
Standalone CLI harness for research workflows, independent of hub_server.py.
python -m mcp_factory.workflow_runner --list
python -m mcp_factory.workflow_runner --run my-skill
python -m mcp_factory.workflow_runner --validate
python -m mcp_factory.workflow_runner --write-registry
python -m mcp_factory.workflow_runner --check
python -m mcp_factory.workflow_runner --run my-skill --cache-policy force-refresh
python -m mcp_factory.workflow_runner --run my-skill --cache-policy read-only
How it works
workflow_runner.py scans ~/research by default (override with --scan-root) for SKILL.md files containing YAML frontmatter. Each SKILL.md defines a named workflow with metadata:
---
name: my-skill
description: What this workflow does
output_path_template: "~/vault/output/{date}/{name}.md"
---
Prompt body passed to claude -p subprocess...
- Discover:
git ls-files to enumerate tracked SKILL.md files under each scan root
- Validate: checks required frontmatter fields (
name, description)
- Cache: SHA-based cache keyed on prompt content;
auto (default) skips re-run if output unchanged, force-refresh always re-runs, read-only never writes
- Run: invokes
claude -p <prompt> as a subprocess, streams output
- Write output: expands
output_path_template, writes result to vault
- Registry:
--write-registry persists discovered skills to registry.json; --check detects drift between filesystem and registry without writing
Directory Layout
mcp-factory/
โโโ hub_server.py # CLI entry point (factory / scan / serve)
โโโ mcp_factory/
โ โโโ manifest.py # Manifest dataclass + YAML loader + validation
โ โโโ generator.py # Python MCP server stub scaffolder
โ โโโ config.py # claude.json entry builder + comparator
โ โโโ scan.py # --scan mode: manifest discovery + diff/apply
โ โโโ workflow_runner.py # Day 4: standalone CLI harness for SKILL.md workflows
โ โโโ templates/ # packaged as data so `pip install` ships them too
โ โ โโโ python_server.py.j2 # Jinja2 template โ raw mcp SDK stubs (style: raw, default)
โ โ โโโ python_fastmcp.j2 # Jinja2 template โ FastMCP v2 stubs (style: fastmcp)
โ โ โโโ node_server.js.j2 # Jinja2 template for generated Node.js stubs
โ โโโ runtime/
โ โโโ subprocess_adapter.py # subprocess MCP client (JSON-RPC proxy)
โ โโโ registry.py # tool registry with collision detection
โ โโโ hub.py # async hub MCP server
โโโ tests/
โ โโโ fixtures/
โ โ โโโ fleet_health.yaml # Day 1 self-verification fixture
โ โ โโโ minimal.yaml # Minimal valid manifest
โ โ โโโ mock_mcp_server.py # Stdlib-only mock MCP server for adapter tests
โ โโโ test_manifest.py
โ โโโ test_generator.py
โ โโโ test_subprocess_adapter.py
โ โโโ test_registry.py
โ โโโ test_scan.py
โ โโโ test_hub_cli.py
โ โโโ test_mcp_pkg.py
โ โโโ test_node_template.py
โ โโโ test_python_template.py
โ โโโ test_fastmcp_template.py # style: fastmcp generation + import + serve-smoke tests
โ โโโ test_register_flag.py
โ โโโ test_registration.py
โ โโโ test_smoke_hub.py
โ โโโ test_watcher.py
โ โโโ test_workflow_runner.py # Day 4: workflow_runner unit + integration tests
โ โโโ test_integration_fleet_health.py # live integration tests (skipped if server absent)
โโโ examples/
โ โโโ fleet_health.yaml # Example manifest referencing an existing server
โ โโโ node_example.yaml # Example manifest for the node template
โ โโโ fastmcp_example.yaml # Example manifest for the fastmcp template
โโโ pyproject.toml
Self-Verification
The examples/fleet_health.yaml manifest references an example server. Running:
python hub_server.py --manifest examples/fleet_health.yaml --verify ~/.claude.json
confirms the factory produces a matching ~/.claude.json entry.
Running Tests
python -m pytest tests/ -v
On a clean checkout (Python 3.12), with pip install -e .[dev]: 233 passed, 6 skipped, 0 failed โ the same numbers the public CI run produces and gates on.
The 6 skipped tests are real integration tests that need external resources and skip automatically when those are absent:
test_integration_fleet_health.py (5 tests) requires a fleet-health server.py on disk (FLEET_HEALTH_SERVER_PATH).
test_node_template.py (1 test) requires node and @modelcontextprotocol/sdk (node_modules/) to be present.
test_smoke_hub.py (4 tests) no longer needs a live bot fleet to run for real: the hub's demo-manifest fallback (see below) gives it something to discover even against an empty scan root, so these run unconditionally on a clean checkout now.
(On the maintainer's fleet machine, where the fleet-health server and live bots exist, the remaining skipped integration tests run for real and the passed count is higher โ but this README claims only what a clean checkout and public CI reproduce.)
The fastmcp-style template tests (test_fastmcp_template.py) are not in this skip list โ fastmcp is installed as a [dev] extra, so they run for real on a standard dev setup.
Commercial support
Maintained by Jaimen Bell. For production MCP integrations, custom servers, or agent-reliability work, see jaimenbell.dev.
Building your own MCP server? The MCP Starter Kit has templates, a build playbook, and packaging war-stories from shipping this one.
mcp-name: io.github.jaimenbell/mcp-factory