
Key Takeaways
- MCP server development is the 2026 standard for exposing tools to AI agents - write the integration once as an MCP server and reuse it across Claude, Cursor, custom agents, and internal apps without rewriting per client.
- The minimum viable MCP server is 40 lines of Python using the official mcp SDK. Production servers add auth, schema validation, structured logging, and resource limits - typically 300-600 lines.
- Our Singapore invoice processing system exposes the ERP, vendor master, and approval engine as three MCP servers behind OAuth - that architecture is why a single agent reduced manual invoice work by 70% across the deployment.
- Do not put business logic in your MCP server. The server is a thin, typed wrapper around an existing API. Logic belongs in the agent or in the upstream system.
We built the same Salesforce integration four times in 2024 - once for a LangChain agent, once for a Claude Desktop project, once for a Cursor MCP demo, and once for an internal Slack bot. Every implementation was 80% the same code. The fourth time we shipped it as an MCP server and never built that integration again. MCP server development is the productivity unlock most teams underestimated when Anthropic released the protocol in late 2024. By mid-2026, every production agent system at Bitontree exposes its tools as MCP servers - not because it is fashionable, but because it cuts integration work by 60-80% across the portfolio. If you are new to the protocol, our primer on why MCP is the secret weapon for startups and SMEs covers the business case before this guide gets into the implementation playbook: the architecture, the code, the auth patterns, and the deployment choices we have shipped through real client work.
What Is the Model Context Protocol (MCP)?
The Model Context Protocol is an open standard that defines how AI applications connect to external data sources and tools. Anthropic released MCP in November 2024 as an open spec; by 2026 it is supported by Claude, OpenAI Agent SDK, Cursor, Windsurf, Zed, LangChain, LlamaIndex, and dozens of other clients. The protocol has the same role for AI tooling that USB has for hardware peripherals - one standard, many vendors, plug-and-play.
The protocol defines three primitives a server can expose:
- Tools - functions the model can invoke (read data, write data, call an API)
- Resources - read-only context the model can access (files, database rows, URLs)
- Prompts - parameterized prompt templates the host can present to users
A server can expose any combination. A GitHub MCP server might expose tools (create_issue, merge_pr), resources (file contents at a specific commit), and prompts ("review this PR"). The host application - Claude Desktop, Cursor, or a custom agent - discovers these capabilities at startup and presents them to the model.
The transport layer is JSON-RPC 2.0 over stdio (local, parent-child process) or HTTP+SSE (remote, networked). Stdio is the default for desktop and developer tools. HTTP+SSE is the deployment target for shared, multi-user, hosted servers - the pattern we use in production for any server that connects to a customer's systems.The full JSON-RPC 2.0 specification defines the request and response format used at the wire level.
The MCP specification, the reference servers, and the SDKs are all open source under github.com/modelcontextprotocol. The Python and TypeScript SDKs are the most mature; the Rust, Go, and Java SDKs are usable but newer. Bitontree ships most production servers in Python or TypeScript.The official MCP GitHub organization hosts the specification, reference servers, and SDK source for every supported language.
Why MCP Replaced Custom Tool Integrations in 2025-2026
Three forces pushed teams onto MCP - agent fragmentation, integration sprawl, and the platform shift to multi-client AI workflows. Before MCP, every agent framework had its own tool format. LangChain tools, OpenAI function definitions, custom JSON schemas - none were portable. Building one Salesforce integration meant building four; building a tool used across Claude Desktop, Cursor, and an internal agent meant building it three times.
The integration sprawl problem compounds over time. A typical mid-size enterprise has 20-50 internal APIs an AI agent might want to call. Wrapping each one per agent framework produces 200-500 distinct integration codebases. MCP collapses that into 20-50 servers, each consumed by every agent.

