Published on

Todos-MCP: Lessons from Creating a Todo App as a Minimal MCP Server

I wanted to wrap my head around what writing and publishing an MCP server looks like today.

What better way than the age old tradition of making a "todo list" app?

If you're also interested in building MCP servers, read on.

If you just want to get started using MCP servers, we have the fastest guide on the internet.

Demo video

If you want to try it out you can: Todos MCP server

Development overview

Frameworks

I used the fastmcp repo as a foundation. Next time instead I would just install fastmcp or another equivalent as a dependency (rather than cloning and modifying the fastmcp repo as they suggest).

There are a handful - let me know if there's more I should add to the list:

Tools

Inspector

The Model Context Protocol team have an Inspector you can use:

Model Context Protocol Inspector

It allows you to interact with your MCP server, and monitor the requests and responses. It handles Tools, Resources, Sampling and Roots.

mcp-cli

mcp-cli is another tool for interacting with your MCP server during development.

It doesn't give you the same depth of debugging info as the MCP Inspector, but it can be quicker to use from the terminal in your IDE.

mcp-cli screenshot

Deploying

There's a lot of different workflows for deploying to a package repo (in this case NPM).

I leveraged the GitHub actions from the fastmcp repo to autodeploy to NPM and JSR for every new release.

There are a few caveats to be aware of:

  1. You have to have the package created on NPM before you can deploy to it. I did this initially with $ npm publish from the CLI. You can read more here.
  2. The fastmcp repo uses the @semantic-release package as part of the deploy pipeline. This will automatically parse your commit logs (since the previous commit) to work out if a new release should be created. This is great once you have a project up and running, but can be a blocker when you're trying to initially publish a package and forgot to include a key (like NPM_TOKEN) in your GitHub actions environment.

Issues I ran in to

Configuration

MCP servers need to be runnable via a CLI command, and easy to distribute. For todos-mcp I decided to just support npx usage.

Because it has to run as an executable without the user having having access to the runtime, using environment variables for configratuion doesn't really make sense. How would they set the variables? So instead we've gotta use command-line arguments. I'm using yargs for todos-mcp.

Some benefits:

  • Immediate usage without environment setup (npx todos-mcp --baseDir ~/.claude/todos)
  • Built-in help documentation (--help)
  • Easily set defaults
  • Type validation and coercion out of the box
  • Nested command support for future extensibility

Spec support

The MCP spec defines both Tools and Resources, but I found there's a gap in how they're used in practice. While implementing todos-mcp, I discovered that Claude only effectively engages with Tools, not Resources.

For example, when implementing a todo list endpoint as a Resource:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "resources": [
      {
        "uri": "file:///todos/all.yaml",
        "name": "all.yaml",
        "description": "List of all todos",
        "mimeType": "text/plain"
      }
    ]
  }
}

Claude would fail to recognise and engage with the resource.

What are Resources for then? From the spec: "Tools enable models to interact with external systems, such as querying databases, calling APIs, or performing computations", and "Resources allow servers to share data that provides context to language models, such as files, database schemas, or application-specific information". It seems like Claude's MCP Client still needs some work to be using the protocol satisfactorily.

Debugging & Logging

Debugging MCP servers presents unique challenges:

  1. Standard console logging (console.log()) isn't useful since STDOUT is captured by the LLM client, not visible to the developer
  2. Server state can be hard to inspect during LLM interactions

We use pino for logging to a file, with a convenience script to monitor logs in real-time. This provides:

  • Structured JSON logging
  • Log level filtering
  • Timestamp and request correlation
  • Pretty printing for development

Our solution in todos-mcp:

{
  "scripts": {
    "logs": "tail -f ~/.claude/filesystem/todos/logs/app.log|npx pino-pretty"
  }
}

Storage

Persistent storage for MCP servers is also another curiousity. The server needs a reliable place to store data, but it's being run as an executable via npx, so how should we define where the storage goes?

For todos-mcp, we store data in ~/.claude/filesystem/todos/ by default (configurable via --baseDir), using YAML for todo lists.

Documentation

I'm also interested in how you document an MCP server. In some way they should be self-documenting. They advertise their capabilities as a response to requests from the Client. Documenting all that for a human reader is very verbose:

Request
{
  "method": "tools/list",
  "params": {}
}
Response
{
  "tools": [
    {
      "name": "Get-Todo",
      "description": "Get a specific todo by ID",
      "inputSchema": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "exclusiveMinimum": 0
          }
        },
        "required": [
          "id"
        ],
        "additionalProperties": false,
        "$schema": "http://json-schema.org/draft-07/schema#"
      }
    },
    ...
  ]
}