Skip to main content
Glama
muhammadwaqasmbd

mcp-server-template

README.md
# mcp-server-template

A production-shaped starting point for an MCP server.

The quickstart in the MCP docs gets you a working tool in ten lines. This is
what you end up adding over the following three weeks, once that tool is being
called by something you do not control.

```python
@mcp.tool()
def add(a: int, b: int) -> int:
    return a + b          # fine on a laptop
```

What is missing there is not features. It is **what happens when the tool hangs,
raises, returns a novel, or gets called forty times at once** — and what the
model is allowed to see when it does.

---

## The problem this solves

An MCP tool's caller is a language model, which changes the engineering.

- A model **cannot read a stack trace**, but it will cheerfully repeat one to
  your user. So a leaked traceback is both useless and a disclosure.
- A model has **its own deadline**. A tool that hangs does not produce a slow
  answer; it produces a dead conversation.
- A model **cannot tell a truncated result from a complete one**. Silently
  overflowing its context does not raise an error — it degrades the answer, and
  you find out from a customer.
- A model will **retry** if you let it. So "not found" and "upstream is down"
  have to be different answers, or it will hammer a service over a record that
  was never there.

Every one of those is handled once, in one place, so that a tool added on a
Friday afternoon inherits the same protections as the one written carefully on
day one.

---

## What you get

| | |
|---|---|
| **Per-tool timeout** | Real cancellation, not a warning afterwards. Returns a `retryable` error the model can act on |
| **Concurrency ceiling** | Bounded parallel execution, so a burst cannot stampede whatever your tools call |
| **Error boundary** | Declared errors reach the caller; unexpected ones become `internal_error` with no detail, and the traceback goes to the log |
| **Secret redaction** | Applied to logs *and* outbound messages, because keys escape through interpolated exception strings more often than through code |
| **Visible truncation** | Oversized results are cut with a marker, never silently |
| **Correlation ids** | One id per call, in the log and in the error the user can quote back to you |
| **Structured logs on stderr** | stdout belongs to the protocol — a stray `print()` corrupts the stream |
| **Fail-fast config** | Bad settings stop the server at boot, not on the first request |
| **Offline tests** | The suite runs on a train. No live keys, no network |

---

## Quickstart

```bash
git clone https://github.com/muhammadwaqasmbd/mcp-server-template
cd mcp-server-template
make install
make test
make run          # stdio, ready for a desktop MCP client
```

Serve it over the network instead:

```bash
TRANSPORT=streamable-http PORT=8000 python -m mcp_server_template
```

### Point a desktop client at it

```json
{
  "mcpServers": {
    "template": {
      "command": "python",
      "args": ["-m", "mcp_server_template"],
      "cwd": "/absolute/path/to/mcp-server-template"
    }
  }
}
```

---

## Adding your own tool

Write the function. Nothing else.

```python
# src/mcp_server_template/tools/orders.py
from ..errors import InvalidInput, UpstreamUnavailable

async def cancel_order(order_id: str) -> dict:
    """Cancel an order. Returns the order's new state."""
    if not order_id.strip():
        raise InvalidInput("order_id must not be empty")     # model can fix this
    ...
    raise UpstreamUnavailable("order service timed out")     # model may retry
```

Register it behind the guard:

```python
mcp.tool(name="cancel_order", description="Cancel an order by id.")(
    guard.wrap(orders.cancel_order)
)
```

It now has the timeout, the ceiling, the error boundary, the truncation and the
logging. You wrote none of that.

**Raise `InvalidInput` when the model can fix it. Raise `UpstreamUnavailable`
when retrying might work. Return normally for outcomes that are simply false** —
a missing record is an answer, not a failure.

---

## Architecture

```
server.py      the ONLY module that imports the MCP SDK
   │
   ├── guard.py          timeout · concurrency · error boundary · truncation · timing
   ├── errors.py         what a model is allowed to see, and secret redaction
   ├── observability.py  JSON logs on stderr, correlation ids
   ├── config.py         validated once at boot, immutable thereafter
   └── tools/            plain functions. No protocol knowledge. No decorators
```

The dependency arrow points one way: tools know nothing about MCP, and the guard
knows nothing about your tools. That is why the tests run in milliseconds
without a server, and why an SDK change touches exactly one file.

---

## What this deliberately does not do

Being honest about the edges is more useful than a longer feature list.

- **No authentication.** Over stdio the OS boundary is the security boundary. If
  you expose it over HTTP, put real auth in front — the SDK supports it, and
  wiring it here would imply a threat model you have not chosen yet.
- **No retry logic inside tools.** The guard reports whether a failure is
  retryable; deciding to retry belongs to the caller, which has the context and
  the budget.
- **No rate limiting per caller.** The concurrency ceiling bounds total work, not
  per-identity fairness. If you need that, you need identity first.
- **No persistence, queue or scheduler.** A tool server that quietly became a job
  runner is a distributed system nobody designed.
- **No streaming partial results.** Worth adding for long-running tools; left out
  because it complicates the error boundary and most tools do not need it.

---

## Testing

```bash
make test
```

The suite is deliberately about failure, not coverage. It asserts that a hung
tool is cancelled, that an unexpected exception cannot leak its message, that
oversized output is truncated visibly, that the concurrency ceiling holds under
ten simultaneous calls, and that a blocking sync tool does not starve the event
loop.

---

## Licence

MIT — see [LICENSE](LICENSE).

Built by [Muhammad Waqas](https://muhammadwaqas.pages.dev/), who spends most of
his time on agent systems in regulated industries, where a confident wrong
answer is a reportable incident.

TDQS

A4/5.0

Scored across 3 tools

Disambiguation5/5

Each tool addresses a separate concern: one computes string lengths, one looks up records, and one simulates a timeout. There is no overlap or realistic chance of choosing the wrong tool.

Naming Consistency4/5

Two tools follow a clear snake_case verb_noun pattern (summarise_lengths, fetch_record), but slow_operation reads as adjective_noun. The naming is still readable and predictable overall.

Tool Count5/5

Three tools is a reasonable size for a template server. Each tool exists to demonstrate a distinct capability, so none feel redundant or excessive.

Completeness5/5

As a template, the set covers the demonstrated scenarios: a utility operation, a record lookup with a defined absent-case, and a timeout example. There are no obvious dead ends or missing pieces for that purpose.

Maintenance

ActivityMaintained
ResponsivenessNo issues