AI & Machine Learning3 min read628 words

How to Build an MCP Server: A Step-by-Step Guide

A working MCP server in five steps — choosing what to expose, defining tools the model can actually use, handling authorisation, testing against a host, and the mistakes that make a server unusable in practice.

JL

Jishu Labs

Most MCP servers fail for the same reason most APIs fail: the interface makes sense to the person who wrote it and not to the caller. The difference is that your caller here is a language model, which will not read your docs, will not ask for clarification, and will confidently invent an argument if your schema allows it.

1. Decide what to expose

Start with read-only tools that answer questions your team asks repeatedly. Resist exposing your ORM. A tool named `query_database` with a free-text SQL argument is a server that will eventually run something you did not intend.

  • Good: `get_deployment_status(service, environment)` — narrow, typed, obvious.
  • Bad: `run_query(sql)` — unbounded, unauditable, impossible to authorise meaningfully.
  • Good: `list_open_incidents(severity?)` — optional filter, bounded result set.
  • Bad: `admin_action(command)` — a shell by another name.

2. Define tools the model can use correctly

The tool description is a prompt. It is the only documentation the model gets, and vague descriptions produce wrong calls. Say what the tool returns, what the arguments mean, and when *not* to use it.

typescript
server.tool(
  'get_deployment_status',
  {
    service: z.string().describe('Service name exactly as it appears in the registry, e.g. "checkout-api".'),
    environment: z.enum(['staging', 'production']).describe('Which environment to check.'),
  },
  { description: 'Returns the currently deployed version and health of one service in one environment. Use for "what is deployed" questions. Does not deploy anything and cannot modify state.' },
  async ({ service, environment }) => { /* ... */ }
);

Enums beat free strings everywhere you can use them. Every unconstrained string argument is a place the model can hallucinate a value that your service then has to reject.

3. Put authorisation behind the tool, not in it

The host decides whether to call your tool. It does not know your permission model. Resolve the caller's identity in your service and apply the same authorisation you would apply to an HTTP request from that user. A tool that trusts its arguments is an open endpoint.

Tool descriptions are attacker-reachable text

Anything that lands in the model's context — a retrieved document, a ticket body, a filename — can attempt to instruct the model to call your tools. Assume every call is potentially adversarial: authorise it, rate-limit it, and log it with the resolved user identity, not the model's claim about who it is acting for.

4. Test against a real host

A server that passes unit tests can still be unusable, because usability here means the model picks the right tool with the right arguments. Test the behaviour, not just the function:

  • Ask the question three different ways and check the same tool is chosen.
  • Give a question your server cannot answer and confirm it declines rather than calling something adjacent.
  • Feed a deliberately ambiguous argument and check the failure is a clear error, not a silent wrong answer.
  • Run the whole suite again after any description change — descriptions are behaviour.

5. Ship it stateless if you can

The July 2026 specification revision moved MCP toward a stateless architecture specifically because session affinity makes horizontal scaling painful. If your server can answer each call independently, it can sit behind an ordinary load balancer like any other service.

Frequently Asked Questions

What language should I write an MCP server in?

Whichever your service already uses — official SDKs exist for TypeScript and Python among others. The protocol is transport-agnostic, so the deciding factor is proximity to the data you are exposing.

How many tools should one server expose?

Few enough that the model can choose correctly. Large flat tool lists degrade selection accuracy; if you have dozens, split by domain into separate servers.

Should tools ever write data?

Only behind an explicit human confirmation step for anything irreversible. Read-only servers are where to start, because the worst outcome is a wrong answer rather than a wrong write.

References

  1. Model Context Protocol — 2026-07-28 SpecificationModel Context Protocol
  2. The 2026 MCP RoadmapModel Context Protocol
JL

About Jishu Labs

Jishu Labs is a software development company founded in 2016. We build custom software, AI/ML systems, and full-stack web and mobile applications for clients, and we make eight AI tools for software teams.

Related Articles

AI & Machine Learning3 min read

Small Language Models vs Frontier Models: A Cost Framework

Serving a 7B model is roughly 10-30x cheaper than a frontier model for tasks where accuracy is equivalent. The engineering question is which tasks those are, and how to find out without guessing.

Jishu Labs

July 28, 2026

AI & Machine Learning3 min read

What Is an AI Memory Layer?

Chat history is not memory. A memory layer is durable, retrievable state about decisions, preferences and facts that survives past the context window. What belongs in one, and what should stay in a log.

Jishu Labs

July 27, 2026

AI & Machine Learning3 min read

RAG in 2026: When You Still Need It, When You Don't

Long context windows and better tool use took work away from retrieval-augmented generation. RAG did not become obsolete — its job got narrower. A decision framework for when to retrieve, when to load, and when to call a tool.

Jishu Labs

July 23, 2026

Ready to Build Your Next Project?

Let's discuss how our expert team can help bring your vision to life.

AI Tools,
Built
End-to-End

Ready to Get Started?

Get consistent results. Collaborate in real-time.
Build Intelligent Apps. Work with Jishu Labs.

SCHEDULE MY CALL