
MCP Servers: Giving AI Coding Assistants Real Dev Tools
How a frustrating habit of copy-pasting dev tool outputs into chat led me to build an MCP server, and what the process taught me about how AI assistants actually work.
I was debugging a project when I noticed what I had been doing. There was a JWT sitting in my terminal and instead of opening a dedicated tool, I was copying it into my chat with Claude to decode the claims. Claude handled it fine. But reading the response back, something clicked — I had been doing this constantly. Regex patterns, timestamps, base64 strings, all going through the same copy-paste loop between my terminal and a chat window. I was the bridge between my dev tools and the AI. That felt like the wrong setup.
The Context-Switching Tax
The problem was not that one JWT. It was the pattern around it. Regex testing happened in a browser tab. Timestamp conversions happened in a different browser tab. Cron expressions were pasted into a third site just to confirm they meant what I thought they meant. Each trip took maybe thirty seconds. None of them felt significant on their own. But by the end of a day I had made a dozen of these small detours, and each one pulled me slightly out of the flow I had been building.
Using Claude in chat was marginally better than opening a browser tab, but only marginally. The values I needed to check were in my terminal. The AI was in a separate window. I was still manually copying between them. The friction was smaller but the pattern was identical.
What an MCP Server Actually Is
MCP stands for Model Context Protocol. The name sounds more complicated than the concept. At its core, MCP is a way to give an AI assistant a set of callable tools that run locally on your machine. You define a tool with a name, a description, and a schema for its inputs. The AI reads those descriptions when it starts up, and from that point on it can call any tool whenever it decides one is relevant.
There is no cloud service involved. No data leaves your machine. No browser. When the AI calls your tool, it sends the input to a local process running on your computer, gets the result back, and uses it directly in its response.
From your side, it looks like the AI just knew the answer. From the AI's side, it used a tool you gave it. That distinction matters more than it sounds.
The Moment the Idea Clicked
The thing that actually pushed me to build something was a cron job. I was setting up a scheduled task and Claude suggested `0 9 * * 1-5`. The expression was almost certainly correct, but I did not know it from memory. So I opened a cron parser site, pasted it in, confirmed it meant every weekday at 9am, closed the tab, and came back to the conversation.
And then I thought: Claude just gave me that expression. It knows what cron syntax means. It could have told me what the expression does without me going anywhere. The only reason I left was that Claude could not run a cron parser. It could only describe one. That gap between knowing something and being able to do something felt suddenly fixable.
Deciding What to Build
Picking the twelve tools was fairly mechanical. I listed everything I had opened a browser tab for in the past month, then crossed off anything that needed the internet, anything that needed a real UI, and anything that felt like a one-off. What was left was a set of stateless, deterministic operations.
- Base64 encoding and decoding
- URL encoding and decoding
- Number base conversion from binary through base-36
- JSON and text file diffing
- JWT decoding and signature verification
- String hashing with MD5, SHA-256, and SHA-512
- UUID v4 and ULID generation
- Timestamp conversion and manipulation
- Regex testing with captured group details
- Cron expression parsing into plain English
Every item on that list shared the same property: deterministic output, well-understood logic, and no real reason it should be handled anywhere other than where I was already working.
What a Single Tool Looks Like
Here is what one tool looks like in an MCP server. This is the base64 encoder, one of the simpler ones in the toolkit. The entire thing is about fifteen lines.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
const server = new McpServer({ name: "my-toolkit", version: "1.0.0" });
server.tool(
"base64_encode",
"Encode a plain text string to Base64",
{ input: z.string().describe("The text to encode") },
async ({ input }) => ({
content: [{ type: "text", text: Buffer.from(input).toString("base64") }],
})
);That is the complete implementation. A name the AI uses to identify the tool. A description the AI reads to decide when to call it. A Zod schema that validates the input and tells the AI what to pass. And a handler that does the actual work. Honestly, I had expected more ceremony than that.
What Building It Taught Me About AI
The most useful thing I learned while building the toolkit had nothing to do with the tools themselves. It had to do with how the AI decides to use them.
The description field is not documentation. It is an instruction. When you write 'Encode a plain text string to Base64', you are not explaining the function to a reader. You are telling a model that has no eyes or ambient context what this tool is for, when it applies, and implicitly, when it does not. A vague description leads to the wrong tool being called. An overly broad description leads to the tool being called when it should not be.
I rewrote several descriptions after watching Claude make unexpected choices. The JWT decoder was initially described as 'Decode a JWT token'. Claude would call it on regular base64 strings that happened to look like tokens. Changing the description to 'Decode the header and claims from a JWT. Expects three dot-separated base64 segments.' reduced false calls immediately.
The model is not magic. It is matching your description against the situation it finds itself in. The better your description, the more reliably it matches. Treat tool descriptions as an interface, not metadata.
Once I understood that, writing tools stopped feeling like plumbing work and started feeling like prompt engineering with a type system. You are shaping the AI's decision-making surface as much as you are writing functions.
The Shift in How I Think About AI
After the server was running and connected to Claude, something shifted in how I thought about the whole setup. I had been thinking about it as a convenience, a way to avoid browser tabs. That framing was too small.
What I actually had was a runtime. Claude could now call the cron parser, read the result, and explain whether the schedule I described matched what I intended, all in one response. It could generate a UUID and use it immediately in a migration script it was helping me write. It could decode a JWT, check the expiration timestamp using the timestamp tool, and tell me whether the token was still valid.
None of those chains were hard to do manually. But having them happen automatically, inside a conversation that already had context about what I was building, changed the character of the interaction. Claude stopped being a thing I talked to and started being a thing I worked with. That shift is hard to quantify, but it is real, and it came entirely from giving the AI something to do instead of something to explain.
Where to Start If You Want to Build One
Pick one tool. Pick the one browser tab you open most often that has nothing to do with reading, only converting or computing. Write the handler. Write the description carefully, as if you were explaining to someone who cannot see your screen exactly when they should use this function and when they should not. Connect it and use it for a week.
After a week, you will know what the second tool should be.
The developer toolkit I built is open source on GitHub. It covers twelve tools, has 158 unit tests at 100% statement coverage, and supports Claude Desktop, Cursor, Windsurf, Zed, and VS Code out of the box. Use it as a starting point or just read the source to see how twelve tools are structured together.