Large language models are excellent at understanding language, generating content, and reasoning over information.
But an LLM cannot automatically access your database, inspect a local file, check a deployment pipeline, update a ticket, or call an internal API.
Developers have traditionally solved this by building custom integrations for every combination of:
- AI application
- Model provider
- Data source
- Business system
- External tool
That approach works until the number of integrations begins to grow.
The Model Context Protocol, or MCP, provides a standardized way for AI applications to discover and interact with external tools, data sources, and reusable workflows.
Instead of building a separate integration for every AI client, you can expose a capability once through an MCP server and make it available to compatible clients.
In this tutorial, we will understand the MCP architecture and build a practical release-readiness MCP server using Python. We will then test it with MCP Inspector and package it with Docker.
What is the Model Context Protocol?
MCP is an open protocol for connecting AI applications to external systems.
A helpful comparison is USB-C.
Before USB-C, devices often required different connectors. MCP attempts to provide a similarly standardized connection between AI applications and the systems they need to use.
Through MCP, an AI application can connect to:
- Local files
- Databases
- REST APIs
- Search engines
- Development tools
- Cloud platforms
- Internal business systems
- Reusable prompt workflows
MCP was introduced by Anthropic in November 2024 and has since developed into a broader open ecosystem for AI integrations.
The problem MCP solves
Imagine that you are building an engineering assistant.
The assistant needs to:
- Read deployment documentation.
- Check the number of failed tests.
- inspect whether a database migration is included.
- Calculate deployment risk.
- Generate a release checklist.
Without MCP, you might implement a custom function-calling interface for one model provider.
Later, another team wants to use the same functionality from a different AI client. You may need another integration.
Then someone wants to use it from an IDE.
Then from a desktop assistant.
Then from an internal agent platform.
The underlying capability has not changed, but the integration code keeps multiplying.
MCP separates these concerns:
AI application concerns:
- User interaction
- Model selection
- Reasoning
- Tool selection
- Approval experience
MCP server concerns:
- Business capability
- Input validation
- Data access
- External API calls
- Authorization
- Tool execution
The server exposes a standardized interface, while the AI application decides when and how to use it.
MCP architecture
A typical MCP interaction contains four main pieces:
MCP host
The host is the application in which the user interacts with the AI.
Examples include an AI-enabled IDE, coding assistant, desktop assistant, or enterprise agent platform.
MCP client
The client lives inside the host and maintains the connection to an MCP server.
When a host connects to multiple MCP servers, it commonly creates a separate client connection for each server.
MCP server
The MCP server exposes capabilities to the client.
A server might provide access to GitHub, Jira, a database, a local filesystem, a cloud service, or an internal application.
Transport
The transport carries MCP messages between clients and servers.
For local integrations, the most common option is standard input/output, or stdio. The host launches the MCP server as a subprocess and exchanges JSON-RPC messages through its input and output streams.
For remote servers, the current standard transport is Streamable HTTP. It replaced the older standalone HTTP+SSE transport used by the original MCP specification.
Tools, resources, and prompts
An MCP server can expose several primitives. The three most important for beginners are tools, resources, and prompts.
Tools
Tools are executable functions.
Examples include:
- Create a support ticket
- Query a database
- Calculate deployment risk
- Send a notification
- Run a test
- Update a customer record
A tool may produce side effects, so the host should make the action visible and request approval when appropriate.
Resources
Resources expose information that can be loaded into the model’s context.
Examples include:
- A deployment runbook
- Product documentation
- A configuration file
- A database record
- A knowledge-base article
A resource is conceptually similar to reading information through a GET endpoint.
Prompts
Prompts are reusable interaction templates.
An MCP server can publish a prompt that tells the host how to perform a particular workflow, such as reviewing a release, investigating an incident, or summarizing a customer account.
The official Python SDK describes resources as data-loading interfaces, tools as executable functionality, and prompts as reusable interaction patterns.
MCP is not the same as function calling
Function calling allows a model to produce structured arguments for a function.
MCP operates at a different layer.
Function calling:
Model decides that a function should be invoked.
MCP:
Standardizes how AI applications discover, describe, connect to,
and invoke capabilities supplied by external servers.
An MCP host may still use function calling internally. MCP gives the host a standardized source of tools and schemas.
MCP is not a replacement for APIs
MCP servers frequently use existing APIs internally.
For example:
User
↓
AI application
↓
MCP tool: create_issue
↓
Jira REST API
↓
New Jira issue
The REST API remains the interface used by the underlying application.
MCP provides an AI-oriented layer that describes the tool, publishes its input schema, handles the protocol, and returns the result to the AI client.
MCP versus RAG
Retrieval-Augmented Generation and MCP can work together, but they are not interchangeable.
| RAG | MCP |
|---|---|
| Retrieves relevant information | Connects AI applications to capabilities |
| Usually supports read-oriented workflows | Can retrieve information and perform actions |
| Adds retrieved text to model context | Exposes tools, resources, and prompts |
| Commonly uses search or vector retrieval | Uses a standardized client-server protocol |
A RAG pipeline might retrieve relevant policy documents.
An MCP server might expose that retriever as a resource or tool while also providing tools to open a case, request human approval, or update a workflow.
RAG is primarily a retrieval technique. MCP is a broader integration protocol for data access and action.
Building a release-readiness MCP server
We will build a server that exposes:
- A tool for calculating release risk
- A tool for generating a deployment checklist
- A release-runbook resource
- A reusable release-review prompt
The example does not require an API key or an external service.
Prerequisites
You will need:
- Python 3.10 or later
- A terminal
- Node.js for MCP Inspector
- Docker for the container section
The current stable Python SDK line is v1.x. Because v2 remains prerelease at the time of writing, we will explicitly pin the dependency below version 2.
Step 1: Create the project
mkdir release-readiness-mcp
cd release-readiness-mcp
python -m venv .venv
source .venv/bin/activate
On Windows:
.venv\Scripts\activate
Install the MCP SDK:
pip install "mcp[cli]>=1.27,<2"
Save the dependency in requirements.txt:
mcp[cli]>=1.27,<2
Step 2: Create the MCP server
Create a file named server.py:
from typing import Literal
from mcp.server.fastmcp import FastMCP
mcp = FastMCP(
"release-readiness",
instructions=(
"Use this server to assess software release risk, "
"generate deployment checklists, and review release plans."
),
)
@mcp.tool()
def assess_release_risk(
changed_files: int,
failed_tests: int = 0,
has_database_migration: bool = False,
has_rollback_plan: bool = True,
) -> dict[str, object]:
"""Estimate the risk of a proposed software release.
Args:
changed_files: Number of files changed in the release.
failed_tests: Number of currently failing automated tests.
has_database_migration: Whether the release changes the database.
has_rollback_plan: Whether a documented rollback plan exists.
"""
if changed_files < 0:
raise ValueError("changed_files cannot be negative")
if failed_tests < 0:
raise ValueError("failed_tests cannot be negative")
score = 0
reasons: list[str] = []
if changed_files > 100:
score += 3
reasons.append("The release contains more than 100 changed files.")
elif changed_files > 30:
score += 2
reasons.append("The release contains a moderately large change set.")
elif changed_files > 10:
score += 1
reasons.append("The release contains several changed files.")
if failed_tests > 0:
score += min(failed_tests * 2, 6)
reasons.append(f"{failed_tests} automated test(s) are failing.")
if has_database_migration:
score += 2
reasons.append("The release includes a database migration.")
if not has_rollback_plan:
score += 3
reasons.append("A documented rollback plan is missing.")
if score <= 2:
level = "low"
elif score <= 6:
level = "medium"
else:
level = "high"
recommendations = []
if failed_tests:
recommendations.append("Resolve or formally approve all failing tests.")
if has_database_migration:
recommendations.append(
"Validate migration compatibility and test database rollback."
)
if not has_rollback_plan:
recommendations.append(
"Create and review a rollback plan before deployment."
)
if not recommendations:
recommendations.append(
"Proceed through the normal deployment approval process."
)
return {
"risk_level": level,
"risk_score": score,
"reasons": reasons or ["No major release-risk indicators were detected."],
"recommendations": recommendations,
}
@mcp.tool()
def build_deployment_checklist(
environment: Literal["development", "staging", "production"],
) -> list[str]:
"""Generate a deployment checklist for the selected environment."""
checklist = [
"Confirm the intended artifact version.",
"Review automated test results.",
"Verify configuration and environment variables.",
"Confirm deployment ownership.",
]
if environment in {"staging", "production"}:
checklist.extend(
[
"Complete smoke testing.",
"Verify monitoring dashboards and alerts.",
"Review dependent services.",
]
)
if environment == "production":
checklist.extend(
[
"Record the change approval.",
"Confirm the rollback plan.",
"Notify affected stakeholders.",
"Monitor critical metrics after deployment.",
]
)
return checklist
@mcp.resource("runbook://release")
def release_runbook() -> str:
"""Return the standard software-release runbook."""
return """
# Release Runbook
1. Review the scope of the change.
2. Confirm that automated tests have completed.
3. Validate configuration changes.
4. Review database migrations.
5. Confirm monitoring and alert coverage.
6. Document rollback instructions.
7. Obtain the required approval.
8. Deploy to the target environment.
9. Run post-deployment validation.
10. Record the outcome of the release.
""".strip()
@mcp.prompt()
def review_release(
service_name: str,
environment: str = "production",
) -> str:
"""Create a reusable prompt for reviewing a release."""
return f"""
Review the proposed release for the service "{service_name}"
to the "{environment}" environment.
Use the release runbook and available risk-assessment tools.
Return:
1. Release-risk level
2. Primary risk factors
3. Missing information
4. Required validation
5. Rollback considerations
6. Final recommendation
Do not recommend deployment when unresolved critical risks remain.
""".strip()
if __name__ == "__main__":
mcp.run(transport="stdio")
FastMCP uses Python type hints and docstrings to generate tool descriptions and input schemas. This removes much of the manual JSON Schema and protocol-handling code that would otherwise be required.
Step 3: Understand what we built
The server publishes two tools.
assess_release_risk
build_deployment_checklist
It also publishes one resource:
runbook://release
And one prompt:
review_release
When an MCP client connects, it can discover these capabilities without us manually creating a separate integration contract for that client.
For example, a user might ask:
Review the release readiness of the payments service. It changes 42 files, includes a database migration, has no failing tests, and has a rollback plan.
The host can:
- Discover
assess_release_risk. - Extract the arguments from the request.
- Ask the user for approval if required.
- Call the tool.
- Receive the structured result.
- Use the result when composing its response.
The LLM does not directly execute the Python function. The MCP host and client remain between the model and the server.
Step 4: Test with MCP Inspector
MCP Inspector is an interactive debugging tool for connecting to servers, examining their capabilities, and invoking tools, resources, and prompts.
Run:
mcp dev server.py
You can also start the Inspector explicitly:
npx -y @modelcontextprotocol/inspector python server.py
The command opens an Inspector interface in your browser.
From the Inspector, you can:
- Connect to the server.
- Open the Tools section.
- Select
assess_release_risk. - Enter test arguments.
- Run the tool.
- Inspect the structured result.
- Test the published resource and prompt.
The official MCP documentation recommends Inspector as the first tool for testing and debugging MCP servers.
Step 5: Connect the server to Claude Code
Claude Code can launch a local MCP server as a subprocess.
Use the absolute path to server.py:
claude mcp add release-readiness -- \
python /absolute/path/to/release-readiness-mcp/server.py
List the configured servers:
claude mcp list
Inside Claude Code, you can also use:
/mcp
Now try a request such as:
Assess a production release with 75 changed files,
two failed tests, a database migration,
and no documented rollback plan.
The client should discover the appropriate tool and request permission before invoking it.
The same basic server can also be connected to other compatible clients, although their registration and configuration formats may differ.
Containerizing the MCP server
A container provides a reproducible Python environment and avoids requiring every user to configure the project dependencies manually.
Step 6: Create the Dockerfile
Create Dockerfile:
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY server.py .
ENTRYPOINT ["python", "/app/server.py"]
Build the image:
docker build -t release-readiness-mcp .
Because the server uses stdio, start the container with interactive input enabled:
docker run -i --rm release-readiness-mcp
The process may appear to wait silently. That is expected: it is waiting for MCP messages through standard input.
Do not print normal application logs to standard output when using stdio. Standard output is reserved for valid MCP protocol messages. Send diagnostic logging to standard error instead.
Step 7: Register the Dockerized server
You can ask Claude Code to launch the container instead of running Python directly:
claude mcp add release-readiness-docker -- \
docker run -i --rm release-readiness-mcp
The interaction now looks like this:
Claude Code
│
│ launches
▼
Docker container
│
│ runs
▼
Python MCP server
│
├── assess_release_risk
├── build_deployment_checklist
├── runbook://release
└── review_release
The -i option is important because it keeps the container’s standard input open for MCP communication.
Moving from a local server to a remote server
A local stdio server works well for:
- Developer utilities
- Local file access
- IDE extensions
- Personal automation
- Private workstation workflows
A remote server is more appropriate when:
- Multiple users need the same capability
- The server accesses shared enterprise systems
- Centralized authentication is required
- The service must scale independently
- Central monitoring and auditing are necessary
For remote MCP servers, use Streamable HTTP, not the deprecated standalone HTTP+SSE transport.
With stable FastMCP v1.x, the transport can be changed to:
if __name__ == "__main__":
mcp.run(transport="streamable-http")
The server is then commonly exposed through an endpoint such as:
https://example.com/mcp
Remote deployment introduces additional requirements, including authentication, authorization, TLS, rate limiting, tenant isolation, origin validation, and centralized auditing.
Security considerations
An MCP server can provide an AI application with meaningful access to real systems. Treat it as an application integration boundary, not merely as a prompt extension.
Apply least privilege
A tool should receive only the permissions it needs.
A read-only documentation server should not possess credentials that can modify production data.
Separate read and write tools
Prefer explicit tools such as:
get_customer
update_customer
delete_customer
Avoid a single generic tool that can execute arbitrary operations.
This gives the host a better opportunity to distinguish low-risk retrieval from high-impact actions.
Validate every input
Do not assume that arguments generated by an LLM are safe or correct.
Validate:
- Identifiers
- Paths
- URLs
- Numeric ranges
- Enumerated values
- Query parameters
- Requested operations
Require approval for consequential actions
Creating, updating, deleting, publishing, deploying, or sending information should normally be visible to the user before execution.
The model proposing an action is not equivalent to the user authorizing it.
Protect secrets
Keep credentials in environment variables or a secret-management system.
Do not return tokens, passwords, connection strings, or unrelated sensitive records in tool responses.
Restrict filesystem access
A filesystem tool should use an allowlisted root directory and reject attempts to escape it.
A tool intended to read one project should not automatically receive access to the entire machine.
Authenticate remote servers
Remote MCP endpoints should not be exposed as unauthenticated public interfaces when they access private data or perform privileged operations.
Log tool activity
Record enough information to investigate:
- Which tool was called
- When it was called
- Which user or client initiated it
- Whether approval was provided
- Whether execution succeeded
- Which external system was accessed
MCP security guidance emphasizes user consent, privacy, scope minimization, safe tool execution, authorization, monitoring, and protection against untrusted servers and outputs.
Common MCP design mistakes
Exposing one giant tool
A tool named execute_action with a large free-form input makes behavior difficult for both the model and the user to understand.
Prefer small, clearly described tools.
Writing vague descriptions
The tool description helps the model decide whether and how the tool should be used.
Bad:
"""Processes data."""
Better:
"""Estimate deployment risk from the change size,
failed tests, database changes, and rollback readiness."""
Returning unnecessary data
Returning an entire API response can waste context and expose irrelevant fields.
Return the information the model actually needs, preferably as a structured result.
Mixing logs with protocol output
For stdio servers, writing normal logs to stdout can corrupt the protocol stream.
Use stderr or a proper logging configuration.
Treating the LLM as an authorization system
An LLM can help select a tool, but it should not be the only mechanism deciding whether a user is allowed to perform an operation.
Authorization must be enforced by deterministic application logic.
Assuming MCP automatically makes a tool safe
MCP standardizes communication.
It does not automatically provide:
- Correct authorization
- Safe business logic
- Input validation
- Data isolation
- Human approval
- Auditability
- Protection against prompt injection
Those remain application and platform responsibilities.
Where MCP fits in an agentic system
MCP is one component of an agent architecture.
User request
↓
Agent or AI application
↓
Planning and tool selection
↓
Policy and authorization checks
↓
MCP client
↓
MCP server
↓
External system
↓
Tool result
↓
Validation and observation
↓
Final response or next action
MCP standardizes the connection to tools and context.
The surrounding system must still manage:
- Planning
- State
- Memory
- Identity
- Authorization
- Human approval
- Retries
- Evaluation
- Observability
- Cost controls
- Error recovery
In other words, MCP gives an agent standardized hands and connections. It does not replace the agent’s brain, policies, or operational harness.
Final thoughts
MCP is valuable because it separates AI interaction from capability implementation.
You can build a server that exposes a well-defined tool, resource, or workflow and connect it to multiple compatible AI applications.
In this tutorial, we created a server that:
- Published typed Python functions as MCP tools
- Exposed a release runbook as a resource
- Defined a reusable release-review prompt
- Used
stdiofor local communication - Was tested with MCP Inspector
- Connected to Claude Code
- Ran inside Docker
The example is intentionally small, but the same architecture can support production integrations with databases, ticketing platforms, cloud services, internal APIs, and enterprise workflows.
The most important design principle is not to expose every system capability to an AI model.
Expose narrow, understandable, permission-aware capabilities—and make every consequential action observable and controllable.

Top comments (0)