A Beginner's Guide to Writing Your First MCP Server: Exposing Internal Tools to AI Agents Without Breaking Existing Services
If you've been working as a junior backend developer in 2026, you've almost certainly heard the phrase "make it available to the AI agent" at least once in a sprint planning meeting. The rise of agentic AI workflows has created a new and surprisingly practical challenge: how do you let an AI model call your internal tools, query your databases, or trigger your business logic without ripping apart the services you already have running in production?
The answer, increasingly, is the Model Context Protocol (MCP). Originally introduced by Anthropic and now widely adopted across the AI tooling ecosystem, MCP has become the de facto standard for giving AI agents structured, safe access to external tools and data sources. Think of it as a USB-C port for AI: a universal connector that lets any compliant AI host talk to any compliant tool server, regardless of what's underneath.
This guide is written specifically for junior and mid-level backend developers who understand REST APIs and basic server concepts but have never built an MCP server before. By the end, you'll know exactly how to write one, how to expose your internal tools through it, and how to do so without touching your existing service boundaries.
What Exactly Is MCP, and Why Should You Care?
Before writing a single line of code, it's worth understanding what problem MCP actually solves. Prior to its widespread adoption, every AI integration was essentially a bespoke project. You'd write a custom plugin, a one-off function-calling schema, or a brittle webhook that tied your internal service directly to a specific model provider. When the model changed, or when you wanted to use a different AI host, you'd start over.
MCP solves this by defining a standardized client-server protocol built on top of JSON-RPC 2.0. The architecture has three main players:
- The MCP Host: The AI application or agent runtime (such as Claude Desktop, a custom LangGraph agent, or an enterprise AI orchestrator) that wants to use tools.
- The MCP Client: A thin layer inside the host that speaks the MCP protocol and manages connections to one or more servers.
- The MCP Server: Your code. A lightweight process that advertises a set of capabilities (tools, resources, and prompts) and executes them when the host asks.
The key insight is that your MCP server is a translation layer, not a replacement for your existing services. It sits in front of them, translates AI-friendly requests into the calls your services already understand, and returns structured results. Your existing REST APIs, internal SDKs, and database clients don't need to change at all.
The Three Primitives You Need to Know
MCP servers can expose three types of capabilities. As a beginner, you'll mostly work with the first one, but understanding all three is important.
1. Tools
Tools are the most important primitive. A tool is a function that an AI agent can call to perform an action or retrieve information. Each tool has a name, a human-readable description (which the AI uses to decide when to call it), and a JSON Schema that defines its input parameters. Examples include search_customer_by_email, get_order_status, or trigger_refund_workflow.
2. Resources
Resources represent data that the AI can read, similar to GET endpoints in a REST API. They are identified by URIs and can be static (like a configuration file) or dynamic (like a live database record). Resources are ideal for giving an agent read-only context without the overhead of a tool call.
3. Prompts
Prompts are reusable, parameterized message templates that your server can provide to the host. They're less commonly used by beginners but are powerful for standardizing how the AI approaches specific workflows in your domain.
Setting Up Your First MCP Server in Python
The official MCP SDK for Python (maintained under the mcp package) is the fastest way to get started. Let's walk through building a minimal but realistic server that exposes two internal tools: one to look up a user account and one to check a service's health status.
Step 1: Install the SDK
Create a new virtual environment and install the SDK:
python -m venv .venv
source .venv/bin/activate
pip install mcp httpx
We're also installing httpx because our tools will make HTTP calls to existing internal services. This is the pattern you'll use most often: your MCP server calls your existing APIs on behalf of the AI agent.
Step 2: Create Your Server File
Create a file called server.py. Here is a complete, annotated example:
import httpx
from mcp.server.fastmcp import FastMCP
# 1. Instantiate the server with a name.
# This name is how the AI host identifies your server.
mcp = FastMCP("internal-tools-server")
# 2. Define your first tool using the @mcp.tool() decorator.
# The docstring becomes the tool's description for the AI.
@mcp.tool()
async def get_user_account(email: str) -> dict:
"""
Look up a user account by email address.
Returns the user's ID, name, plan tier, and account status.
Use this before performing any account-level operations.
"""
async with httpx.AsyncClient() as client:
# This calls your EXISTING internal user service.
# No changes needed to that service whatsoever.
response = await client.get(
f"http://user-service.internal/api/v1/users",
params={"email": email},
headers={"X-Internal-Token": "your-service-token"}
)
response.raise_for_status()
return response.json()
# 3. Define a second tool.
@mcp.tool()
async def check_service_health(service_name: str) -> dict:
"""
Check the health and current status of a named internal service.
Valid service names include: 'payments', 'notifications', 'inventory'.
Returns status, uptime percentage, and last incident timestamp.
"""
async with httpx.AsyncClient() as client:
response = await client.get(
f"http://monitoring.internal/health/{service_name}"
)
return {
"service": service_name,
"status": response.json().get("status"),
"uptime": response.json().get("uptime_percent"),
"last_incident": response.json().get("last_incident_at")
}
# 4. Run the server using stdio transport (standard for local/agent use).
if __name__ == "__main__":
mcp.run(transport="stdio")
That's a fully functional MCP server. Notice a few things about this code:
- The decorator pattern (
@mcp.tool()) handles all the protocol boilerplate. The SDK automatically generates the JSON Schema for your tool's inputs from Python type hints. - The docstring is doing real work. The AI model reads this description to decide when and how to call your tool. Write it like you're explaining the function to a smart colleague, not like a code comment.
- Your existing services are called via plain HTTP. Nothing about your user service or monitoring service has changed.
Step 3: Understanding Transport Modes
MCP supports two primary transport mechanisms, and choosing the right one matters:
- stdio (Standard I/O): The host process spawns your server as a child process and communicates over stdin/stdout. This is the default for local development and for tools like Claude Desktop. It's simple, secure, and requires no network configuration.
- HTTP with SSE (Server-Sent Events): Your server runs as a standalone HTTP service. This is the right choice for production deployments where multiple AI agents or hosts need to connect to a shared server over a network.
For your first server, stdio is the right choice. When you're ready to deploy to a shared environment, switching to HTTP transport is a one-line change in the mcp.run() call.
The Golden Rule: Respect Your Service Boundaries
This is the most important architectural principle in this entire guide, and it's the one junior developers most often get wrong. Your MCP server should never become a second backend.
Here's what that means in practice:
- Do NOT put business logic in your MCP server. If your tool needs to calculate a refund amount, that calculation should live in your payments service. Your MCP tool should call the payments service and return the result. If the business logic changes, it changes in one place.
- Do NOT give your MCP server direct database access if a service already owns that data. Bypassing the service layer to query the database directly creates hidden coupling and breaks data ownership contracts.
- DO treat your MCP server like an API gateway layer. Its job is translation, authentication forwarding, and schema adaptation, not computation.
- DO keep your tools granular and single-purpose. A tool called
do_everything_for_orderis a red flag. Preferget_order_details,cancel_order, andissue_order_refundas separate tools. This gives the AI agent the flexibility to compose them correctly.
Think of your MCP server as a thin adapter between the AI's world and your services' world. The thinner it is, the better.
Writing Tool Descriptions That Actually Work
One of the most underappreciated skills in MCP development is writing good tool descriptions. The AI model uses your descriptions to make decisions, and a vague or misleading description will cause the agent to call the wrong tool, pass wrong parameters, or fail to use your tool at all.
Here are some practical rules:
Be Explicit About When to Use the Tool
Instead of: "Gets user data."
Write: "Retrieves a user's account details including subscription plan and status. Call this first whenever a request involves account-level operations or when you need to verify a user exists before taking action."
Describe the Return Value
Tell the model what it will get back. If your tool returns a dictionary with specific keys, mention the most important ones in the description. The model needs to know what to do with the result.
Document Valid Input Ranges and Formats
If a parameter expects an ISO 8601 date string, say so. If a service name must be one of a specific set of values, list them. This dramatically reduces runtime errors from the agent passing malformed inputs.
Use Warnings for Destructive Actions
If a tool triggers a side effect (sending an email, charging a card, deleting a record), say so clearly in the description. Good agents are designed to be cautious with destructive tools, but only if you flag them.
Handling Errors Gracefully
AI agents don't crash the way a human user would if something goes wrong. They interpret your error responses as information and may retry, try an alternative tool, or report the error back to the user. This means your error handling strategy matters more than you might think.
The best practice is to return structured error information rather than raising unhandled exceptions. Here's a pattern that works well:
@mcp.tool()
async def get_user_account(email: str) -> dict:
"""Look up a user account by email address."""
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"http://user-service.internal/api/v1/users",
params={"email": email},
timeout=5.0
)
if response.status_code == 404:
return {"error": "user_not_found", "message": f"No account found for email: {email}"}
response.raise_for_status()
return response.json()
except httpx.TimeoutException:
return {"error": "timeout", "message": "The user service did not respond in time. Try again shortly."}
except httpx.HTTPStatusError as e:
return {"error": "upstream_error", "message": f"User service returned status {e.response.status_code}"}
Returning a dictionary with an error key gives the AI agent actionable, parseable information. It can then decide whether to retry, escalate to the user, or try a different approach.
Testing Your MCP Server Locally
The MCP ecosystem includes a command-line inspector tool that lets you test your server interactively without needing a full AI host. You can install and run it with:
npx @modelcontextprotocol/inspector python server.py
This launches a browser-based UI where you can see all the tools your server advertises, call them with custom inputs, and inspect the raw JSON-RPC messages being exchanged. It's invaluable for debugging and for verifying that your tool schemas look correct before connecting to a real agent.
For automated testing, treat your MCP server like any other Python service: write unit tests for each tool function by mocking the HTTP calls to your internal services with a library like pytest-httpx. The tool functions are just async Python functions, so they're easy to test in isolation.
A Quick Security Checklist Before You Deploy
Exposing internal tools to an AI agent introduces a new attack surface. Before you ship anything, run through this checklist:
- Authenticate every tool call. If your MCP server is running in HTTP mode, require a valid token on every request. Don't assume that because it's "internal" it's safe.
- Validate all inputs. The AI model generates inputs based on its understanding of your schema. Treat every input as untrusted. Use Pydantic models or explicit validation before passing values to your services.
- Apply the principle of least privilege. The service token your MCP server uses to call internal APIs should have only the permissions it needs. Don't use an admin token because it's convenient.
- Log every tool invocation. You want a full audit trail of what the AI agent called, with what parameters, and what was returned. This is essential for debugging and for compliance in regulated industries.
- Rate-limit your tools. An agent in a loop can call your tools hundreds of times per minute. Make sure your internal services can handle this, or add rate limiting at the MCP server layer.
Where to Go From Here
Once you've built and deployed your first MCP server, a few natural next steps open up:
- Add Resources. Expose read-only data like configuration documents, runbooks, or schema definitions as MCP resources. This gives agents rich context without requiring tool calls.
- Explore multi-server architectures. A single AI host can connect to multiple MCP servers simultaneously. You can have one server for user data, one for billing, and one for infrastructure tools, each maintained by a different team.
- Contribute to your organization's MCP registry. Many engineering teams in 2026 are maintaining internal registries of approved MCP servers, similar to how they maintain internal package registries. Getting your server listed makes it reusable across multiple AI products.
- Read the official MCP specification. The full spec at
modelcontextprotocol.iocovers advanced topics like sampling (letting your server ask the AI a question mid-tool-call), roots, and progress notifications.
Conclusion
The Model Context Protocol represents one of the most pragmatic shifts in how backend developers interact with AI systems. Instead of bending your architecture to fit a specific model's quirks, MCP gives you a clean, stable contract: you define what your tools do, the AI figures out when to use them.
For junior backend developers, this is genuinely good news. Building an MCP server doesn't require a deep understanding of machine learning or prompt engineering. It requires exactly the skills you already have: designing clean interfaces, writing reliable HTTP clients, handling errors gracefully, and thinking carefully about security. The AI side of the equation is largely handled for you.
Start small. Pick two or three internal tools that your team constantly looks up manually, wrap them in an MCP server, and connect it to your team's AI assistant. The feedback you'll get from watching an agent actually use your tools in real workflows is worth more than any tutorial, including this one.
The best MCP server is the one you ship this week. Go build it.