Isometric illustration of a central MCP server chip connected to icons for database, security, keys, and configuration

KI-Buster Blog · MCP

Build Your Own MCP Server: Architecture and a Python Example

Building your own MCP server sounds like a complex AI project at first. In reality, a first working server can be built with just a few lines of Python – as long as you start with a clean architecture and an allowlist instead of an open shell.

Published and reviewed on September 9, 2026

Building your own MCP server sounds like a complex AI project at first. In reality, a first working server can be built with just a few lines of Python.

MCP only gets interesting once it goes beyond a simple demo server, though. The Model Context Protocol becomes exciting once an AI is allowed to access your own data, APIs, or administrative functions through clearly defined interfaces.

That is exactly what we build in this tutorial. We create a small MCP server for Linux that lets an MCP-capable AI client query the status of selected systemd services. The AI explicitly does not get a free shell. Instead, it may only check services that have been defined in advance.

The example demonstrates the most important principles of a sound MCP architecture at the same time:

  • cleanly separate MCP host, client, and server
  • expose tools in a controlled way
  • use resources as a context source
  • define prompts
  • validate inputs
  • limit permissions
  • distinguish local from remote transports
  • test the MCP server
  • bake security boundaries into the design from the start

What is an MCP server?

MCP stands for Model Context Protocol. The open protocol standardizes communication between AI applications and external systems. We cover the basics of hosts, clients, and servers in detail in Model Context Protocol (MCP) Explained Simply (Read article).

An MCP server can, for example, provide access to systems such as:

  • files
  • databases
  • REST APIs
  • monitoring systems
  • ticketing systems
  • Git repositories
  • internal company services
  • cloud platforms
  • Linux systems
  • automation tools

A common misconception is worth calling out here: the MCP server is not the AI. It also does not normally talk directly to the language model.

The official MCP architecture distinguishes three components:

User
↓
AI application / MCP host
↓
MCP client
↓
MCP server
├── Tool
├── Resource
└── Prompt
↓
Files / APIs / Linux / database / infrastructure

The host is, for example, an AI application or a development environment. Inside this host sits an MCP client. This client communicates with your MCP server using the Model Context Protocol. The server then provides exactly the functions and information you have allowed it to expose.

This separation matters because it prevents every AI system from needing a completely custom integration.

The three core building blocks of an MCP server

At its core, an MCP server provides three different kinds of functionality:

1. Tools

Tools are executable functions. A tool might be called service_status, for example. The AI could then call it with a parameter: service = nginx. The MCP server runs the underlying Python function and returns the result.

Other typical tools would be:

  • get_server_status
  • check_backup
  • create_ticket
  • restart_container
  • query_database
  • get_certificate_expiry

Tools are especially powerful because they can execute actions. That also makes them the most security-critical part of many MCP servers.

2. Resources

Resources provide information. A resource might, for example, contain the list of allowed Linux services, or the documentation for an internal API.

Further examples: config://production, documentation://backup-system, server://inventory, policy://allowed-services.

Resources are well suited for information an AI system should read without triggering an action.

3. Prompts

An MCP server can also provide prepared prompts. A prompt might define, for example:

Analyze the service status and explain possible causes,
without making any changes to the server.

This lets you standardize recurring workflows.

The current official Python documentation describes tools, resources, and prompts as exactly the three central server primitives. Names, descriptions, and schemas can be derived directly from Python functions, docstrings, and type hints.

Local MCP server or remote MCP server?

Before we start writing Python, we still need to decide how the server should be reachable. MCP supports several transport options. Two are particularly relevant for new implementations:

TransportUse case
STDIOlocal MCP server
Streamable HTTPremote MCP server

STDIO

STDIO is particularly well suited to local MCP servers. The MCP host starts the server as its own process and communicates with it over standard input and standard output.

That is ideal for local developer tools, desktop applications, coding agents, local admin tools, and personal MCP servers.

STDIO is also the default transport in the current Python SDK.

Streamable HTTP

If the MCP server needs to be reachable over the network, Streamable HTTP comes into play. For example:

AI client
↓ HTTPS
Reverse proxy
↓
MCP server
├── internal API
├── monitoring
└── database

