How we build MCP-powered tools — the frameworks we use, the patterns we follow, and why.
| Component | Role | Link |
|---|---|---|
MCP Python SDK (mcp) |
The official SDK for building MCP servers and clients. Provides FastMCP for decorator-based tool definition and ClientSession for calling other servers. |
github.com/modelcontextprotocol/python-sdk |
| Model Context Protocol | The open protocol that defines how AI agents discover and call tools. All MCP servers speak this protocol over stdio or HTTP. | modelcontextprotocol.io |
| Click | CLI framework. Thin CLI wrappers use Click for argument parsing and terminal formatting. | click.palletsprojects.com |
| agentskills.io | Open standard for AI agent skills (SKILL.md format). Used by Claude Code, Gemini CLI, VS Code/Copilot, Cursor, and 30+ other tools. | agentskills.io |
| Component | Role | Link |
|---|---|---|
| mcp-app | Config-driven framework for deploying MCP servers as multi-user HTTP services. Adds JWT auth, per-user data isolation, and admin endpoints. Used for cloud-deployed solutions. | github.com/krisrowe/mcp-app |
| gapp | Deployment lifecycle for Python MCP servers on Google Cloud Run. Handles Terraform, secrets, credential mediation, CI/CD. | github.com/krisrowe/gapp |
| app-user | Auth library for multi-user apps. ASGI middleware for JWT validation, user management, per-user data stores. | github.com/krisrowe/app-user |
| aicfg | Cross-platform skill and configuration management for Claude Code and Gemini CLI. Marketplace registration, skill installation, MCP server management. | github.com/krisrowe/aicfg |
┌─────────────────────────────────────────────────┐
│ Solution Repo │
│ │
│ sdk/ ← business logic (importable) │
│ mcp/ ← thin MCP wrapper (FastMCP) │
│ cli/ ← thin CLI wrapper (Click) │
│ │
├──────────────┬──────────────────────────────────-┤
│ Local mode │ Cloud mode │
│ stdio │ mcp-app (HTTP + auth) │
│ pipx install│ gapp deploy (Cloud Run) │
│ │ app-user (JWT + user data) │
└──────────────┴───────────────────────────────────┘
Local-only tools (dotfiles-manager, aicfg, bills-agent): The SDK,
MCP server, and CLI are the complete architecture. Install via pipx,
register the MCP server locally via stdio transport. No cloud
infrastructure needed.
Cloud/multi-user tools (food-agent, monarch-access, gworkspace-access): Same SDK+MCP+CLI core, plus mcp-app for HTTP transport and JWT auth, app-user for per-user data isolation, and gapp for deployment to Cloud Run. The SDK layer is identical to local tools — the cloud stack wraps it, just as CLI and MCP wrap it.
We use the official MCP Python SDK (from mcp.server.fastmcp import FastMCP) for all servers. See docs/mcp-framework.md
for a detailed comparison with the standalone fastmcp package and
why we chose the official SDK.
Every solution repo separates business logic from interface layers:
my-app/
sdk/ ← all logic: importable, testable, no I/O concerns
cli/ ← thin wrapper: calls SDK, formats for terminal
mcp/ ← thin wrapper: calls SDK, exposes as MCP tool schema
The rule: If you're writing logic in a CLI command or MCP tool, stop and move it to SDK.
This exists for three reasons:
-
Consistency. The same logic drives the interactive CLI, the AI agent interface (MCP), and any direct Python import. No behavior divergence between interfaces.
-
Testability. SDK functions take arguments and return dicts. They don't print, don't format, don't depend on transport. Unit tests call them directly — no subprocess spawning, no MCP client setup.
-
Future clients. The SDK is structured for programmatic consumption whether or not external consumers exist today. Any repo can be
pip installed and its SDK imported by another package. Some already are (gworkspace-access's SDK is imported by agentic-consult). The architecture is ready for this whether it happens in a week or never.
"SDK" is an architectural posture, not an adoption metric. The term is justified by how the code is structured, not by how many consumers exist:
- Functions take arguments and return structured data (dicts)
- No CLI or transport concerns mixed into the logic
- Importable as a Python package
- Designed so that any future client can consume it without refactoring
Alternatives considered:
core/— common in Django, Home Assistant, Celery. Implies "internal to this app," which undersells the intent for repos where the SDK is or will be consumed cross-repo.lib/— common in Rust and Rails, uncommon in Python where it often implies vendored dependencies.
We chose sdk/ for consistency across the ecosystem and because it
accurately describes the design intent.
SDK functions return dicts, not formatted strings. Every SDK
function returns a dict with a success key and relevant data. The CLI
layer formats for the terminal; the MCP layer returns as-is.
# sdk/sync.py
def track(path: str) -> dict:
# ... business logic ...
return {"success": True, "path": str(rel_path), "committed": committed}CLI is one-liner calls to SDK plus formatting:
# cli/main.py
@main.command()
@click.argument("path")
def track(path):
result = sync.track(path)
if result["success"]:
click.echo(f"Tracked: {result['path']}")MCP is one-liner calls to SDK:
# mcp/server.py
@mcp.tool()
def dot_track(path: str) -> dict:
"""Start tracking a file."""
return sync.track(path)No business logic in CLI or MCP. If a function does something beyond calling an SDK function and formatting the result, it's in the wrong layer.
SDK modules can import each other. sdk/sync.py may call
sdk/repo.py. The SDK is a cohesive package, not isolated functions.
Tests target the SDK directly. Unit tests call SDK functions, not CLI subprocesses or MCP tool invocations. See develop-unit-tests for the testing philosophy.
Mature repos following this pattern:
| Repo | SDK package | Notes |
|---|---|---|
| gworkspace-access | gwsa.sdk |
Google Workspace API integration. Cross-repo SDK consumer exists. Cloud-deployed via gapp. |
| monarch-access | monarch.sdk |
Monarch Money financial API integration. Cloud-deployed via gapp. |
| ticktick-mcp | ticktick.sdk |
TickTick task management API integration. Cloud-deployed via gapp. |
Every solution repo should have:
- README.md — what it is, how to install, how to use. User-facing.
- CONTRIBUTING.md — architecture, design principles, constraints, testing, how to add features. Contributor-facing (human and agent).
- CLAUDE.md — imports README.md and CONTRIBUTING.md via
@syntax so Claude Code loads them as context. - .gemini/settings.json — lists README.md and CONTRIBUTING.md as context files so Gemini CLI loads them.
See setup-agent-context for a skill that configures this automatically.
Solution repos are installable Python packages with entry points in
pyproject.toml:
[project.scripts]
my-app = "my_app.cli.main:main" # CLI entry point
my-app-mcp = "my_app.mcp.server:main" # MCP server entry pointInstall with pipx install for CLI use. Register MCP server with
claude mcp add or aicfg mcp add for agent use.
The SDK, CLI, and MCP layers ship in a single package today. All three
install together. For repos where the SDK is consumed cross-repo (e.g.,
gworkspace-access's
gwsa.sdk is imported by other packages), this means the consumer pulls
in Click and FastMCP as transitive dependencies even though it only
needs the SDK.
The solution is optional extras in pyproject.toml:
[project]
name = "gwsa"
dependencies = [
# only SDK dependencies here
"google-auth", "httpx",
]
[project.optional-dependencies]
cli = ["click>=8.0"]
mcp = ["mcp>=1.0.0"]This lets consumers choose what they need:
# SDK only (for cross-repo import)
pip install "git+https://github.com/USER/my-app.git"
# SDK + CLI + MCP (full install)
pipx install "my-app[cli,mcp] @ git+https://github.com/USER/my-app.git"No code changes required — the SDK already doesn't import Click or
FastMCP. This is purely a packaging change in pyproject.toml.
The SDK, MCP server, and CLI all live in the same repository. This is the right default for most products — single repo, single version, single release. Separating into distinct repos is valid for mature products where independent release cadences or separate contributor communities justify the overhead, but adds packaging complexity that is rarely warranted at early stages.