The platform shift matters as much as the technical wins. By 2026, knowledge workers use multiple AI tools - a coding assistant in the IDE, a research assistant in Claude, a domain agent inside a vertical app. They want their company's systems available in all three without IT building three integrations. MCP servers - especially remote, OAuth-protected ones - are the only way to make that economical.
The 2026 MCP ecosystem includes:
- Official reference servers - GitHub, Slack, Google Drive, Postgres, Sentry, Linear, Cloudflare, Stripe, Notion
- Vendor-built servers - most major SaaS vendors now ship an official MCP server (or list one as supported)
- Internal servers - what most enterprise teams build for their proprietary systems
The Bitontree practice is to use the official server when one exists (we do not rebuild GitHub or Slack integrations), audit it for security, and build internal MCP servers only for proprietary systems or systems with custom auth requirements.See our MCP server development company practice for the production patterns we use across client deployments.
MCP Architecture: Hosts, Clients, and Servers
MCP has three roles - host, client, and server. The host is the application a user interacts with (Claude Desktop, Cursor, an internal agent app). The client is the protocol-handling component inside the host, one per connected server. The server exposes tools, resources, or prompts. A host can run many clients connected to many servers in parallel.
| Role | Responsibility | Examples |
|---|---|---|
| Host | User-facing app, owns model calls and UX | Claude Desktop, Cursor, custom agent |
| Client | Protocol stack, one instance per server | Built into the host, usually invisible |
| Server | Exposes capabilities | GitHub server, Postgres server, internal CRM server |
The lifecycle of a session:
1. Initialize - host starts each server (stdio) or connects to it (HTTP); negotiates protocol version
2. List capabilities - host calls tools/list, resources/list, prompts/list to discover what the server offers
3. Use capabilities - model decides which tool to call; host sends tools/call with arguments
4. Stream results - server returns content blocks (text, image, embedded resource)
5. Shutdown - host closes the connection or terminates the subprocess
The model never talks to the server directly. The model emits a tool-call request as part of its response; the host's client invokes the server; the server returns a result; the host formats it back into the model's context. This separation matters for security - the server has no idea what model is using it, what prompt was issued, or what other tools are in play. It exposes capabilities and trusts the host to use them appropriately.
Building Your First MCP Server (Code Walkthrough)
A minimum viable MCP server in Python takes 40 lines using the official mcp SDK. The example below exposes one tool - get_invoice_status - backed by a Postgres database. This is the actual pattern used in our Singapore invoice processing system, simplified.
import asyncpg
from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel
mcp = FastMCP("invoice-server")
class InvoiceStatus(BaseModel):
invoice_id: str
status: str
amount: float
vendor: str
due_date: str
@mcp.tool()
async def get_invoice_status(invoice_id: str) -> InvoiceStatus:
"""Get the current status of an invoice by ID."""
conn = await asyncpg.connect(DATABASE_URL)
try:
row = await conn.fetchrow(
"SELECT invoice_id, status, amount, vendor, due_date "
"FROM invoices WHERE invoice_id = $1",
invoice_id,
)
if not row:
raise ValueError(f"Invoice {invoice_id} not found")
return InvoiceStatus(**dict(row))
finally:
await conn.close()
@mcp.tool()
async def list_pending_invoices(vendor: str | None = None) -> list[InvoiceStatus]:
"""List all pending invoices, optionally filtered by vendor."""
conn = await asyncpg.connect(DATABASE_URL)
try:
if vendor:
rows = await conn.fetch(
"SELECT * FROM invoices WHERE status='pending' AND vendor=$1",
vendor,
)
else:
rows = await conn.fetch(
"SELECT * FROM invoices WHERE status='pending' LIMIT 50"
)
return [InvoiceStatus(**dict(r)) for r in rows]
finally:
await conn.close()
if __name__ == "__main__":
mcp.run()
Running this with python invoice_server.py starts a stdio server. To connect it to Claude Desktop, add it to the config file at ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"invoice": {
"command": "python",
"args": ["/absolute/path/to/invoice_server.py"]
}
}
}
Restart Claude Desktop, and the two tools appear in the model's tool registry. The same server works unmodified in Cursor, Windsurf, and any other MCP-compatible host.
The FastMCP wrapper handles the JSON-RPC protocol, schema generation from Python type hints, and the stdio transport. The Pydantic models become JSON Schema definitions the model uses to format tool calls correctly. This is the entire developer ergonomics story - write a typed Python function, decorate it, ship a server.
MCP Server Patterns: Tools, Resources, and Prompts
The three MCP primitives - tools, resources, and prompts - have different roles and different design rules. Tools are for actions and dynamic queries; resources are for static or semi-static read-only content; prompts are for parameterized templates the user can invoke. Mixing them up produces servers that confuse the model and the host.

