Skip to main content
Glama
mohamedelamraoui1

mcp-shell-server-example

README.md
# mcp-shell-server-example

[![Follow on X](https://img.shields.io/badge/-Follow%20%40mcoding__off-black?style=flat&logo=x)](https://x.com/mcoding_off)
[![WhatsApp Channel](https://img.shields.io/badge/WhatsApp-Channel-25D366?style=flat&logo=whatsapp&logoColor=white)](https://whatsapp.com/channel/0029Vb7WRtT11ulGgJPp4m3y)
[![License](https://img.shields.io/badge/License-MIT-blue?style=flat)](LICENSE)

A small, educational [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server built with the [official Python SDK](https://github.com/modelcontextprotocol/python-sdk).

The goal of this repo is **not** to be a production tool — it's a minimal, readable reference for people learning MCP: how a server exposes **tools** and **resources**, how a client (like an AI agent) discovers and calls them, and how to run/test the whole thing locally or in Docker.

## What is MCP?

The [Model Context Protocol](https://modelcontextprotocol.io/) is an open standard that lets AI applications (like Claude) connect to external systems in a consistent way. An MCP **server** exposes capabilities — mainly:

- **Tools** — functions the AI can call (e.g. "run this shell command", "fetch this URL")
- **Resources** — data the AI can read (e.g. a file's contents)

An MCP **client** (built into an AI app, or a debugging tool like MCP Inspector) connects to the server, discovers what it offers, and calls it on the AI's behalf. Communication happens over a transport — this server uses **stdio** (standard input/output), the simplest option: the client launches the server as a subprocess and talks to it over its stdin/stdout using JSON-RPC.

## Architecture

```mermaid
flowchart LR
    subgraph Client
        A[MCP Client<br/>Claude Code / MCP Inspector]
    end

    subgraph Server["mcp-shell-server-example (stdio)"]
        B[FastMCP server.py]
        T1[terminal<br/>PowerShell]
        T2[terminal_linux<br/>sh]
        T3[benign_tool<br/>curl fetch]
        R1[mcpreadme<br/>resource]
    end

    A <-- "JSON-RPC over stdio" --> B
    B --> T1
    B --> T2
    B --> T3
    B --> R1
    T1 -.-> H[(Host OS)]
    T2 -.-> C[(Container OS)]
    T3 -.-> G[(Remote gist)]
```

The client launches `server.py` (directly with `uv`, or inside a Docker container) as a child process and exchanges MCP messages with it over stdio — no network port required.

## What this server exposes

| Name | Type | Description |
|---|---|---|
| `terminal` | Tool | Runs a command via **PowerShell** on the host. Meant for local/Windows use. |
| `terminal_linux` | Tool | Runs a command via **`/bin/sh`**. Meant for use inside the Docker container. |
| `benign_tool` | Tool | Downloads content from a fixed URL with `curl` and returns it. Included as a **deliberate demo** of a real MCP risk: a tool with an innocuous name that pulls in remote, untrusted content — a pattern known as *tool poisoning* / indirect prompt injection. Useful for learning to think critically about what MCP tools actually do, not just what they're named. |
| `mcpreadme` | Resource | Returns the contents of [`mcpreadme.md`](mcpreadme.md). |

> ⚠️ **`terminal` / `terminal_linux` run arbitrary shell commands with no sandboxing or allowlist.** That's intentional for a learning project, but treat this as a local playground, not something to expose to untrusted clients or the network.

## Prerequisites

- Python 3.12+
- [uv](https://docs.astral.sh/uv/) — used to manage the virtual environment and run the server
- (Optional) [Docker](https://www.docker.com/) — to run the server in a container
- (Optional) [Node.js](https://nodejs.org/) — needed to run [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) via `npx`

## Running locally

```bash
git clone https://github.com/mohamedelamraoui1/mcp-shell-server-example.git
cd mcp-shell-server-example
uv sync
uv run server.py
```

The server then waits on stdio for an MCP client to connect — this is normal, it won't print anything and won't respond to plain typed text (it only understands JSON-RPC).

## Running in Docker

```bash
docker build -t shell-server-app .
docker run -i --rm shell-server-app
```

Inside the container, use `terminal_linux` instead of `terminal` — PowerShell isn't installed in the (Debian-based) image.

## Testing with MCP Inspector

[MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) is a web UI for manually calling a server's tools/resources without needing a full AI client.

**Against the local server:**
```bash
uv run mcp dev server.py
```

**Against the Docker image:**
```bash
npx @modelcontextprotocol/inspector docker run -i --rm shell-server-app
```

Either command prints a local URL — open it, click **Connect**, then use the **Tools** tab to call `terminal` / `terminal_linux` / `benign_tool`, or the **Resources** tab to read `mcpreadme`.

Example: get "HELLO MCP" printed back via the `terminal` tool by passing this as the `command` argument:
```powershell
echo " _   _      _ _       __  __  ____ ____  "
echo "| | | | ___| | | ___  |  \/  |/ ___|  _ \ "
echo "| |_| |/ _ \ | |/ _ \ | |\/| | |   | |_) |"
echo "|  _  |  __/ | | (_) || |  | | |___|  __/ "
echo "|_| |_|\___|_|_|\___/ |_|  |_|\____|_|    "
```

## Connecting it to Claude Code (real-world usage)

This server was built and tested end-to-end using [Claude Code](https://claude.com/claude-code) as the MCP client — including scaffolding the server itself, containerizing it, and then wiring it back in as a live tool Claude Code could call. That's a good demonstration of the MCP loop in practice: an AI agent using a protocol-standard interface to run real commands and fetch real data, instead of a one-off custom integration.

To connect it yourself, add a `.mcp.json` file at your project root (Claude Code loads this automatically and asks you to approve it on startup):

```json
{
  "mcpServers": {
    "m-shell-server": {
      "command": "uv",
      "args": ["run", "--directory", "/absolute/path/to/mcp-shell-server-example", "server.py"]
    }
  }
}
```

Restart Claude Code in that directory, approve the server when prompted, then check it's connected with `/mcp`. From then on, Claude Code can call `terminal`, `terminal_linux`, `benign_tool`, and read the `mcpreadme` resource directly as part of answering your prompts.

## Project structure

```
.
├── server.py         # the MCP server: tools + resource definitions
├── mcpreadme.md       # content exposed via the mcpreadme resource
├── pyproject.toml     # project metadata + dependencies (managed by uv)
├── uv.lock            # locked dependency versions
├── Dockerfile          # container build, following uv's official Docker guide
├── .dockerignore
└── .mcp.json           # example Claude Code project-scoped MCP config
```

## References

- [Model Context Protocol](https://modelcontextprotocol.io/)
- [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk)
- [uv Docker integration guide](https://docs.astral.sh/uv/guides/integration/docker/#installing-uv)

TDQS

C2.7/5.0

Scored across 3 tools

Disambiguation4/5

Tools have distinct functions: benign_tool downloads via curl, terminal runs local commands, terminal_linux runs containerized commands. However, the two terminal tools share the same core purpose (shell execution) and could be confused without careful reading.

Naming Consistency3/5

Names use snake_case, but 'benign_tool' does not follow a verb_noun pattern like the others. 'terminal' and 'terminal_linux' are similar but 'terminal' is generic, breaking consistency.

Tool Count5/5

Three tools is appropriate for a shell server focused on command execution and file retrieval. No unnecessary bloat or insufficiency.

Completeness4/5

Covers core shell operations with local and containerized execution, plus URL downloads. Minor gap: no file management or process control tools, but essential workflows are supported.

Maintenance

ActivitySlowing
ResponsivenessNo issues