{
  "markdown": "# MCP Factory\n\n[![CI](https://github.com/jaimenbell/MCP-Factory/actions/workflows/ci.yml/badge.svg)](https://github.com/jaimenbell/MCP-Factory/actions/workflows/ci.yml) ![tests](https://img.shields.io/badge/tests-348%20passing-brightgreen) ![python](https://img.shields.io/badge/python-%E2%89%A53.12-blue) [![PyPI](https://img.shields.io/pypi/v/jaimenbell-mcp-factory)](https://pypi.org/project/jaimenbell-mcp-factory/) [![MCP Registry](https://img.shields.io/badge/MCP%20Registry-io.github.jaimenbell%2Fmcp--factory-blueviolet)](https://registry.modelcontextprotocol.io/)\n\n> *The test count is verifiable below (`python -m pytest tests/` → **348 passed, 8 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.*\n\n## 60-Second Quickstart\n\n**From PyPI (registry users):**\n\n```bash\npip install jaimenbell-mcp-factory\nmcp-factory-hub --serve\n# equivalent: python -m mcp_factory --serve\n```\n\n**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.\n\n**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.\n\nThe 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.\n\n### Browse before you reply\n\nThis 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:\n\n| Claim | Where it lives | What to look for |\n|---|---|---|\n| **Validated, env-scoped manifests** | [`mcp_factory/manifest.py`](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 |\n| **Fail-soft subprocess proxying** | [`mcp_factory/runtime/subprocess_adapter.py`](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 |\n| **Collision-safe, manifest-driven registry** | [`mcp_factory/runtime/registry.py`](mcp_factory/runtime/registry.py) · [`registry.json`](registry.json) | `CollisionError` on duplicate `<bot>.<tool>` names; the registry is built from manifests, not hand-maintained |\n| **Tested on a clean checkout** | [`tests/`](tests/) | **348 passed, 8 skipped, 0 failed** (Python 3.12); the 8 skips are real integration tests or lint checks that no-op when the thing they need is absent |\n\n> **Honesty rails:** `348` 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.\n\n## Quick Start\n\n### Factory mode (generate config from one manifest)\n\n```bash\n# Reference an existing MCP server (no code generated — just the config entry)\npython hub_server.py --manifest examples/fleet_health.yaml\n\n# Scaffold a new MCP server from scratch\npython hub_server.py --manifest my_bot/mcp.yaml --output-dir my_bot/\n\n# Dry run — preview without writing\npython hub_server.py --manifest my_bot/mcp.yaml --dry-run\n\n# Self-verify: compare factory output to live ~/.claude.json entry\npython hub_server.py --manifest examples/fleet_health.yaml --verify ~/.claude.json\n```\n\nOutput always goes to `~/.claude.json.factory-test` by default — **never** to the live `~/.claude.json`. Copy entries manually after review.\n\n### Scan mode (batch-register all bots)\n\n```bash\n# Dry-run diff: show what would change in ~/.claude.json\npython hub_server.py --scan C:\\path\\to\\projects\n\n# Apply: write ~/.claude.json after backing it up\npython hub_server.py --scan C:\\path\\to\\projects --apply\n\n# Force-update entries already registered\npython hub_server.py --scan C:\\path\\to\\projects --apply --force\n```\n\n`--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.\n\n**Skip logic:** manifests whose name already exists in `~/.claude.json` are skipped unless `--force` is passed. This prevents accidentally overwriting manually-crafted entries.\n\n### Serve mode (runtime hub — single MCP for all bots)\n\n```bash\n# Run the hub as a live MCP server (stdio transport)\npython hub_server.py --serve\n\n# Serve with multiple scan roots (repeatable)\npython hub_server.py --serve \\\n  --scan-root C:\\path\\to\\projects \\\n  --scan-root C:\\path\\to\\Claude\n\n# Register+serve in one step (scan --apply then start hub)\npython hub_server.py --register \\\n  --scan-root C:\\path\\to\\projects \\\n  --scan-root C:\\path\\to\\Claude\n```\n\nThe 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.\n\n**Hub meta-tool:** `_hub.list_bots` returns the registered bots and their subprocess status.\n\nHub is pre-registered in `~/.claude.json` as `mcp-factory-hub` (see `scripts/register_hub.py`).\n\n### Node.js template\n\nFactory generates Node.js stubs when `runtime.type: node` is set in `mcp.yaml`:\n\n```yaml\nruntime:\n  type: node\n  command: \"node\"\n  output: \"path/to/server.js\"\n```\n\nGenerated stubs use `@modelcontextprotocol/sdk` with stdio transport and zod for argument validation. See `examples/node_example.yaml` for a working demo.\n\n### Two Python styles: raw SDK vs. FastMCP\n\nFor `runtime.type: python`, the factory can scaffold either of two styles from the exact same manifest:\n\n```yaml\nruntime:\n  type: python\n  command: \"python\"\n  style: raw       # default — official `mcp` SDK, hand-rolled list_tools/call_tool\n  # style: fastmcp # FastMCP v3 (PrefectHQ/fastmcp), decorator-based tool registration\n```\n\nBoth 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).\n\n| | `style: raw` (`python_server.py.j2`) | `style: fastmcp` (`python_fastmcp.j2`) |\n|---|---|---|\n| 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) |\n| Tool registration | manual `@server.list_tools()` / `@server.call_tool()` dispatch | one `@mcp.tool(...)`-decorated function per tool |\n| 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 |\n| 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 |\n| `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 |\n\n**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.\n\nSee `examples/fastmcp_example.yaml` for a working demo manifest.\n\n## mcp.yaml Schema\n\n```yaml\nname: my-bot                   # REQUIRED — unique MCP server name (key in claude.json)\ndescription: >                 # REQUIRED — shown in Claude's tool descriptions\n  What this bot does and when to use it.\n\nruntime:                       # REQUIRED\n  type: python                 # python | node | binary\n  command: \"C:\\\\Python314\\\\python.exe\"  # full path to interpreter\n  script: \"path/to/server.py\"  # existing server (skips scaffold generation)\n  output: \"path/to/out.py\"     # where to write generated scaffold (omit = auto)\n  style: raw                   # python only: raw (default) | fastmcp — see \"Two Python styles\"\n\ntools:                         # REQUIRED — list of MCP tools to expose\n  - name: tool_name            # REQUIRED — programmatic identifier\n    title: \"Tool Name\"         # Optional — human display name (see below)\n    description: >             # REQUIRED — used by Claude for routing\n      What this tool does.\n    annotations:               # Optional — the four MCP behaviour hints\n      readOnlyHint: true       # tool does not modify anything\n      destructiveHint: false   # tool does not perform destructive updates\n      idempotentHint: true     # repeat calls have no additional effect\n      openWorldHint: false     # tool does not touch an open external world\n    args:                      # Optional list of arguments\n      - name: arg_name         # REQUIRED\n        type: string           # string | number | boolean | object | array\n        required: true         # default: true\n        description: \"...\"     # shown in Claude's tool schema\n\nenv_required:                  # env var names that must be set at runtime\n  - MY_API_KEY\n\nenv:                           # static env vars injected into claude.json entry\n  MY_API_KEY: \"\"               # leave value empty — fill in ~/.claude.json manually\n\ntags: [trading, health]        # for documentation / future routing\npriority: high                 # high | medium | low\n```\n\n### Key rules\n\n- `runtime.script` + existing file → factory references it, skips scaffold\n- `runtime.script` + missing file → validation error (use `runtime.output` for new scaffolds)\n- `runtime.output` → explicit path for generated stub (absolute recommended)\n- Neither `script` nor `output` → error at config-write step\n\n### `title` and `annotations`\n\nBoth are optional and both are worth declaring.\n\n`title` is the human display name, which lets `name` stay a programmatic\nidentifier. Display precedence is `title` → `annotations.title` → `name`; the\nfactory exposes only the top-level `title` so one manifest cannot declare two\ncompeting display names.\n\n`annotations` matters more than it looks. The four hints have **pessimistic\ndefaults** — `destructiveHint` and `openWorldHint` both default to `true`,\n`readOnlyHint` to `false` — so a tool that declares nothing is treated by a\ncareful client as the most dangerous thing it could be. Declaring\n`readOnlyHint: true` on a read-only tool is how you opt out of that. Only the\nfour hint names above are accepted, each must be a real boolean, and an unknown\nkey is a hard error rather than a silent drop (the typo `readonlyHint` would\notherwise leave the tool effectively unannotated on the wire).\n\nAnnotations are **hints, not a security boundary** — the spec is explicit that a\nclient should never make tool-use decisions based on annotations from an\nuntrusted server. They inform a client's UX; they do not enforce anything.\n\nBoth fields are emitted by the two Python templates. The Node template does not\nrender them yet — the repo vendors no `node_modules`, so an altered call shape\ncould not be executed in test, and shipping unverified generated code is worse\nthan shipping the current shape. Declaring them in a node manifest is still\nvalid and forward-compatible.\n\n## Generation-time tool-design lint\n\nManifest validation is a *security* gate: it fails closed on anything that could\ninject code into a generated server. It says nothing about whether the resulting\ntools are any good. A manifest can be perfectly valid and still produce a server\nthat makes an agent measurably worse.\n\nEvery run of factory mode therefore also runs a **tool-design lint** (Step 2),\ncovering 15 rules across three scopes:\n\n| Scope | Checks |\n|---|---|\n| manifest | tool-count budget, names colliding once case/separators are ignored, bare generic names that collide across servers |\n| tool | empty / thin / filler descriptions, descriptions that only restate the name, non-snake_case and over-long names, missing annotations, unbounded listing tools, annotations that contradict the name |\n| arg | undescribed parameters, unqualified parameter names, structured (`object`/`array`) parameters with no description and therefore no schema shape |\n\nIt is **advisory by default** — findings print, generation proceeds, exit 0.\n`--lint-strict` makes error-severity findings fatal: it reports them, writes no\nscaffold, and exits `3`.\n\n```bash\nmcp-factory-hub --manifest examples/fastmcp_example.yaml --lint-strict\n```\n\nThree deliberate properties:\n\n- **It reports `INDETERMINATE`, not a clean bill**, when it has no discriminating\n  power — a manifest whose `runtime.script` exists (the factory references that\n  hand-written file rather than generating one, so the tool list may not match\n  the real server), or a runtime with no template. `--lint-strict` never blocks\n  on an indeterminate result: the absence of a verdict must not be converted\n  into one.\n- **There is no tool-name charset rule**, even though SEP-986 defines one. The\n  manifest's own identifier validation is already stricter and rejects violations\n  at parse time, so such a rule could never fire on any input reaching the lint —\n  and a check that cannot fail is not a check. Only the length half of SEP-986 is\n  unenforced upstream, so only the length half is a rule.\n- **Every rule ships a positive control**: a test proving it fires on a known-bad\n  tool definition and stays silent on a known-good one, plus a suite-level test\n  asserting a well-formed manifest produces zero findings. The examples in\n  `examples/` are themselves held to the lint by `tests/test_examples_lint_clean.py`.\n\n## How to Add a New MCP\n\n1. Write `mcp.yaml` at your bot repo root (or in `examples/`)\n2. Run the factory:\n   ```bash\n   python hub_server.py --manifest path/to/mcp.yaml\n   ```\n3. Review `~/.claude.json.factory-test` — confirm the entry looks correct\n4. Copy the entry into `~/.claude.json` under `mcpServers`\n5. Restart Claude Code\n\nIf 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.\n\n## Runtime Hub Architecture\n\n```\nhub_server.py --serve\n  └── mcp_factory/runtime/\n      ├── hub.py               async MCP server (lists + routes all tools)\n      ├── registry.py          maps <bot>.<tool> → manifest + adapter\n      └── subprocess_adapter.py  spawns per-bot MCP server, proxies JSON-RPC\n```\n\n**Subprocess lifecycle:**\n- Adapters start lazily on first tool call (no upfront spawn)\n- Keep-alive for the hub session (one process per bot)\n- `_hub.list_bots()` reports status: `idle` (not yet started) or `running`\n- All adapters stopped via `atexit` on hub exit; `stop()` kills if needed after 5 s\n\n**Tool naming:** `<bot-name>.<tool-name>` — hyphens preserved, dots as separator.\nExample: `fleet-health.fleet_status`, `my-bot.get_alerts`.\n\n## Day 4 — workflow_runner.py\n\nStandalone CLI harness for research workflows, independent of `hub_server.py`.\n\n```bash\n# Discover and list all SKILL.md workflows\npython -m mcp_factory.workflow_runner --list\n\n# Run a specific workflow\npython -m mcp_factory.workflow_runner --run my-skill\n\n# Validate all discovered SKILL.md files\npython -m mcp_factory.workflow_runner --validate\n\n# Write/update registry.json from discovered skills\npython -m mcp_factory.workflow_runner --write-registry\n\n# Check for drift between discovered skills and registry.json\npython -m mcp_factory.workflow_runner --check\n\n# Control cache behavior\npython -m mcp_factory.workflow_runner --run my-skill --cache-policy force-refresh\npython -m mcp_factory.workflow_runner --run my-skill --cache-policy read-only\n```\n\n### How it works\n\n`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:\n\n```yaml\n---\nname: my-skill\ndescription: What this workflow does\noutput_path_template: \"~/vault/output/{date}/{name}.md\"\n---\nPrompt body passed to claude -p subprocess...\n```\n\n- **Discover:** `git ls-files` to enumerate tracked `SKILL.md` files under each scan root\n- **Validate:** checks required frontmatter fields (`name`, `description`)\n- **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\n- **Run:** invokes `claude -p <prompt>` as a subprocess, streams output\n- **Write output:** expands `output_path_template`, writes result to vault\n- **Registry:** `--write-registry` persists discovered skills to `registry.json`; `--check` detects drift between filesystem and registry without writing\n\n## Directory Layout\n\n```\nmcp-factory/\n├── hub_server.py              # CLI entry point (factory / scan / serve)\n├── mcp_factory/\n│   ├── manifest.py            # Manifest dataclass + YAML loader + validation\n│   ├── generator.py           # Python MCP server stub scaffolder\n│   ├── config.py              # claude.json entry builder + comparator\n│   ├── scan.py                # --scan mode: manifest discovery + diff/apply\n│   ├── workflow_runner.py     # Day 4: standalone CLI harness for SKILL.md workflows\n│   ├── templates/              # packaged as data so `pip install` ships them too\n│   │   ├── python_server.py.j2    # Jinja2 template — raw mcp SDK stubs (style: raw, default)\n│   │   ├── python_fastmcp.j2      # Jinja2 template — FastMCP v2 stubs (style: fastmcp)\n│   │   └── node_server.js.j2      # Jinja2 template for generated Node.js stubs\n│   └── runtime/\n│       ├── subprocess_adapter.py  # subprocess MCP client (JSON-RPC proxy)\n│       ├── registry.py            # tool registry with collision detection\n│       └── hub.py                 # async hub MCP server\n├── tests/\n│   ├── fixtures/\n│   │   ├── fleet_health.yaml   # Day 1 self-verification fixture\n│   │   ├── minimal.yaml        # Minimal valid manifest\n│   │   └── mock_mcp_server.py  # Stdlib-only mock MCP server for adapter tests\n│   ├── test_manifest.py\n│   ├── test_generator.py\n│   ├── test_subprocess_adapter.py\n│   ├── test_registry.py\n│   ├── test_scan.py\n│   ├── test_hub_cli.py\n│   ├── test_mcp_pkg.py\n│   ├── test_node_template.py\n│   ├── test_python_template.py\n│   ├── test_fastmcp_template.py  # style: fastmcp generation + import + serve-smoke tests\n│   ├── test_register_flag.py\n│   ├── test_registration.py\n│   ├── test_smoke_hub.py\n│   ├── test_watcher.py\n│   ├── test_workflow_runner.py  # Day 4: workflow_runner unit + integration tests\n│   └── test_integration_fleet_health.py  # live integration tests (skipped if server absent)\n├── examples/\n│   ├── fleet_health.yaml      # Example manifest referencing an existing server\n│   ├── node_example.yaml      # Example manifest for the node template\n│   └── fastmcp_example.yaml   # Example manifest for the fastmcp template\n└── pyproject.toml\n```\n\n## Self-Verification\n\nThe `examples/fleet_health.yaml` manifest references an example server. Running:\n\n```bash\npython hub_server.py --manifest examples/fleet_health.yaml --verify ~/.claude.json\n```\n\nconfirms the factory produces a matching `~/.claude.json` entry.\n\n## Running Tests\n\n```bash\npython -m pytest tests/ -v\n```\n\nOn a clean checkout (Python 3.12), with `pip install -e .[dev]`: **348 passed, 8 skipped, 0 failed** — the same numbers the public CI run produces and gates on.\n\nThe 8 skipped tests skip automatically when the resource or condition they need is absent:\n- `test_integration_fleet_health.py` (5 tests) requires a fleet-health `server.py` on disk (`FLEET_HEALTH_SERVER_PATH`).\n- `test_node_template.py` (1 test) requires `node` and `@modelcontextprotocol/sdk` (`node_modules/`) to be present.\n- `test_examples_lint_clean.py` (2 tests) skips `examples/fleet_health.yaml`, which references an existing hand-written server — the tool-design lint correctly reports `INDETERMINATE` there rather than judging code it cannot see, and a skip is the honest way to record that.\n\n`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.\n\n(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.)\n\nThe 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.\n\n\n## Commercial support\n\nMaintained by [Jaimen Bell](https://jaimenbell.dev). For production MCP integrations, custom servers, or agent-reliability work, see [jaimenbell.dev](https://jaimenbell.dev).\n\nBuilding your own MCP server? The [MCP Starter Kit](https://jaimenbell.gumroad.com/l/adnojp) has templates, a build playbook, and packaging war-stories from shipping this one.\n\n<!-- MCP registry ownership marker -->\nmcp-name: io.github.jaimenbell/mcp-factory\n",
  "bytes": 22870,
  "sha": "1509ca1a0bee7f1dea73e9c3f64b368baead53e96368661d019ad965804038d1",
  "repo_slug": "jaimenbell/mcp-factory",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_jaimenbell_mcp_factory_866bedf7/readme"
}