New remote implementations should no longer rely on the older HTTP+SSE transport. The current MCP specification from July 28, 2026 leans further into a stateless protocol core, and the legacy SSE transport is now considered deprecated.

Practical example: build your own MCP server with Python

Now let's build our server. The goal: an AI system is allowed to query the status of a few approved Linux services via MCP. We allow nginx, ssh, and docker. Arbitrary service names or free-form shell commands are not allowed.

That way we avoid a classic anti-pattern such as:

subprocess.run(command, shell=True)

A tool like that would turn an MCP server into a practical remote shell. That is explicitly not what we want.

Step 1: Set up the Python project

The current MCP Python SDK requires Python 3.10 or newer. The official documentation describes version 2 as the current stable release line.

With uv, the project can be created like this:

uv init mcp-linux-status
cd mcp-linux-status
uv add "mcp[cli]"

Alternatively, plain pip also works:

python3 -m venv .venv
source .venv/bin/activate

pip install "mcp[cli]"

After that, we create server.py.

Step 2: Create the MCP server

Our basic skeleton looks like this:

from mcp.server import MCPServer

mcp = MCPServer("Linux Service Status")

if __name__ == "__main__":
    mcp.run()

This already is an MCP server. It cannot do anything particularly interesting yet, though. So let's add our first tool.

Step 3: Create an MCP tool

First, we define an allowlist.

ALLOWED_SERVICES = {
    "nginx",
    "ssh",
    "docker",
}

Only these services may be checked. Now we create our tool:

import subprocess
from mcp.server import MCPServer

mcp = MCPServer("Linux Service Status")

ALLOWED_SERVICES = {
    "nginx",
    "ssh",
    "docker",
}

@mcp.tool()
def service_status(service: str) -> dict:
    """Returns the systemd status of an approved service."""
    if service not in ALLOWED_SERVICES:
        return {
            "service": service,
            "status": "denied",
            "message": "This service is not approved.",
        }

    result = subprocess.run(
        ["systemctl", "is-active", service],
        capture_output=True,
        text=True,
        timeout=5,
        check=False,
    )

    status = result.stdout.strip()
    if not status:
        status = result.stderr.strip()

    return {
        "service": service,
        "status": status,
        "returncode": result.returncode,
    }

if __name__ == "__main__":
    mcp.run()

Our MCP server now has the tool service_status. A client can, for example, call service_status("nginx") and get back a structured response, such as:

{
  "service": "nginx",
  "status": "active",
  "returncode": 0
}

Why an allowlist matters so much

You could, of course, simply write:

subprocess.run(["systemctl", "is-active", service])

and accept any service name. Even more dangerous would be:

subprocess.run(user_input, shell=True)

That would quickly turn a tool into a general-purpose command executor. A well-designed MCP server instead follows a different principle:

AI
↓
defined MCP tools
↓
validation
↓
allowlist
↓
limited operating-system function

The AI does not get shell access this way, only the ability to ask a precisely defined question of the operating system. That is a significant difference.

Step 4: Add an MCP resource

Now we want to let the client know which services can even be queried. For that, we create a resource:

@mcp.resource("service-policy://allowed")
def allowed_services() -> str:
    """List of services exposed via MCP."""
    return "\n".join(sorted(ALLOWED_SERVICES))

The server now also has the resource service-policy://allowed. The client can read it and gets back, for example:

docker
nginx
ssh

Resources are great for this kind of metadata.

Step 5: Add an MCP prompt

We also add a small diagnostic prompt:

@mcp.prompt()
def diagnose_service(service: str) -> str:
    """Creates a safe prompt for service diagnostics."""
    return (
        f"Analyze the status of the Linux service '{service}'. "
        "Use read-only checks only. "
        "Do not make any changes, restarts, or "
        "configuration changes without explicit approval."
    )

Our server now offers all three basic MCP building blocks:

Linux Service MCP Server
├── Tool
│   └── service_status
├── Resource
│   └── service-policy://allowed
└── Prompt
    └── diagnose_service

That already gives us a small but sensibly structured MCP server.

The complete MCP server

Our server.py file now looks like this:

