Building your own MCP server for Claude Code: a working tutorial
After running ten community MCP servers for months, the next step is writing your own. Here is the start-to-finish tutorial, including the parts the docs gloss over.
The first ten MCP servers guide covers wiring up existing servers. This piece covers the next step: writing your own. It is a real tutorial — start to finish — including the parts where the official docs are thin, and grounded in the conversation about what MCP is and is not that has played out across YouTube and r/mcp through the first half of 2025.
If you have been using Claude Code with MCP and started thinking “I wish there was an MCP server for my internal X tool,” this is the piece for you.
Where MCP actually sits in the stack (and why people disagree about it)
The single best one-minute framing of MCP is in Fireship’s “Claude’s Model Context Protocol is here… Let’s test it” (6 min, 1.3M views, March 2025): “a USB-C port for AI applications.” MCP is a standard for connecting LLMs to external context (data resources) and external actions (tools). His example builds an MCP server that wires a Postgres database, a storage bucket, and a REST API into Claude Desktop — three integrations that previously required custom one-off plumbing become one server speaking the protocol.
The opposite-pole framing, from corbin’s “MCP Servers Explained in 5 Minutes” (5 min, 149K views, April 2025), is worth knowing about because it reflects how a meaningful slice of working engineers see the protocol: “we’ve been able to do this for the probably the last year, maybe two years.” His point is that ChatGPT Custom GPTs with Custom Actions have offered the same conceptual capability — letting an LLM call your API — since 2023. MCP is mostly a standardisation play, not a new capability.
Both are right. The standardisation is the thing that matters. Anthropic’s own “The Model Context Protocol (MCP)” (32 min, 239K views, June 2025) lays this out at length — once every IDE (Cursor, Windsurf, Claude Code, Continue.dev), every agent framework, and every desktop client speaks the same protocol, the same server you write works in all of them. That portability is what Custom GPTs never delivered.
The practical implication for this guide: you are not writing “a Claude Code plugin”. You are writing an MCP server that happens to work in Claude Code first.
What you are actually building
An MCP server is a process that:
- Speaks the Model Context Protocol over stdio (or HTTP)
- Exposes one or more “tools” (functions the model can call)
- Optionally exposes “resources” (data the model can read) and “prompts” (templated prompts the user can invoke)
The simplest useful MCP server is a single tool that does one thing. We are going to build that.
The example: a “git stats” MCP server
Concrete goal: an MCP server that exposes a git_stats tool which Claude Code can call to get summary stats about the current git repo (commit count, last-commit-author, hot files, etc.). Useful for “summarise what changed this sprint” prompts.
Step 1: Project setup
mkdir mcp-git-stats && cd mcp-git-stats
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
npx tsc --init
Adjust tsconfig.json for ESM:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "./dist"
},
"include": ["src/**/*"]
}
Add "type": "module" to package.json.
Step 2: The minimal server skeleton
src/index.ts:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { execSync } from 'node:child_process';
const server = new McpServer({
name: 'git-stats',
version: '0.1.0',
});
server.tool(
'git_stats',
'Get summary statistics about the current git repository.',
{
repoPath: z.string().describe('Absolute path to the git repository'),
sinceDays: z.number().optional().default(30).describe('Look back N days'),
},
async ({ repoPath, sinceDays }) => {
const since = `--since="${sinceDays} days ago"`;
const commitCount = execSync(
`git -C ${repoPath} log ${since} --oneline | wc -l`,
)
.toString()
.trim();
const topAuthor = execSync(
`git -C ${repoPath} shortlog -sn -e ${since} | head -1`,
)
.toString()
.trim();
const hotFiles = execSync(
`git -C ${repoPath} log ${since} --pretty=format: --name-only | sort | uniq -c | sort -rn | head -5`,
)
.toString()
.trim();
return {
content: [
{
type: 'text',
text: `Repository: ${repoPath}\nWindow: last ${sinceDays} days\nCommits: ${commitCount}\nTop author: ${topAuthor}\nHot files:\n${hotFiles}`,
},
],
};
},
);
const transport = new StdioServerTransport();
await server.connect(transport);
Build: npx tsc. Result: dist/index.js, executable as a Node process.
Fireship uses the same pattern in his video — McpServer class, zod schema validation on tool arguments, an async handler that returns the structured response. The shape is identical across the Python, Go, and Java SDKs the spec ships.
Step 3: Wire it into Claude Code
In ~/.claude/settings.json (or your project-level .claude/settings.json):
{
"mcpServers": {
"git-stats": {
"command": "node",
"args": ["/absolute/path/to/dist/index.js"]
}
}
}
Restart Claude Code. Verify with claude mcp list — git-stats should appear.
Step 4: Use it
Inside Claude Code:
“Use the git_stats tool to give me a summary of the topinsight repo over the last 14 days.”
The model recognises it has a git_stats tool available, calls it with the right arguments, gets back the text response, and synthesises a summary for you.
You just built an MCP server. The same server works unchanged in Claude Desktop, in Cursor’s MCP integration, in Windsurf, and in any other client speaking the protocol.
The parts the docs gloss over
After building three real MCP servers, here are the things I wish I had known earlier:
1. Tool descriptions matter more than you think
The model decides whether to call your tool based on the description. Be specific. Bad: “Gets git info.” Good: “Get summary statistics about the current git repository — commit count, top author, hot files. Use this when the user asks about repo activity over a time window.”
Spend time on this. Bad descriptions = the model never uses your tool.
2. Schema design matters too
The Zod schema becomes the JSON schema the model sees. Use .describe() liberally on every parameter. The model uses these descriptions to know what to pass. Fireship makes this point in his video explicitly: “providing data types along with a description will make your MCP server far more reliable” — the alternative is the model hallucinating argument values.
3. Errors should be model-readable
When your tool fails, do not just throw. Return a structured error in the response:
if (!fs.existsSync(repoPath)) {
return {
content: [{
type: 'text',
text: `Error: repository path does not exist: ${repoPath}`,
}],
isError: true,
};
}
The model can read this and decide what to do (try a different path, ask the user, give up gracefully).
4. Sub-process safety matters
The execSync example above is fine for trusted personal use. For a real server you would want to validate repoPath against a whitelist, escape arguments properly, or use a structured git library (isomorphic-git, etc.). MCP servers expose your machine to the LLM — treat them with appropriate paranoia.
This is exactly where the most-upvoted critical thread on r/mcp lands. “MCP is a security joke” (333 ups, June 2025) opens with the clean version of the concern: “One sketchy GitHub issue and your agent can leak private code… No sandboxing. No proper scoping. And worst of all, no observability.” The top comment is the genuinely-useful link to the awesome-mcp-security repo, which is the right starting point if you are building servers other people will run. The most-upvoted push-back under the OP is the correct rebuttal: “saying MCP has security problems is like saying APIs have a security problem. It depends on what it does.” Both are right. MCP is a protocol; the security posture is determined by the servers you wire up to it.
If you are writing a server you intend to share, the threat model is the LLM as an untrusted argument source. If you are writing a server you alone run against your own laptop, the threat model is much narrower. Be honest with yourself about which one you are doing.
5. Performance: keep responses small
The model has a context budget. A tool that returns 100KB of text on every call burns context fast. Return concise text; let the model ask for more if needed.
6. Naming convention
Tool names should be snake_case verbs or noun-verbs: get_commit_log, search_codebase, run_tests. Avoid generic names like query or execute.
What the community actually uses (and what it does not)
The MCP ecosystem has gone through one full hype-to-disillusionment cycle in its first six months, and r/mcp is the best place to watch the shape of it. Two threads frame the curve.
“My 5 most useful MCP servers” (466 ups, July 2025) is the upbeat version — the author’s working list is Context7 (docs lookup), Playwright (browser automation), Sentry (error tracking), GitHub, and Filesystem. The top comments add Atlassian for Jira shops and mcpdoc for pointing the model at custom internal docs sites. This is the “MCP is delivering” thread.
“I spent 3 weeks building my dream MCP setup and honestly, most of it was useless” (692 ups, August 2025) is the reality check that ran on the front page of the subreddit a few weeks after this guide was published. The author wired up 15 MCP servers — GitHub, Postgres, Playwright, Context7, Figma, Slack, Google Sheets, Linear, Sentry, Docker, and more — and ended up using only 4 daily. The most-upvoted comment under it tells you everything: “I stopped using the GitHub MCP and just use gh CLI. I find that my agent is much better with the CLI vs the MCP.” Several other top comments reach the same conclusion — for many tools, a CLI the agent can shell out to is more reliable than an MCP server wrapping the same functionality, because the agent has thousands of examples of the CLI in its training data and very few of your specific MCP server.
The synthesis the community arrived at: build MCP servers for things that cannot be reduced to a shell command (Figma design extraction, browser interactions, Postgres queries against a specific database with access controls, internal docs lookup) and skip MCP servers for things that already have a good CLI (GitHub via gh, AWS via aws, kubectl, etc.). The MCP layer is a wrapping cost; only pay it when the wrapping buys you something.
This is the most important framing change between the first wave of MCP enthusiasm and where the community sits in mid-2025. Build with it in mind.
When to write your own MCP server
Write one when:
- You repeatedly want to give Claude Code access to the same internal tool, API, or data
- An existing MCP server does not cover your use case
- You want strict control over what data the model can access (auditable code)
- The thing you are exposing has no good CLI equivalent (browser, design tool, internal API)
Do not write one when:
- An existing MCP server already does what you need (check the directory first)
- The integration is “call OpenAI API” — that is not what MCP is for
- The tool already has a great CLI the agent can shell out to (
gh,aws,gcloud,kubectl) - You would be better off with a slash command or shell script
Creator POV vs Reddit dissent
Creator coverage of MCP in 2025 (Fireship, corbin, Anthropic’s official rollout video, Tech With Tim’s longer tutorials) is uniformly positive on the protocol itself — the standardisation is the thing, the ecosystem will compound, build your own server, plug into the future. That framing is broadly correct.
Reddit’s mood is more textured. The r/mcp dissent is not “MCP is bad” — it is two more specific claims: the security model is underbaked relative to the enthusiasm (so do not run random community servers), and the marginal value of wrapping every tool in MCP is much lower than the hype suggests (so build for the cases where wrapping buys real leverage, and use CLIs everywhere else). Both critiques are coming from people who actively use MCP daily.
If you have only watched the YouTube coverage, you would build twelve servers. If you read the r/mcp threads from the same period, you would build three — and those three would be the ones that actually compound.
The bigger pattern
MCP is the actual interesting part of Claude Code as a platform. The CLI and model are commodities; the wrapper of “what tools can I plug in?” is what produces compounding value.
Writing your own MCP server is the unlock that turns Claude Code from a generic agent into one that knows about your specific work. It takes an afternoon for a simple server, a weekend for a real one. The leverage is meaningful — once you have it, your agent is genuinely smarter about your domain than any out-of-the-box tool can be.
The discipline that the community has converged on after six months of MCP-everything: write servers for the cases where MCP actually adds value, and let CLIs handle the rest. That is the working pattern in mid-2025, and it is more durable than the maximalist version.
For the broader Claude Code workflow, see our Claude Code review and the essential MCP servers list.
Sources
Every reference behind this piece. If we make a claim, it's because at least one of these said so — or we lived it ourselves.
- Firsthand Built three production MCP servers across personal and client projects
- Docs Model Context Protocol specification — MCP working group
- Docs MCP TypeScript SDK — MCP
- Docs Claude Code MCP integration documentation — Anthropic
- YouTube Claude's Model Context Protocol is here... Let's test it — Fireship
- YouTube MCP Servers Explained in 5 Minutes (for beginners) — corbin
- YouTube The Model Context Protocol (MCP) — Anthropic
- Blog r/mcp — MCP is a security joke — r/mcp
- Blog r/mcp — I spent 3 weeks building my "dream MCP setup" and honestly, most of it was useless — r/mcp
- Blog r/mcp — My 5 most useful MCP servers — r/mcp