# What is an MCP server? Three I run in production

> What an MCP server is, how the Model Context Protocol differs from a REST API, and what three MCP servers I run in production with Claude taught me.

2026-10-15 · Gustavo Karsten

An MCP server is a program that exposes tools and data to an AI client like Claude, in a format the model can figure out on its own. I run three MCP servers in production: one inside a SaaS for contractors, one over Massachusetts building permits, and one over official auctions in Brazil. This post explains what MCP is and what each of those servers taught me.

## What is MCP (Model Context Protocol)?

MCP stands for Model Context Protocol. It's an open protocol, created by Anthropic, that defines how an AI client discovers and calls capabilities in an external system. The client can be Claude Desktop, Claude Code, Codex, Cursor or your own agent.

An MCP server offers three kinds of things:

- **Tools:** functions the model can call, like `search_permits` or `create_quote`.
- **Resources:** documents the client can read, like a dataset guide.
- **Prompts:** reusable instruction templates.

The transport is either stdio (a local process) or HTTP. All three of my servers use stateless Streamable HTTP: each request builds a server and a transport, answers, and closes.

## An MCP server is not just a REST API with a new name

Under the hood, an MCP server often calls the same code as an API. The difference is who consumes it.

A REST API is read by a developer. They read the docs, write the code and decide when to call each route. With an MCP server, the model decides, at runtime. That changes three things:

1. **The tool description becomes an instruction.** The model reads each tool's text to choose which one to use and how. A vague description leads to the wrong call.
2. **The input schema is the contract.** Each tool declares its parameters (JSON Schema, which I write in Zod or with Python type hints). The client validates them and the model knows what to send.
3. **The client is swappable.** The same server works with Claude, Codex or any client that speaks the protocol. I don't write one integration per client.

The protocol also has annotations like `readOnlyHint` and `destructiveHint`. Clients use them to decide what can run without asking for permission.

## LedgerJob: one tool list for the agent and the MCP server

[LedgerJob](https://gustavokarsten.com/en/projects/ledgerjob) is a multi-tenant SaaS for US construction contractors. Owners run leads, quotes, projects and invoices by chatting with an agent inside the app. The same 66 tools that agent uses are exposed through an MCP server.

The key point is that there's only one list. The MCP server has no tools of its own: it maps the agent's list. A new tool ships to both surfaces in the same deploy, with no duplicated code.

```ts
export const mcpToolList = assistantTools.map((t) => ({
  name: t.name,
  description: t.requiresConfirmation
    ? `IRREVERSIBLE: sends a message, spends money or permanently changes data. Ask the user before calling. ${t.description}`
    : t.description,
  inputSchema: t.parameters,
  annotations: {
    title: t.label,
    readOnlyHint: Boolean(t.readOnly),
    destructiveHint: Boolean(t.requiresConfirmation),
  },
}));
```

Inside the app, an irreversible action (sending email or SMS, spending money with third parties, deleting data) never runs inside the agent loop. It becomes a card the user confirms. Over MCP there is no card. A comment in the code says why: there, human approval is the client's job, like Claude Code's permission prompt. So those tools carry an `IRREVERSIBLE` prefix in the description and the `destructiveHint` annotation.

Identity comes from the API key, never from an argument the model fills in. Each user creates their own key, which is stored only as a SHA-256 hash and carries the tenant, user and role. The MCP server acts as that user, with the same permissions, rate limiting and plan checks.

That has a consequence: the agent and the MCP server skip the HTTP route validators. So business rules live in the service layer. A real bug, where the assistant could book a 6 pm to 5 pm appointment, moved that rule into the service, with a test.

The server also sends general instructions the client reads before picking a tool: how a lead becomes a client, that an approved quote becomes a project, and "search before acting."

## Cape Cod Permits: read-only, audited, scoped by key

[Cape Cod Permits](https://gustavokarsten.com/en/projects/cape-cod-permits) pulls Massachusetts building permits, scattered across six municipal systems, into one PostgreSQL database. Its MCP server has 7 tools, all read-only, plus a dataset guide.

Read-only is declared on every tool:

```js
const READ_ONLY = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
```

Each MCP key has its own name. That name shows up in the audit log, and every read of personal contact data is logged with the filters used. Each key also has its own per-minute request limit. A comment in the code explains the choice: an agent has no human pacing, and if you ask it for "every owner" it will paginate until the pages run out. The limit is per credential, not per IP, because the budget belongs to whoever received the key.

## Guiding the model with tool descriptions

Public city data has traps. Each town covers a different date range, permit types and statuses use different vocabulary from town to town, and project cost is self-reported. A model with no context will compare two towns and conclude one had less construction, when its data actually stops in July.

So the descriptions do the work of a careful analyst:

- `list_facets` says to call it before comparing towns, because it returns the date range each town covers.
- `permit_stats` says to answer "how many" and "how much" with aggregates instead of paging through `search_permits`, to prefer the median over the mean, and to check how many records had a cost before quoting a total.
- `get_permit` says to use it to dig into one case, "never in a loop over a list."

The dataset guide ships as an MCP resource and also as a tool (`dataset_docs`), because not every client reads resources.

On [Achado de Leilão](https://gustavokarsten.com/en/projects/achado-de-leilao), a single search across official Brazilian auctions, the MCP server has 7 tools: lot search, lot detail, upcoming auctions, summary, sale results, lot history and SQL. The SQL tool is the most dangerous and the most useful. Its protection doesn't depend on the model: the database session opens read-only and with a timeout.

```python
def _query(sql: str, params=()) -> list[dict]:
    # read-only session + timeout: not even the SQL tool can write or lock the database
    opts = "-c default_transaction_read_only=on -c statement_timeout=15000"
    with psycopg.connect(DATABASE_URL, options=opts, row_factory=dict_row) as conn:
        return [_json(r) for r in conn.execute(sql, params or None).fetchmany(MAX_ROWS)]
```

Results are capped at 200 rows, and the token is checked in constant time. The tool description lists the view's columns, explains that a lot with `dup_of` set is a property already counted under another lot, and asks the model to include the link so the user can open the listing.

## Checklist for your first MCP server

1. **Start from what exists.** If you already have an agent or a service layer, expose that same list. Don't write a second set of tools.
2. **Write descriptions for the model.** Say when to use a tool, when not to, and which tool to prefer for each kind of question.
3. **Send server instructions.** One paragraph about the domain keeps the model from guessing how the data fits together.
4. **Identity comes from the credential.** Tenant and user are never parameters the model fills in. Store keys as hashes.
5. **Mark what's destructive.** Use `readOnlyHint` and `destructiveHint`, and say in the description what needs approval.
6. **Enforce limits in the database, not the prompt.** A read-only session, a timeout and a row cap hold even when the model gets it wrong.
7. **Rate-limit and log per key.** Per-credential limits, plus an audit log for sensitive data.
8. **Trim large responses.** Return a total, paginate, and truncate with a hint to filter.

I build these servers with Claude Code. If you want an MCP server over your own system's data, [get in touch](https://gustavokarsten.com/en/contact).

---
Page: https://gustavokarsten.com/en/articles/o-que-e-servidor-mcp
Contact: gustavo.karsten@outlook.com · https://www.linkedin.com/in/gustavo-karsten-88a04383/ · https://github.com/gustavo-karsten