import subprocess
from mcp.server import MCPServer

mcp = MCPServer("Linux Service Status")

ALLOWED_SERVICES = {
    "nginx",
    "ssh",
    "docker",
}

@mcp.tool()
def service_status(service: str) -> dict:
    """Returns the systemd status of an approved service."""
    if service not in ALLOWED_SERVICES:
        return {
            "service": service,
            "status": "denied",
            "message": "This service is not approved.",
        }

    result = subprocess.run(
        ["systemctl", "is-active", service],
        capture_output=True,
        text=True,
        timeout=5,
        check=False,
    )

    status = result.stdout.strip()
    if not status:
        status = result.stderr.strip()

    return {
        "service": service,
        "status": status,
        "returncode": result.returncode,
    }

@mcp.resource("service-policy://allowed")
def allowed_services() -> str:
    """List of services exposed via MCP."""
    return "\n".join(sorted(ALLOWED_SERVICES))

@mcp.prompt()
def diagnose_service(service: str) -> str:
    """Creates a safe prompt for service diagnostics."""
    return (
        f"Analyze the status of the Linux service '{service}'. "
        "Use read-only checks only. "
        "Do not make any changes, restarts, or "
        "configuration changes without explicit approval."
    )

if __name__ == "__main__":
    mcp.run()

That is already a complete, small MCP server.

Testing an MCP server with the Inspector

Before connecting an MCP server to a real AI, it should be tested in isolation. The official Python SDK ships with support for the MCP Inspector.

Our example can be started with uv run mcp dev server.py. The Inspector shows the available tools, resources, resource templates, and prompts. Our tool can be called directly from there, for example service_status with the parameter service = nginx.

The official Python documentation explicitly recommends this workflow during development and describes the Inspector as a tool for testing the primitives a server publishes.

Starting an MCP server over STDIO

With mcp.run() inside the if __name__ == "__main__": block, the current Python SDK defaults to STDIO. That is well suited to local clients.

One thing to keep in mind: STDOUT is part of the protocol for a STDIO MCP server. Uncontrolled print() output can therefore cause problems. Use proper logging for your own diagnostic messages instead.

The official SDK documentation also points out that stdin and stdout are the actual communication channel for the STDIO transport.

Turning the local server into a remote MCP server

Our server can also be offered over HTTP. All we need to change is the startup code:

if __name__ == "__main__":
    mcp.run(
        transport="streamable-http",
        stateless_http=True,
        json_response=True,
    )

Our server now uses Streamable HTTP. The current documentation recommends combining Streamable HTTP, stateless HTTP, and JSON responses for scalable remote deployments.

The architecture then looks something like this:

MCP client
↓ HTTPS
Reverse proxy
├── TLS
├── authentication
├── rate limiting
└── logging
↓
MCP server
↓
Linux system

For a production system, it is not enough to simply expose a port to the internet, though.

Why the 2026 MCP specification matters for remote servers

The MCP specification from July 28, 2026 changed the remote architecture significantly. The protocol core has become more stateless for modern connections. A request no longer necessarily needs to be handled by the same server instance as a previous request.

That makes things like horizontal scaling, load balancing, reverse proxies, API gateways, cloud deployments, and multiple MCP instances easier. For modern HTTP requests, gateways can also use MCP-specific header information for routing and authorization.

For small local MCP servers, you barely need to think about these details at first. Once MCP becomes part of enterprise infrastructure, though, they become important.

Security rules for your own MCP server

A working MCP server is quick to build. A secure MCP server takes more thought. For a deeper dive into this topic, see How to Operate MCP Servers Securely: Permissions, Tools, and Risks Explained (Read article).

1. Don't provide a general-purpose shell

Avoid tools like execute_command(command) or run_shell(command). Such tools are extremely powerful. Narrowly defined functions such as get_service_status(service), get_disk_usage(), check_backup_status(), or get_certificate_expiry() are a better fit.

2. Validate inputs

Every parameter should be checked. In our example, the allowlist handles that: if service not in ALLOWED_SERVICES: .... The same principle applies to file paths, database tables, APIs, users, hostnames, containers, virtual machines, and Git repositories.

