Skip to content
· 11 min read

MCP beyond hello-world: patterns for genuinely useful servers

Almost every MCP tutorial stops at an endpoint that returns the current time. The real work starts when the server has to deal with auth, pagination, and errors that are transparent to the agent.

Victor Dantas AI Committee · Bamse

Writing an MCP server that returns the current time takes fifteen minutes. It's the protocol's "hello-world": register a tool, return a string, the agent reads it. It works on the first try and is useless in production. MCP is just the plumbing - a standard way to expose tools, resources, and prompts to an agent over stdio or HTTP. The real work is everything the tutorial skips.

When an MCP server leaves the toy stage and touches real data, three problems show up in the same order, every time: who can call what, how much fits in the response, and what happens when it goes wrong. Auth, pagination, and errors. That's where the real design lives.

Auth: never trust the agent

A toy server runs over stdio on your machine and inherits your permissions. A remote server exposed over HTTP is an attack surface. The spec treats the MCP server as an OAuth 2.1 resource server: without a valid token, it answers 401 and tells you where the authorization server is. The client runs the flow, comes back with a token, and only then does the tool execute.

The thing most people get wrong is assuming the agent owns the authorization. It doesn't. The token has a scope, and the scope is per-request, not per-connection. An agent holding a read-only token shouldn't even see the write tool in tools/list - the tool list can vary by the permissions presented. The server validates everything again on its side: identity, scope, and authorization against the specific resource being touched. Trusting that the model "will behave" is not a security strategy.

"The agent is a client, not an identity. The server decides what it can do - on every call, against the token's scope."

Pagination: the context window is your budget

The most common failure I see in MCP servers is the tool that returns "all the records". On a three-row table, beautiful. In production, the payload blows the context window, costs a fortune in tokens, and buries the relevant answer among a thousand that don't matter. Every token you push is an attention token you spent.

The protocol already hands you the right tool: cursor-based pagination. The server returns a bounded page and an opaque nextCursor; the client sends the cursor back for the next page. The cursor is opaque on purpose - the client doesn't know, and shouldn't know, whether behind it sits an offset, an encoded token, or a database pointer. The server decides the page size, and that size should be reasoned about as a token budget, not as "however many rows the query returned".

In practice, this changes how you design the tool. Instead of list_orders dumping everything, you return the columns that matter, a handful of records, and make it explicit in the description that a cursor exists. The agent reads a few results, decides whether it needs more, and paginates on demand. The server controls the size of the response, not luck.

Errors: transparent, not opaque

Here's the difference between a server the agent can use and one it gets stuck on. MCP separates two kinds of error, and that separation is the detail nobody reads in the tutorial. A protocol error - unknown tool, malformed request - becomes a JSON-RPC error, and the model can almost never recover from it. A tool execution error - failed validation, external API down, business rule violated - comes back inside the result, with the isError: true flag and a message the model reads.

The practical rule is simple: if the agent has a chance to fix it, the error has to be readable. "Invalid departure date: must be in the future, today is 04/02/2026" is a message the model recovers from on its own. A raw 500 or a whole stack trace is noise it can't act on. An error message, in an MCP server, is interface - write it as if the reader were the next step in the agent's reasoning, because that's exactly what it is.

* Rule of thumb

Treat each tool as a slot in a function list the model reads before acting. A clear, descriptive name, a description that says when NOT to use it, and a response shaped for the model's attention - lean text to read, structuredContent when it needs a type.

A tool name is the first context the model sees. getUser says more than handler_3, and a description that spells out the limits saves a whole wrong call. A good MCP server isn't the one with the most tools - it's the one with the right tools, with auth that doesn't trust blindly, pagination that respects the budget, and errors the agent can read. Hello-world proves the protocol works. These three details prove the server is worth anything - and they're exactly what I keep exploring in the research MCP server I maintain.

Liked this piece?

I can take these and other notes to your team as a workshop or recurring mentorship through Bamse's AI Mentorship program.

Talk about mentorship
Next Writing

Harness engineering: why the agent's environment matters more than the model