How it works, what broke, and how I published it.
I recently built my first MCP server, and in the process, learned quite a bit about how MCP works in practice. If you're interested in building one yourself, or just want to know what goes into it, I thought it might be useful to walk through the key steps, the problems I ran into, and what I’d do differently next time.
The project is a simple todo list manager — nothing fancy, just a clean way to test out the structure and behavior of a basic MCP server.
MCP (Model Context Protocol) is a new spec that lets you serve up tools and context to LLMs in a structured way. You can think of an MCP server as a small, self-contained application that knows how to describe itself and respond to requests from language models.
Once a model like Claude is connected to an MCP server, it can choose from available tools, resources, etc — all via natural language.
For this project, I started with punkpeye/fastmcp, which gives you a basic server framework to build from. I also looked at a few other projects, like:
I also used:
MCP servers are typically run as one-off executables (npx my-mcp-server). That makes things like environment variables or .env files kind of a bad fit.
Instead, I switched to parsing command-line flags using yargs. This made things much clearer — users can pass in a base directory or config path explicitly, and you can offer built-in help messages.
npx todos-mcp --baseDir ~/.claude/todos
I’d use this approach again.
The spec supports both Tools and Resources, but Claude currently seems to mostly support Tools.
I added several resources to the server (in YAML format), but in testing, Claude didn't appear to load or reference them at all. It's possible this will improve over time, but for now, I’d focus on Tools if you want consistent behavior.
Most MCP servers use stdout to respond to LLMs — which means any logging you add to stdout just disappears. I ended up using pino to log to a file, and then created a script that tails the logs nicely:
"logs": "tail -f ~/.claude/filesystem/todos/logs/app.log | npx pino-pretty"
This setup worked well for local development.
If your MCP server runs via npx, where does it store persistent data? I ended up defaulting to a writable path like ~/.claude/todos, and storing YAML files there for things like tasks, user preferences, etc.
This approach isn’t perfect, but it works for personal projects and early-stage experiments.
Publishing to NPM and JSR
I wanted to make the MCP server installable via both npm and jsr.io. This took some setup, especially around GitHub Actions and semantic-release.
One key detail: you must manually publish your package to npm at least once before semantic-release can push updates.
Also, don’t forget to set the appropriate secrets (NPM_TOKEN, etc.) in your GitHub repo.
MCP servers describe themselves in JSON, which makes them self-documenting — but only in a technical sense. If you want human-friendly docs, you’ll still need to write them manually (README, usage examples, etc.).
In the future, I’d love to automate this a bit more — maybe by generating Markdown from tool metadata.
Building an MCP server gave me a much clearer understanding of how tool use works in practice. It’s still early days, and the ecosystem is rough in spots, but it’s surprisingly powerful once it’s running.
If you want to build your own:
If you've read this far, you're obviously as curious about this space as I am. You can see a demo video of the tool and read a bit more detail about the gotchas of developing for MCP here: https://mcps.sh/blog/news/2025-04-08-the-most-productive-mcp-server-todos-mcp
Let me know if you build one too — I'm excited to see how the MCP space develops.