Tools are for any action the model takes. They have arguments, they execute logic, they return content. Tool design rules:
- One verb per tool - create_issue, update_issue, close_issue (not manage_issue)
- Typed arguments with strict schemas (Pydantic in Python, Zod in TypeScript)
- Return structured content the model can reason over, not opaque blobs
- Idempotent where possible - the model may retry; design for safe retries
Resources are for read-only context the model needs but should not have to "decide" to fetch. They are addressed by URI. A file resource might be file:///project/README.md. A database row might be postgres://invoices/INV-1027. Resources are subscribed-to: the host can present them to the user as attachments, and the model gets their content in context.
The split between "tool that reads data" and "resource" is subtle but important. Use a resource when:
- The content is identifiable by a stable URI
- The model needs the content as ambient context, not as a result of explicit retrieval
- Users may want to manually select or pin the resource
Use a tool when the content depends on dynamic arguments or filters.
Prompts are parameterized templates. A "summarize this incident" prompt takes an incident ID as an argument and produces a fully-formed prompt for the user to send. Prompts are useful for surfacing power-user workflows - they show up as slash commands in MCP-aware hosts. We use them sparingly; most production work lives in tools.
The Singapore invoice processing system exposes three MCP servers, each with a clear primitive split:
| Server | Tools | Resources | Prompts |
|---|---|---|---|
| erp-invoice | get_invoice, create_invoice, update_status | None | None |
| vendor-master | search_vendor, validate_vendor_tax_id | vendor://[id] | None |
| approval-engine | request_approval, get_approval_status | approval://[id] | "review_pending_invoices" |
Authentication, Security, and Production Hardening
Production MCP servers need authentication, authorization, audit logging, and resource limits, none of which the SDK gives you for free. The official SDK handles the protocol; you handle the security model. We have seen multiple early MCP deployments expose production databases over stdio servers running as the user, which is exactly as bad as it sounds.
The 2026 MCP spec added OAuth 2.1 support for remote servers, which is the right pattern for any multi-user deployment. The flow:
1. Discovery - host fetches the server's /.well-known/mcp metadata, including auth endpoints
2. Authorization - user authenticates against the server's IdP (or a federated provider), authorizes scopes
3. Token exchange - host receives an access token, includes it in every MCP request
4. Refresh - host refreshes the token before expiry; server validates per request
For internal servers behind a corporate IdP (Okta, Azure AD, Auth0), use that IdP's OAuth endpoints. For servers exposed to external customers, run your own OAuth server or proxy through a service like Clerk or Stytch.
Related: Model Context Protocol vs API: When MCP Replaces a Direct Integration - the decision framework we use to choose between exposing a system through MCP versus calling its REST API from agent code.
The non-negotiable hardening checklist for production MCP servers:
- Input validation - every tool argument validated against the Pydantic/Zod schema; reject extras
- Output validation - strip secrets and PII before returning; PII redaction is your responsibility, not the model's
- Rate limiting - per-token and per-user limits to prevent runaway agents from exhausting upstream APIs
- Audit logging - every tool call written to a structured log with user ID, tool name, arguments hash, and result hash
- Resource limits - max payload size, max execution time, max concurrent connections
- Network isolation - server runs in a VPC with egress restricted to required upstream services only
The Singapore invoice MCP servers run in a private VPC, behind an internal load balancer, authenticated via the client's Azure AD tenant. Every tool call writes to CloudWatch with a request ID that links back to the calling agent's trace. That observability is what makes the deployment auditable for finance compliance.
Deploying MCP Servers: Local, Remote, and Containerized
Three deployment modes cover the production landscape - stdio for local developer tooling, remote HTTP+SSE for shared servers, and containerized HTTP+SSE for enterprise deployments. The choice depends on who runs the server (the user or a central team) and which clients connect to it.
Stdio (local) - the server is a subprocess of the host. Used for developer tools (Cursor, Claude Desktop, IDE plugins) where each user runs their own instance. Pros: simple, no auth needed (runs as the user), fast startup. Cons: each user installs separately, no centralized updates, no shared state.
Remote HTTP+SSE (hosted) - the server runs as a long-running web service. Hosts connect over HTTPS using OAuth. Used for multi-user systems and any server that connects to a shared backend. Pros: centralized deployment, shared auth, observable. Cons: needs infrastructure, OAuth complexity, networking concerns.
Containerized HTTP+SSE (enterprise) - the same remote pattern, packaged as a Docker container and deployed on Kubernetes, ECS, or Cloud Run. This is the standard Bitontree deployment for client work. The container runs the MCP server, an auth proxy, and a structured logger. Health checks, autoscaling, and rolling updates work the same way as any web service.
The reference Dockerfile we use:
FROM python:3.12-slim
WORKDIR /app
RUN pip install --no-cache-dir uv
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev
COPY src/ ./src/
ENV MCP_TRANSPORT=sse
ENV MCP_PORT=8080
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s \
CMD curl -f http://localhost:8080/health || exit 1
CMD ["uv", "run", "python", "-m", "src.server"]
We deploy this behind an Application Load Balancer with TLS, run it on ECS Fargate with autoscaling on CPU and request count, and wire CloudWatch logs into the same Datadog account the rest of the client's services use. Standard web service hygiene. Nothing exotic.
The one MCP-specific concern is server-sent events (SSE) connection handling. MCP keeps long-lived connections; load balancers must support sticky sessions or - better - a connection-aware routing layer. AWS ALB handles this with target group stickiness. Cloud Run handles it with session affinity. Plain round-robin Kubernetes services do not work well - connections get torn down on autoscale events.
Testing and Observability for MCP Servers
MCP servers need three layers of testing - unit tests for tool logic, integration tests for the protocol, and end-to-end tests with a real client. Skipping any layer means production breaks in a way the unit tests miss. The MCP Inspector - Anthropic's official debug tool at inspector.modelcontextprotocol.io - handles manual protocol testing. For automated tests, use the SDK's testing utilities or roll a thin client.
Unit tests cover tool logic in isolation:
import pytest
from invoice_server import get_invoice_status
@pytest.mark.asyncio
async def test_get_invoice_status_returns_pending():
result = await get_invoice_status("INV-1027")
assert result.status == "pending"
assert result.invoice_id == "INV-1027"
Integration tests verify the protocol layer:
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
@pytest.mark.asyncio
async def test_list_tools():
params = StdioServerParameters(command="python", args=["invoice_server.py"])
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
assert "get_invoice_status" in [t.name for t in tools.tools]
End-to-end tests run the server inside a Docker container, connect a real MCP client, and assert on the round trip. We run these in CI on every PR.
Observability stack we ship by default:
- Structured logs - every tool call writes a JSON line with timestamp, tool name, user ID, latency, result status
- Metrics - Prometheus or CloudWatch metrics on requests per minute, error rate per tool, p95 latency per tool
- Distributed tracing - OpenTelemetry spans for each tool call, propagated upstream from the agent's trace
- Audit log - separate, append-only log of every tool execution for compliance review
The audit log matters more than teams expect. When the regulators or the security team ask "what did the agent do last Tuesday at 3pm," the answer must come from the MCP server's audit log, not from the agent's chain-of-thought. We log every successful tool call, every failed call, the arguments, and a hash of the result.
Production Lessons from MCP Deployments
Four lessons from running MCP servers in production over the past 18 months - keep servers thin, version the protocol, handle the cold start, and budget for tool sprawl.
Keep servers thin: An MCP server should be a typed wrapper around an existing API or database. The temptation is to embed business logic in the server - "this server doesn't just fetch invoices, it also classifies them." Resist this. Business logic belongs in the agent or in the upstream system. A thin server is testable, debuggable, and reusable; a thick server is a microservice in disguise and inherits all the problems of microservice architecture.
Version the protocol AND the tools: MCP has its own version negotiation, but your tool contracts also change. Adding a required argument breaks every client. Renaming a tool is worse. Treat tool signatures the way you treat API contracts - additive changes only, deprecation paths for breaking changes, and a published changelog.
Plan for the cold start: A new MCP server with no docs and no examples is hard for an LLM to use well. The first 50 production runs are noisy because the model is exploring the tool surface. We pre-warm new servers with prompt examples in the description fields and ship integration tests that exercise common call patterns. After 200-500 production runs, the agent's tool-use pattern stabilizes.
Budget for tool sprawl: Every server you add increases the model's tool registry. Models with 60+ tools available start to misroute calls. Group related capabilities into one server with 5-15 tools; split when one server grows past 25 tools. The Bitontree default is ≤10 tools per server, partitioned by domain.
The Singapore invoice processing deployment runs three MCP servers, 24 tools total, against three upstream systems (Xero, the vendor master DB, and Slack for approvals). The agent dispatches to the right server based on the task. The deployment cut manual invoice work by 70% compared to the previous Excel-and-email workflow. The architecture is unremarkable; the operational discipline is what made it work.
Where to Start With MCP Server Development
The pattern that has held up across every production MCP deployment we have shipped - write the server thin, expose ≤10 typed tools per domain, secure it with OAuth and audit logging, and run it like any other production web service. Most teams overcomplicate the server and underbuild the operational layer; both mistakes cost more to fix than to prevent. If you are starting on mcp server development for an enterprise integration and want a second opinion on the architecture, Book a Free AI Fit Assessment and we will tell you whether MCP is the right protocol layer or whether a direct integration fits better.

