Building Your First MCP Server for Claude Code
What You’ll Learn
- What an MCP server actually does in a Claude Code workflow
- How to build a minimal MCP server in TypeScript
- How to register a tool with input validation
- How to connect your local server to Claude Code over stdio
- The two mistakes that break most first-time MCP setups
If you have been using Claude Code by pasting API responses, log snippets, or project metadata into chat, MCP is the piece that removes that manual step.
An MCP server is just a thin adapter between Claude and a capability you control. That capability might be a local script, a database query, an internal API, or a deployment tool. Claude does not need a custom plugin system for each one. It talks to any server that follows the Model Context Protocol.
This matters for client work because it turns repetitive glue code into reusable infrastructure. Once you have one solid server, you can keep extending it instead of rebuilding the same automation inside every project.
In this post, I will build the smallest useful server possible: one tool that summarizes a block of text. It is simple enough to understand in one sitting, but it covers the same moving parts you use in real projects.
What MCP Looks Like in Practice
When Claude Code connects to an MCP server, it can discover tools exposed by that server and call them with structured arguments.
That means instead of prompting like this:
Here is a long error log. Please summarize the important parts.
You can give Claude a tool that already knows how to process logs, query APIs, or inspect local state.
The mental model is straightforward:
- Claude Code launches or connects to an MCP server.
- The server declares the tools it supports.
- Claude calls a tool with validated input.
- The server runs your code and returns structured output.
For freelance automation work, this is where things start compounding. A one-off script helps once. An MCP tool becomes something Claude can reuse every day inside your workflow.
Project Setup
The current TypeScript SDK quickstart uses @modelcontextprotocol/server and zod. Start with a plain Node project:
npm init -y
npm install @modelcontextprotocol/server zod
npm install -D @types/node typescript
Create src/index.ts, then add a minimal tsconfig.json if you do not already have one:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "build",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src"]
}
You can keep this tiny. No framework, no HTTP layer, no extra abstraction.
Build a Minimal MCP Server
Here is the full server:
import { McpServer, StdioServerTransport } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';
const server = new McpServer({
name: 'starter-tools',
version: '1.0.0',
});
server.registerTool(
'summarize-text',
{
title: 'Summarize Text',
description: 'Return a short summary of an input block of text.',
inputSchema: z.object({
text: z.string().min(1).describe('The text to summarize'),
maxSentences: z
.number()
.int()
.min(1)
.max(5)
.default(3)
.describe('Maximum number of sentences to return'),
}),
},
async ({ text, maxSentences }) => {
const cleaned = text.replace(/\s+/g, ' ').trim();
const sentences = cleaned.split(/(?<=[.!?])\s+/).filter(Boolean);
const summary = sentences.slice(0, maxSentences).join(' ');
return {
content: [
{
type: 'text',
text: summary || 'No summary could be generated.',
},
],
};
},
);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('starter-tools MCP server running on stdio');
}
main().catch((error) => {
console.error('Fatal error in main():', error);
process.exit(1);
});
There are only three parts that matter:
1. Create the server
const server = new McpServer({
name: 'starter-tools',
version: '1.0.0',
});
This is just metadata, but it is what the client sees. Use a stable name and version because you will want to evolve the server later.
2. Register a tool
server.registerTool('summarize-text', { ... }, async ({ text, maxSentences }) => {
...
});
The important detail is the inputSchema. This is where zod pays off. Claude does better when the tool contract is explicit, and you get argument validation for free.
3. Connect over stdio
const transport = new StdioServerTransport();
await server.connect(transport);
For a local tool, stdio is the fastest way to get running. Claude Code starts the process, communicates over standard input/output, and treats your tool like any other MCP server.
One important rule from the SDK docs: keep protocol output on stdout, and use console.error() for logs. If you write debugging noise to stdout, you can corrupt the JSON-RPC stream and your server will fail in confusing ways.
Add Build Scripts
Your package.json should include at least this:
{
"type": "module",
"scripts": {
"build": "tsc",
"start": "node build/index.js"
}
}
Then compile it:
npm run build
At this point, you have a real MCP server process. The only thing left is wiring it into Claude Code.
Connect It to Claude Code
For a project-scoped setup, create a .mcp.json file in your project root:
{
"mcpServers": {
"starter-tools": {
"type": "stdio",
"command": "node",
"args": ["./build/index.js"]
}
}
}
If you prefer the CLI, Claude Code also supports adding local stdio servers directly.
On macOS or Linux:
claude mcp add --transport stdio starter-tools -- node ./build/index.js
On native Windows, use the documented cmd /c wrapper for commands like npx. For a direct node command, a project .mcp.json file is usually the simpler route, especially if you want the setup checked into the repo.
Once Claude Code loads the server, run:
/mcp
You should see your server in the list. Then try a prompt like:
Use the summarize-text tool to summarize this deployment log in 2 sentences.
If Claude can see the tool, your integration is working.
Two Mistakes That Usually Break First-Time MCP Servers
Writing logs to stdout
This is the most common one. MCP stdio transport uses standard output for protocol messages. If you spam console.log() while debugging, the connection can break even though your code looks fine.
Use console.error() for logging instead.
Overbuilding the first version
A lot of developers try to start with auth, remote APIs, multiple tools, config loading, and a production-grade server lifecycle. That is useful later, but it makes the first test harder than it needs to be.
The first goal is simpler: get one tool discovered and called successfully.
Once that works, add real value around it. In client projects, that usually means one of these:
- wrap an internal REST API
- expose a safe read-only database query tool
- add deployment or environment inspection helpers
- turn a frequently reused script into a callable tool
Where to Go Next
After your first local server works, the next step is not “add more code everywhere.” It is choosing one useful surface area and making it reliable.
Good second tools usually have these properties:
- they save repeated manual work
- they have clear, structured inputs
- they can fail safely
- they return results Claude can act on immediately
That is why MCP fits real freelance delivery work so well. Instead of promising “AI automation” in the abstract, you can build concrete tools that shorten a client workflow, then let Claude operate them naturally.
If you want help building MCP servers, AI automation workflows, or internal developer tools, take a look at my portfolio: voidcraft-site.vercel.app.