DEV Community

Cover image for Model Context Protocol Explained: Build Your First MCP Server with Python and Docker
Anuj Tyagi
Anuj Tyagi

Posted on

Model Context Protocol Explained: Build Your First MCP Server with Python and Docker

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:

  1. Read deployment documentation.
  2. Check the number of failed tests.
  3. inspect whether a database migration is included.
  4. Calculate deployment risk.
  5. 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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

On Windows:

.venv\Scripts\activate
Enter fullscreen mode Exit fullscreen mode

Install the MCP SDK:

pip install "mcp[cli]>=1.27,<2"
Enter fullscreen mode Exit fullscreen mode

Save the dependency in requirements.txt:

mcp[cli]>=1.27,<2
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

It also publishes one resource:

runbook://release
Enter fullscreen mode Exit fullscreen mode

And one prompt:

review_release
Enter fullscreen mode Exit fullscreen mode

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:

  1. Discover assess_release_risk.
  2. Extract the arguments from the request.
  3. Ask the user for approval if required.
  4. Call the tool.
  5. Receive the structured result.
  6. 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
Enter fullscreen mode Exit fullscreen mode

You can also start the Inspector explicitly:

npx -y @modelcontextprotocol/inspector python server.py
Enter fullscreen mode Exit fullscreen mode

The command opens an Inspector interface in your browser.

From the Inspector, you can:

  1. Connect to the server.
  2. Open the Tools section.
  3. Select assess_release_risk.
  4. Enter test arguments.
  5. Run the tool.
  6. Inspect the structured result.
  7. 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
Enter fullscreen mode Exit fullscreen mode

List the configured servers:

claude mcp list
Enter fullscreen mode Exit fullscreen mode

Inside Claude Code, you can also use:

/mcp
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

Build the image:

docker build -t release-readiness-mcp .
Enter fullscreen mode Exit fullscreen mode

Because the server uses stdio, start the container with interactive input enabled:

docker run -i --rm release-readiness-mcp
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

The server is then commonly exposed through an endpoint such as:

https://example.com/mcp
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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."""
Enter fullscreen mode Exit fullscreen mode

Better:

"""Estimate deployment risk from the change size,
failed tests, database changes, and rollback readiness."""
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 stdio for 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.

Further reading

Top comments (0)