# MCP servers worth wiring into a Python project

> Source: https://learn-python.com/ai/mcp/
> Part of Learn Python, free to read.

The Model Context Protocol is a standard way for an agent to call tools it did not ship with — read your database schema, query your error tracker, fetch current library documentation. It solved a real problem: before it, every tool integration was bespoke to one client.

The failure mode now is the opposite one. Installing ten servers puts several hundred tool definitions into every request, which crowds out your code, slows every turn and measurably degrades tool selection. **Each server should have to justify itself.**

:::verdict The rule
Install a server when it gives the agent information it genuinely cannot get from your repo or your shell. If the agent could get the same answer by running a command you have already allowlisted, the command is better — it costs no context until it is used.
:::

## The ones that earn their place

**Documentation retrieval.** The single highest-value category for Python, because library APIs move faster than training data. A model confidently writing the 2023 signature of a fast-moving library is a recurring, expensive failure; a docs server that fetches the version you actually have installed removes it.

**Your database, read-only.** Letting an agent inspect the real schema — column types, nullability, indexes, foreign keys — rather than inferring it from your models eliminates an entire class of "wrote a query against a column that does not exist" errors. Read-only, against a non-production database.

**Error tracking.** "Here is the stack trace and the last twenty occurrences" turns a debugging session from a description of a bug into the bug itself. This is where MCP is at its most obviously useful.

**Issue tracker.** Worth it if your issues are actually written; not worth it if your issues are two-line reminders to yourself.

## The ones to think twice about

**Filesystem servers.** Your agent already has file access. This duplicates it with a worse interface and a large tool surface.

**Git servers.** Same argument. `git` is a CLI, the agent has a shell, and the CLI has better documentation than any wrapper.

**Anything with write access to production.** The blast radius is not worth the convenience, and it will eventually be exercised by a prompt-injection payload in a page the agent read.

**Aggregator servers exposing fifty tools.** These are the worst offenders for context bloat. If you only need two of the fifty, the other forty-eight are pure tax.

## Configuration

```json .mcp.json
{
  "mcpServers": {
    "docs": {
      "command": "uvx",
      "args": ["some-docs-mcp-server@latest"]
    },
    "postgres-dev": {
      "command": "uvx",
      "args": ["some-postgres-mcp-server", "--read-only"],
      "env": { "DATABASE_URL": "postgresql://localhost/myapp_dev" }
    }
  }
}
```

Commit `.mcp.json` so the team shares a configuration, and keep credentials in the environment rather than in the file. A `DATABASE_URL` in a committed config is a credential in your git history.

:::danger Prompt injection is the real risk here
An MCP server returns text, and that text enters the model's context as data it may act on. A server that fetches web pages, reads issues, or returns error-tracker payloads is returning **content written by other people** — including, potentially, instructions aimed at your agent.

Treat every MCP result as untrusted input:
- Prefer read-only servers.
- Never give a server write access to something you would not let a stranger write to.
- Be suspicious of servers that fetch arbitrary URLs and also have write tools available in the same session. That combination is the whole attack.
:::

## Writing one in Python

Worth doing when you have an internal system the agent keeps needing — a feature-flag service, a staging deploy API, an internal search index. It is much less work than people expect.

```python mcp_server.py
from mcp.server.fastmcp import FastMCP
import httpx

mcp = FastMCP("acme-internal")


@mcp.tool()
async def find_customer(email: str) -> dict:
    """Look up a customer by email in the staging environment.

    Returns id, plan, created_at and feature flags. Staging data only —
    never production. Use this instead of guessing at fixture data.
    """
    async with httpx.AsyncClient() as client:
        r = await client.get(f"{STAGING}/internal/customers", params={"email": email})
        r.raise_for_status()
        return r.json()


@mcp.resource("schema://tables")
def table_list() -> str:
    """The current staging database schema, as CREATE TABLE statements."""
    return introspect_schema()


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

Three things matter more than the code:

1. **The docstring is the interface.** It is what the model reads to decide whether and how to call the tool. Write it for a competent colleague who has never seen your systems — say what it returns, what it does *not* cover, and when not to use it.
2. **Return structured data, not prose.** A dict the model can index beats a sentence it has to parse.
3. **Keep the tool count small.** Four well-described tools beat twenty thin ones, both for selection accuracy and for context cost.

## Measuring the cost

Before adding a server, look at what it actually adds. In Claude Code, `/context` shows the breakdown; most clients have an equivalent. If a server is consuming a meaningful share of your window and you have used it twice this month, remove it — you can add it back for the session where you need it.

:::promo digitalocean
:::

## The shape of a good setup

For a typical Python service, this is usually enough:

```text
docs server        -> current library APIs
postgres (ro, dev) -> real schema
error tracker      -> real stack traces
```

Three servers, each answering a question the repo cannot. Everything else the agent can get with a shell command you have already allowlisted, at zero standing cost.

## Common questions

### Do I need MCP at all?

No. A well-configured agent with shell access, a fast test suite and a good `AGENTS.md` is already most of the value. MCP is worth adding when you notice the agent repeatedly guessing at something that exists in a system it cannot see — a schema, a stack trace, a current API signature.

### Is it safe to connect an agent to my database?

To a development or staging database, read-only, yes. To production, no — and not because the agent is malicious, but because an agent's actions can be influenced by any text that reaches its context, including text written by other people. Read-only against non-production data keeps the worst case boring.

### Why does adding servers make the agent worse at choosing tools?

Because tool selection is a discrimination problem, and every extra tool definition is another near-neighbour to confuse with the right one. It is the same reason a 500-line instructions file works worse than a 50-line one: attention is finite and dilution is real.

### Can I write an MCP server without publishing it?

Yes, and most useful ones are never published. A local script referenced by path in `.mcp.json` works exactly the same as a published package, and internal systems are precisely where the value is highest.
