Writing an MCP Server Your Team's AI Assistant Can Use
What an MCP server is, how it differs from an API, and how to build one that gives your team's AI assistant safe, useful access to internal tools.
Full-Stack, Mobile & AI Engineer
Your team's AI assistant can write code, summarise documents and draft emails. Then someone asks it "did last night's Android build pass?" or "what plan is this customer on?" and it has no idea, because that knowledge lives in your build system, your admin panel and your internal docs. Copying and pasting between tabs works, but it turns a smart assistant into a slow intern.
An MCP server is the standard way to close that gap. This guide explains what an MCP server is in plain English, how it differs from an ordinary API, and how to write one that gives your team's assistants safe, useful access to internal tools, including the security rules in the current specification, which changed significantly with the 2026-07-28 revision.
Key takeaways
- An MCP server exposes tools, resources and prompts to AI applications through the Model Context Protocol, so any compatible assistant can use them.
- It's not a replacement for your API. It usually wraps your APIs in task-shaped tools a model can choose and call correctly.
- Tool design decides success: clear names, descriptions written for the model, tight input types and actionable errors.
- Local servers use the stdio transport and credentials from the environment; remote servers use OAuth-based authorization.
- Never pass tokens through to downstream APIs, start with read-only tools, and keep humans in the loop for actions.
What is an MCP server?
The Model Context Protocol (MCP) is an open protocol for connecting AI applications to the data and tools they need. It uses JSON-RPC messages between three parts:
- Hosts: the AI applications people use, such as a coding assistant, a chat app or an IDE.
- Clients: connectors inside the host, one per server.
- Servers: small services that offer capabilities to those clients.
An MCP server can offer three kinds of capability: tools (functions the model can call, such as "get build status"), resources (data the user or model can read, such as a runbook) and prompts (templates users can invoke). Write one server for your build system, and every MCP-compatible assistant your team uses can check builds, without a custom integration for each one. The specification itself compares MCP to the Language Server Protocol, which did the same thing for programming languages across editors.
MCP server vs API
| Your API | An MCP server | |
|---|---|---|
| Built for | Developers writing code | AI models choosing what to call |
| Discovery | Documentation | Self-describing: the client lists tools with descriptions and schemas |
| Shape | Resources and endpoints | Task-shaped tools ("find customer by email") |
| Auth | Your API's scheme | Credentials from the environment (local) or OAuth 2.1-based authorization (remote) |
| Consumers | Your apps and services | Any MCP-compatible assistant |
So why is an MCP server needed when you already have an API? Because a model doesn't read your API docs. It sees a list of tools, each with a name, a description and an input schema, and decides which to call. An MCP server is the layer that turns "GET /v2/builds?platform=android&limit=1" into a tool called get_latest_build with a description that says when to use it.
MCP server vs client, tools, agents and skills
The vocabulary gets muddled quickly, so here's how the pieces relate:
| Term | What it is |
|---|---|
| MCP server | A program that offers tools, resources and prompts over MCP |
| MCP client | The connector inside an AI application that talks to one server |
| Tool | One function a server exposes, such as get_latest_build |
| Agent | The AI application deciding which tools to call, and when |
| Skill | Packaged instructions for a workflow; the 2026 specification adds an optional extension for delivering skills over MCP |
An agent without MCP servers can only work with what's in its context. Servers are how it reaches the systems your team actually uses.
A minimal internal-tools server
Here's a small server in Python using the official SDK, exposing one read-only tool. The SDK turns type hints and the docstring into the tool's schema and description:
from mcp.server import MCPServer
mcp = MCPServer("ops-tools")
@mcp.tool()
async def get_latest_build(app: str, platform: str = "android") -> str:
"""Get the latest CI build for one of our apps.
Use this when someone asks whether a build passed, failed or is running.
Read-only. For release status in the stores, use get_store_status instead.
Args:
app: App slug, for example "member-app" or "staff-app"
platform: "ios" or "android"
"""
build = await builds.latest(app=app, platform=platform) # your internal API client
if build is None:
return f'No builds found for "{app}" on {platform}. Check the app slug.'
return f"{app} {platform}: {build.status}, version {build.version}, finished {build.finished_at}"
if __name__ == "__main__":
mcp.run(transport="stdio")Point your assistant's MCP configuration at this script and it can answer "did last night's Android build pass?" by itself. The official SDKs for TypeScript, Python and other languages follow the same pattern: create a server, register tools with typed inputs, connect a transport.
The same server in TypeScript
If your internal tools live in a Node.js codebase, the TypeScript SDK follows the same shape. Version 2 of the SDK ships the server as its own package (npm install @modelcontextprotocol/server zod):
import { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import { z } from "zod";
const server = new McpServer({ name: "ops-tools", version: "1.0.0" });
server.registerTool(
"get_latest_build",
{
description:
"Get the latest CI build for one of our apps. Use this when someone asks " +
"whether a build passed, failed or is running. Read-only.",
inputSchema: z.object({
app: z.string().describe('App slug, for example "member-app" or "staff-app"'),
platform: z.enum(["ios", "android"]).default("android"),
}),
},
async ({ app, platform }) => {
const build = await builds.latest({ app, platform }); // your internal API client
if (!build) {
return {
isError: true,
content: [{ type: "text", text: `No builds found for "${app}" on ${platform}. Check the app slug.` }],
};
}
return {
content: [{ type: "text", text: `${app} ${platform}: ${build.status}, version ${build.version}` }],
};
},
);
await server.connect(new StdioServerTransport());Notice the error path. The specification separates protocol errors (an unknown tool, a malformed request) from tool execution errors, which are returned as a normal result with isError: true. The second kind is the one a model can act on: a clear message lets it fix its arguments and try again, while a thrown exception just ends the conversation turn.
What the model actually sees
When a client connects, it calls tools/list, and your server answers with each tool's name, description and input schema. For the tool above, the model receives roughly this:
{
"name": "get_latest_build",
"description": "Get the latest CI build for one of our apps. Use this when someone asks whether a build passed, failed or is running. Read-only.",
"inputSchema": {
"type": "object",
"properties": {
"app": { "type": "string", "description": "App slug, for example \"member-app\" or \"staff-app\"" },
"platform": { "type": "string", "enum": ["ios", "android"], "default": "android" }
},
"required": ["app"]
}
}That JSON is the model's entire view of your tool. It never sees your source code, your API documentation or your database. Everything it knows about when to call get_latest_build, and with which arguments, comes from those few lines, which is why the next section matters more than the code.
Connecting it to Claude Code, Cursor and VS Code
The same stdio server works in every major client; only the configuration differs.
Claude Code registers servers from the command line. Everything after -- is the command that starts your server:
claude mcp add --transport stdio ops-tools -- python /path/to/ops_tools.py
claude mcp list # shows Connected, Needs authentication or Failed to connectCursor reads .cursor/mcp.json in the project (or ~/.cursor/mcp.json for every project):
{
"mcpServers": {
"ops-tools": { "command": "python", "args": ["ops_tools.py"] }
}
}VS Code reads .vscode/mcp.json, with a servers key instead:
{
"servers": {
"ops-tools": { "type": "stdio", "command": "python", "args": ["ops_tools.py"] }
}
}Commit the project-level files so the whole team gets the same tools, and keep credentials in environment variables rather than in the file.
Tool design that assistants get right
The model never sees your code, only each tool's name, description and schema. That contract is where the real work is:
- Name tools by what they do, in verb and noun form:
get_latest_build,find_customer,create_ticket. Avoid vague names likequeryor internal jargon. - Write descriptions for the model: what the tool does, when to use it, what it returns, and when to use a different tool instead.
- Type inputs tightly: enums for fixed values, bounds on numbers, and an example in every field description.
- Keep tools focused. Four small tools beat one
manage_customertool with amodeargument. - Return small, structured results. A wall of JSON burns the model's context and buries the useful fields.
- Make errors actionable. "No builds found for 'membr-app'. Check the app slug." lets the model correct itself; a stack trace doesn't.
When a tool gets misused, fix the description or schema first. Most "the AI called the wrong thing" problems are contract problems.
Security: internal doesn't mean safe
The current MCP specification is explicit about security, and internal tools are exactly where shortcuts get taken.
- Local servers: for servers that run on a developer's machine, use the stdio transport, which limits access to the client that launched it. The authorization specification says stdio implementations should not use its OAuth flow and should "instead retrieve credentials from the environment", so the server acts with that user's own credentials.
- Remote servers: servers shared over HTTP should implement the specification's authorization, which is based on OAuth 2.1. The server acts as a resource server and must validate that tokens were issued for it.
- No token passthrough. The specification states that MCP servers "MUST NOT accept or transit any other tokens". Your server gets its own credentials for downstream APIs, with only the permissions it needs.
- Least privilege: start with read-only tools, scope each server to one job, and request minimal scopes.
- Humans approve actions. The specification says hosts must obtain explicit user consent before invoking any tool. Design write tools (create ticket, refund order) to be clear about consequences, and keep destructive actions out of early versions.
- Treat tool output as untrusted. A ticket description or customer note can contain text written to manipulate a model (prompt injection). Return data, never instructions, and keep sensitive actions behind confirmation.
- Log every call with who, which tool, which arguments and the result, so you can audit and improve.
Is an MCP server without auth ever fine? For a local stdio server running as the user with their own credentials, yes, that's the intended model. For anything reachable over a network that exposes internal data, no.
When an MCP server isn't working
Most "MCP server not showing up" problems come down to a handful of causes:
- Check the status first. In Claude Code,
claude mcp listshows whether each server connected, andclaude mcp get <name>prints the failure reason. Cursor and VS Code show server status in their MCP settings. - Never print to stdout in a stdio server. Stdout carries the protocol's JSON-RPC messages, so a stray
print()orconsole.log()corrupts them. Log to stderr instead. - Use commands the client can find. The client starts your server itself, often without your shell's PATH; use full paths to the interpreter and script if in doubt.
- Pass the environment explicitly. API keys your terminal has may not exist when the client launches the server; set them in the client's config or with
--env. - Restart or reload the client after editing its configuration.
- Check versions. The 2026-07-28 specification changed the base protocol, so an old client and a new server (or the reverse) may not connect until the SDKs match.
What changed in 2026
The 2026-07-28 revision of the specification is the largest so far. The base protocol is now stateless, with self-contained requests and per-request capability negotiation, and servers that need state across calls pass an explicit handle as a tool argument, which the specification says must never be treated as authentication. Optional extensions add long-running Tasks, Skills over MCP and MCP Apps, which render interactive UI inside conversations. If you wrote a server against an older revision, check your SDK's migration notes before upgrading.
From the field: I build MCP servers that wire internal tools into AI assistants, and I use Claude Code and Cursor daily for review, refactors and tests, with every change reviewed by a person before merge. The servers worth starting with are the boring ones: a handful of read-only tools that answer questions people already ask in chat.
What an MCP server costs
The protocol and the official SDKs are free and open source, so a server costs what it takes to build and run: engineering time, hosting if it's a remote server, and the AI usage of whichever client calls it. Vendor-hosted servers, such as those from GitHub, Sentry, Notion, Atlassian (for Jira and Confluence) or Expo, come with the pricing of the product behind them. For internal tools, a small server that wraps APIs you already have is usually a few days of work, plus the security review.
Rolling it out to a team
- List the questions people ask in chat that require opening another tool. Those are your first tools.
- Start with three to five read-only tools in one server, and ship them to a few people.
- Read the logs. Failed or wrong calls show you which descriptions and schemas to fix.
- Distribute configuration through your internal docs or a shared config, so everyone connects the same way.
- Add write tools slowly, each with clear confirmation and a full audit trail.
FAQ
What is an MCP server?
An MCP server is a small service that exposes tools, resources and prompts to AI applications using the Model Context Protocol, an open standard. Any MCP-compatible assistant can discover its tools, read their descriptions and schemas, and call them, without a custom integration for each assistant.
What's the difference between an MCP server and an API?
An API is built for developers writing code against documented endpoints. An MCP server is built for AI models: it describes task-shaped tools in a standard format so a model can decide which to call. MCP servers usually wrap existing APIs rather than replacing them.
Do MCP servers need authentication?
Local servers using the stdio transport take credentials from the environment and act as the user who runs them. Remote servers over HTTP should implement the specification's OAuth 2.1-based authorization, validate that tokens were issued for them and never pass client tokens through to other APIs.
Which language should I use to write an MCP server?
Whichever your team maintains best. Official SDKs exist for TypeScript, Python and several other languages, and they follow the same pattern: create a server, register tools with typed inputs and descriptions, and connect a transport.
Next steps
If you want your team's AI assistants connected to your internal tools safely, I can design and build MCP servers with the right tools, permissions and audit trail. Get in touch, or see my services.
Sources
- Model Context Protocol specification, MCP
- Authorization, MCP specification
- Security best practices, MCP specification
- Build an MCP server, MCP documentation
- Python SDK, GitHub
- Connect Claude Code to tools via MCP, Anthropic
- Model Context Protocol in Cursor, Cursor
- MCP servers in VS Code, Visual Studio Code
Related
How to Evaluate LLM Features with DeepEval Before Shipping
Treat prompts like code. Use DeepEval to write test cases, score faithfulness, relevancy, and hallucination, and run LLM regression tests in CI before you ship.
Building a Production RAG Pipeline in TypeScript with pgvector and Next.js
A practical RAG build in TypeScript: chunk and embed documents, store vectors in Postgres with pgvector, retrieve, re-rank and answer in Next.js.
Adding AI Features to a Mobile App: On-Device vs Cloud
Adding AI features in mobile apps: picking the job, on-device models vs cloud APIs, safe architecture, store rules, cost per user and evaluating quality.