3. Don't run the MCP server as root

Our example normally does not need root privileges for systemctl is-active. So why should the MCP process get root? Ground rule: an MCP server should only have the privileges its defined tools actually need.

4. Separate read and write access

A tool like service_status only reads information. A tool like restart_service, on the other hand, changes system state. These functions should not be lumped carelessly into the same permission level. A better architecture might look like this:

READ ONLY
├── get_status
├── get_logs
├── get_disk_usage
└── get_backup_status

PRIVILEGED
├── restart_service
├── deploy_update
└── change_configuration

Write-capable functions can additionally require explicit approval.

5. Authenticate remote MCP servers

A publicly reachable MCP endpoint must not simply hand administrative tools to every client. Depending on the architecture, options include OAuth, OIDC, access tokens, scopes, an identity provider, or centralized enterprise authentication.

MCP uses an OAuth-based authorization architecture for protected remote resources. The 2026 specification hardened these mechanisms further, among other things around issuer validation and credential binding.

6. Plan for logs and audit trails

For administrative MCP servers, you should be able to trace which tool was called, when, with which parameters, by which user, what the result was, and whether an action was denied. That traceability matters especially once MCP is used inside an organization.

An MCP server is a security boundary

This point is underestimated in many MCP tutorials. An MCP server is not just an adapter. It defines a permission boundary between an AI and your infrastructure.

The real question is therefore not: "What could I let the AI do?" It is: "What does the AI actually need to be able to do for this specific task?"

"The AI should administer Linux" should therefore not automatically turn into "the AI gets SSH and root." Better: "The AI should be able to tell whether important services are running." That gives you service_status().

Should it also analyze log messages? Then another tool could emerge: get_service_logs(service, lines=50). Again, this is scoped: which services, how many log lines, which time range – no free-form journalctl parameters, no shell, no write access.

That is exactly how a good MCP server grows in a controlled way.

What a larger admin MCP server could look like

Our small example could later grow into an architecture like this:

MCP CLIENT
↓
MCP SERVER
├── Monitoring
│   ├── get_status
│   ├── get_load
│   └── get_alerts
├── Linux
│   ├── service_status
│   ├── get_logs
│   └── disk_usage
└── Backup
    ├── check_backup
    ├── last_backup
    └── backup_errors

Each function serves a clearly scoped purpose. That keeps it transparent for administrators which privileges the AI actually has.

Common mistakes when building an MCP server

Too many responsibilities in one tool

A tool should have one clearly defined job. Bad: manage_server(). Better: get_service_status(), get_disk_usage(), get_service_logs().

Allowing arbitrary shell commands

This is one of the most dangerous design mistakes. Controlled functions should be offered instead of a free-form command line. Coding agents such as OpenAI Codex face a very similar challenge, since they need shell and filesystem access to do their job – see How to Use OpenAI Codex Safely: Git, Sandbox, Backups, and Approvals (Read article) for more.

Storing secrets directly in code

Passwords, API keys, and tokens do not belong in server.py. Use environment variables, secret stores, vault systems, container secrets, or Kubernetes secrets instead.

Running remote MCP without authentication

A network port alone is not a security architecture. Especially for tools with access to internal systems, proper identity and authorization checks are essential.

Returning tool results unfiltered

Return data can be sensitive too – passwords, tokens, internal IP addresses, personal data, full log files, configuration files, or database contents. An MCP server should therefore control not only what goes in, but also what comes out.

MCP servers with Python: why the SDK does so much of the work

What's interesting about our example is what we did not have to program. We wrote no custom implementation of tools/list, resources/read, prompts/get, protocol negotiation, schema generation, or MCP messages. The SDK handles all of that.

From @mcp.tool() def service_status(service: str) -> dict:, the SDK can derive the description and the required input schema, among other things. That is one of the biggest advantages of an official MCP SDK.

Which programming languages does MCP support?

You don't have to write an MCP server in Python. Official SDKs exist for several languages. Currently these include TypeScript, Python, C#, Go, Rust, and Java, among others.