I am the founder and CEO of Bitontree, where I lead embedded AI engineering teams that build and run production AI: agents, RAG and knowledge systems, document AI, and workflow automation for healthcare, logistics, legal, and SaaS companies. I write about what it actually takes to ship AI that survives contact with production.
Frequently Asked Questions
What is the Model Context Protocol?

The Model Context Protocol (MCP) is an open standard that defines how AI applications connect to external data sources and tools. Released by Anthropic in November 2024, MCP is now supported by Claude, OpenAI Agent SDK, Cursor, Windsurf, LangChain, and most production AI platforms. It lets developers write a tool integration once and use it across every MCP-compatible client.
What is MCP server development?

MCP server development is building services that expose tools, resources, or prompts to AI agents using the Model Context Protocol. A server can wrap any API, database, or internal system; the same server then works with any MCP-compatible host without modification. The official Python and TypeScript SDKs at github.com/modelcontextprotocol make a basic server about 40 lines of code.
How is MCP different from OpenAI function calling?

OpenAI function calling is a model API feature, you send function definitions in the chat request, the model returns calls, you execute them. MCP is a protocol between separate processes that defines how tools are discovered, invoked, and authenticated. MCP servers are reusable across many model providers; OpenAI function definitions are not.
Can I use MCP with models other than Claude?

