Writing your own MCP server
The Model Context Protocol lets any compatible app use your tools. Learn what a server exposes, how it is structured, and how to keep it to least privilege.
In 60 seconds
Writing your own MCP server
The Model Context Protocol lets any compatible app use your tools. Learn what a server exposes, how it is structured, and how to keep it to least privilege.
What a server can expose
| Primitive | What it is | Who usually decides to use it | Example |
|---|---|---|---|
| Tools | Functions the model can call, with a name, description and input schema. | The model, inside the app's rules. | get_bus_times(stop_id) |
| Resources | Data the app can read and add to context, identified by a URI. | The app or the user. | A file, a database record, a policy document. |
| Prompts | Reusable prompt templates the server offers. | The user, often from a menu. | "Summarise this ticket for a manager". |
The structure of a small server
- 1
Create the server
Give it a name. The SDK handles the protocol messages for you. - 2
Register tools
Each tool: a function, a clear description and typed parameters. The SDK builds the schema. - 3
Register resources and prompts
Only if you need them. Many servers start with tools only. - 4
Run it
Start it over stdio for a local app, or HTTP for a remote one. - 5
Connect a client
Add the server to an MCP-capable app's settings and test each tool.
# Illustrative sketch. Install and API details: see https://modelcontextprotocol.io/
from mcp.server.fastmcp import FastMCP # name may differ in your SDK version
server = FastMCP("city-bus-times")
TIMES = {"STOP-101": ["08:05", "08:25", "08:45"], "STOP-202": ["09:10", "09:40"]}
@server.tool()
def get_bus_times(stop_id: str) -> str:
"""Next departures for ONE bus stop. Use when the user gives a stop id
like STOP-101. Read-only. Returns up to 3 times, or a clear error."""
if stop_id not in TIMES:
return "No stop " + stop_id + ". Stop ids look like STOP-101."
return ", ".join(TIMES[stop_id][:3])
@server.resource("stops://list")
def list_stops() -> str:
"""All stop ids this server knows."""
return "\n".join(sorted(TIMES))
if __name__ == "__main__":
server.run() # stdio by default in many versions; check the docsLeast privilege for your server
- Expose only what is needed. Do not wrap your whole API. Start with a few narrow tools.
- Use the smallest credentials. If the server calls a database, give it a read-only account for read tools. Keep secrets in environment variables or a secrets manager, never in tool descriptions or results.
- Check every input. Arguments come from a model that may have read malicious text. Validate like any public API: types, ranges, allowed ids, and the user's permissions.
- Limit the file system and network. A file tool should only see one folder. A fetch tool should only reach allowed hosts.
- Return small, clean results. Don't return secrets, internal stack traces or other users' data.
- Version and review descriptions. Changing a tool description changes the model's behaviour. Treat it like a code change.
Watch and read more
Lab
A design (and optionally a working version) of a small read-only MCP server with least-privilege notes.
The problem
Design an MCP server for a college notice board. Do the paper part first; the coding part is optional.
- List two tools and one resource. For each tool: name, description with "use when", and typed parameters. Both tools must be read-only.
- Write a least-privilege table: what data the server can reach, with which credential, and what it can not do.
- Write three input checks and three error messages the model can act on.
- Optional: follow the Python quickstart on modelcontextprotocol.io to build it with fake data, and test it with the project's inspector tool or an MCP-capable app.
Server name: college-notices
Tool 1: name | description (use when / do not use for) | parameters | read or write
Tool 2: ...
Resource: uri | what it returns
Least privilege:
data it can reach:
credential used:
can NOT do:
Input checks:
Error messages:You are done when
Hard questions
Try to answer before you reveal. If you can answer these, you understood the lesson.
Q1Later, the college wants a <code>post_notice</code> write tool. What would you add before shipping it?Reveal
A separate, narrower credential that can only create notices; a confirmation step shown to a person with the exact text before posting; checks on length and content; an idempotency key so retries do not post twice; a log of who asked and what was posted; and an easy way to remove a notice. Consider putting the write tool in a separate server so read-only users never load it.
Q2Why is changing a tool's description a security-relevant change, even if the code does not change?Reveal
The model decides what to do partly from descriptions. A changed description can make the model call a tool in new situations, pass different data, or follow hidden instructions. That is how "rug pull" attacks work with third-party servers. So descriptions should be reviewed, versioned and, for third-party servers, pinned.
Questions people ask
Do I need MCP to give tools to my own agent?
No. If one agent in one program uses the tools, plain function calling in your own loop is simpler. MCP helps when you want the same tools to work in several AI apps, or want others to use your tools without writing custom integration code.
What is the difference between a tool and a resource?
A tool is an action the model chooses to call, with arguments. A resource is data that the app or user chooses to add to the context, identified by a URI. If the model should decide when to fetch something, make it a tool.
How do I test an MCP server without an AI app?
The MCP project provides developer tools for this, such as an inspector that lets you connect to a server, list its tools and call them by hand. Check the official site for the current tools. Also write normal unit tests for the functions behind each tool.
Can a remote MCP server see my files?
Only what the client sends it. But a server's tool results and descriptions go into the model's context, so a malicious server can try to influence the model. Only connect servers you trust, and give each one the smallest access it needs.
Lesson test
5 questions. Get 3 right (60%) to pass and complete this lesson.