Python, TypeScript, C#, Go, and – since August 2026 – Rust are listed as Tier 1 SDKs in the current official SDK overview. Which language makes sense depends mainly on your existing infrastructure. For Linux administration and quick internal tools, though, Python is particularly pleasant to work with.

When is your own MCP server worth building?

Building your own MCP server is worthwhile especially when your AI regularly needs to access your own systems. Examples:

System administration: checking server status, retrieving logs, verifying backups, checking certificate expiry.

Monitoring: retrieving alerts, reading metrics, explaining system states.

DevOps: deployment status, CI/CD pipelines, container status, Kubernetes information, Git repositories.

Enterprise: internal knowledge bases, CRM, ticketing systems, document management, your own REST APIs.

Development: build systems, test environments, repositories, issue trackers, documentation.

The benefit is that the actual integration work gets standardized on the MCP side.

Checklist: building your own MCP server

Before your server goes into production, check at least the following:

  • MCP server's purpose is clearly defined
  • tools are as small and specific as possible
  • inputs are validated
  • allowlist instead of unrestricted inputs
  • no free-form shell
  • no unnecessary root access
  • read and write functions are separated
  • secrets live outside the source code
  • sensitive output is filtered
  • error handling is in place
  • timeouts are set
  • logging is set up
  • auditing has been considered
  • the MCP Inspector has been used
  • remote access is authenticated
  • HTTPS is used for remote MCP
  • tools have been tested individually
  • permissions are documented

FAQ: building your own MCP server

What do I need to build my own MCP server?

For a simple Python MCP server you need Python 3.10 or newer, the official MCP SDK, and a few lines of code. Production remote servers additionally require topics such as authentication, TLS, logging, and permission management.

Is an MCP server an AI?

No. An MCP server provides data and functions through the Model Context Protocol. The actual AI model normally lives in a separate application.

Which language is best suited for an MCP server?

Python and TypeScript are especially widespread. Official SDKs also exist for other languages such as C#, Go, Rust, and Java.

What is the difference between an MCP tool and a resource?

A tool executes a function. A resource provides information or content. A prompt, by contrast, delivers a prepared prompt structure for a specific use case.

Can an MCP server run shell commands?

Technically yes. A general-purpose shell over MCP is a significant security risk, however. Narrowly scoped tools with fixed actions and validated parameters are a better approach.

Can I run an MCP server over the internet?

Yes. Streamable HTTP is available for remote MCP servers. A production remote server should nonetheless run with HTTPS, authentication, authorization, logging, and the most restrictive permissions possible.

Does an MCP server need root privileges?

Usually not. An MCP server should always run with the fewest privileges its tools actually need.

How do I test an MCP server?

The official MCP SDK ships with development tools. A Python server can, for example, be tested with the MCP Inspector before it is connected to a production AI client.

Conclusion: building your own MCP server is easier than you think

Building your own MCP server is technically surprisingly simple. With Python and the official MCP SDK, a handful of functions are enough to expose your own tools, resources, and prompts.

The real challenge does not start with the code, though. It starts with the question: what capabilities should an AI actually be given?

Our practical example could easily have exposed a full shell instead. We implemented exactly one clearly defined capability instead: checking the status of approved Linux services.

That mindset is what matters for production MCP systems. A good MCP server is therefore not the one with the most tools and the broadest permissions. A good MCP server provides exactly the functions a specific use case needs – and no unnecessary access beyond that.

Anyone who applies this principle can build very capable integrations for Linux, DevOps, monitoring, internal APIs, enterprise applications, and AI agents with MCP.

Sources and further reading

The technical claims in this article are based in particular on the current MCP specification from July 28, 2026, the official SDK overview, and the documentation of the official MCP Python SDK. The current SDK supports STDIO and Streamable HTTP, among other transports, with Streamable HTTP recommended for new remote implementations.

Technical review as of: September 9, 2026.

Further reading and sources

How to Operate MCP Servers Securely: Permissions, Tools, and Risks Explained (Read article)

Model Context Protocol (MCP) Explained Simply (Read article)

How to Use OpenAI Codex Safely: Git, Sandbox, Backups, and Approvals (Read article)

AI Skills, Plugins, Apps, and MCP: What's the Difference? (Read article)