How to build a Claude connector: MCP server from scratch
If you want Claude to interact with an API you control — whether that is your internal reporting system, a custom ad platform, or a tool your company built — you can write an MCP server in a weekend. This guide walks through the full setup with a focus on the safety patterns that matter for any tool that can cause side effects.
What you are building
An MCP server is an HTTP service that exposes a typed tool manifest and a call endpoint. When Claude connects, it reads the manifest to learn what tools are available. When a conversation triggers a tool, Claude sends a typed JSON payload to the call endpoint. Your server executes the logic and returns a structured result.
For a connector that reads and drafts but does not execute, you will implement two categories of tools: read tools (no side effects) and draft tools (stage changes to a queue, do not apply them). The approval step is a separate, human-triggered action.
Dependencies and project setup
FastMCP is Anthropic's Python SDK for building MCP servers. It handles the protocol serialization, tool registration, and server lifecycle. You write decorated Python functions; FastMCP handles the MCP wire format.
Install FastMCP and dependencies
pip install fastmcp httpx pydantic
# Or with uv (recommended):
uv add fastmcp httpx pydanticDefining your first tool
The get_campaign tool is read-only — it fetches data and returns it. No state changes. This is the pattern for all read tools: call the upstream API, validate the response with Pydantic, return it. Claude receives the typed response and can reason over it.
A minimal read tool with FastMCP
from fastmcp import FastMCP
from pydantic import BaseModel
mcp = FastMCP("my-connector")
class CampaignSnapshot(BaseModel):
campaign_id: str
name: str
daily_budget_usd: float
status: str
@mcp.tool()
async def get_campaign(campaign_id: str) -> CampaignSnapshot:
"""Return the current state of a single campaign."""
# Replace with your actual API call
data = await your_api_client.get_campaign(campaign_id)
return CampaignSnapshot(**data)The draft pattern: staging changes without executing
The propose_budget_change tool writes to a draft queue, not the live API. The live API call happens only when a human approves the draft through a separate mechanism — your admin UI, a CLI command, a webhook. Claude cannot call the approve path because that tool does not exist in the manifest.
A draft tool that writes to a queue, not the live API
from datetime import datetime
import uuid
@mcp.tool()
async def propose_budget_change(
campaign_id: str,
current_budget_usd: float,
proposed_budget_usd: float,
reason: str,
) -> dict:
"""Stage a budget change as a draft. Does not apply the change."""
draft_id = str(uuid.uuid4())
await draft_queue.insert({
"id": draft_id,
"type": "budget_change",
"campaign_id": campaign_id,
"before": current_budget_usd,
"after": proposed_budget_usd,
"reason": reason,
"status": "pending",
"created_at": datetime.utcnow().isoformat(),
})
return {"draft_id": draft_id, "status": "pending", "message": "Draft queued for approval"}The manifest is the boundary
Running the server and connecting Claude
For local testing, run with python main.py. In Claude Desktop, add the server to your MCP config with the local URL. For production, deploy to a container runtime (Cloud Run, Railway, Fly.io) and expose an HTTPS endpoint.
OAuth setup for production connectors requires an authorization server. For internal tools, a shared secret in the Authorization header is sufficient for testing. For any tool touching financial data or production systems, implement OAuth 2.1 with PKCE before shipping.
Start the server with Streamable HTTP transport
# main.py
if __name__ == "__main__":
mcp.run(transport="streamable-http", host="0.0.0.0", port=8080)FAQ
- Do I need FastMCP or can I implement the protocol raw?
- You can implement the protocol from scratch — it is documented at modelcontextprotocol.io. For a production connector, FastMCP removes the protocol boilerplate and lets you focus on tool logic. The time savings are real; the raw path adds a week of work for marginal control.
- Can I use TypeScript instead of Python?
- Yes. Anthropic maintains
@modelcontextprotocol/sdkfor TypeScript. The patterns are identical: register tools as decorated functions, return typed responses, keep draft and approve as separate tools. SpendSignoff's production server is Python (FastAPI + FastMCP) but the TypeScript SDK is mature. - How do I test my tools before connecting Claude?
- The MCP Inspector (shipped with the TypeScript SDK) lets you call tools directly via a browser UI without a connected AI client. It is the fastest way to debug tool schemas and response shapes before wiring to Claude.
Connect an account read-only and watch the operator work.
Reads are free on every plan. Nothing spends without your two-step approval.
Related reading