Yes. By 2026, the OpenAI Agent SDK, LangChain, LlamaIndex, and most agent frameworks support connecting to MCP servers. The protocol is open and model-agnostic. The Claude apps had the earliest support, which is why most early examples use Claude, but the spec works with any LLM that supports tool calling.
How do I secure an MCP server?

For local stdio servers, the server runs as the user and inherits their permissions; the security model is the OS. For remote HTTP+SSE servers, use OAuth 2.1 for authentication, scope-based authorization, structured audit logging, rate limiting, and network isolation. The 2026 MCP spec includes OAuth 2.1 support natively.
What is the difference between MCP tools and MCP resources?

MCP tools are functions the model invokes - they take arguments, execute logic, and return content. MCP resources are read-only context addressed by URI - they are presented to the user or model as ambient context rather than fetched through explicit tool calls. Use tools for actions and dynamic queries; use resources for stable, identifiable content.
How do I deploy an MCP server to production?

Containerize the server, expose HTTP+SSE on a known port, terminate TLS at a load balancer, authenticate via OAuth 2.1, and run health checks plus structured logging. The Bitontree default is ECS Fargate or Cloud Run with autoscaling on CPU and request count, CloudWatch or Datadog for observability, and a private VPC with egress restricted to required upstream services.
How many tools should an MCP server expose?

10 or fewer per server, partitioned by domain. Servers with more than 25 tools confuse the model and become harder to maintain. If a server grows past that, split it by capability, for example, separate "read" and "write" servers, or separate one upstream system from another. Group related capabilities; do not expose every possible operation as a separate tool.
What languages can I use to build MCP servers?

The official SDKs as of 2026 are Python, TypeScript, Go, Rust, Java, and C#. Python and TypeScript are the most mature and have the best documentation. Bitontree ships most production servers in Python (for data-heavy integrations) or TypeScript (for Node.js-friendly environments). Go and Rust are good choices when latency or memory matters.


