# Why I replaced an MCP server with a CLI

> My Things MCP server made macOS ask for permission on every agent session. I replaced its 22 tools with a 598-line CLI and a skill, and here's what changed.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-11-11 | Topics: [AI](https://flaviocopes.com/tags/ai/) | Canonical: https://flaviocopes.com/mcp-server-to-cli/

My tasks live in [Things 3](https://culturedcode.com/things/). I want my coding agents to read them and add to them, because a lot of my plans start in a conversation with an agent.

For most of this year an MCP server did that job. In September I replaced it with a small command line tool, and I haven't missed the server once.

The usual argument for a CLI is the context window. That wasn't my reason. Mine was a macOS permission prompt that I got tired of clicking.

## The MCP server I was using

In January I cloned [things-mcp](https://github.com/hald/things-mcp) into `~/dev/things-mcp` and added it to my agents. It's a Python server built on FastMCP, and it's well made. Version 0.6.0 exposes 22 tools: `get_today`, `get_projects`, `search_advanced`, `add_todo`, `update_todo` and so on.

It reaches Things in two ways. Writes go through the Things URL scheme. Reads go through a library called [things.py](https://github.com/thingsapi/things.py), which opens the SQLite database that Things keeps on disk.

That database lives here:

```txt
~/Library/Group Containers/JLMPQHK86H.com.culturedcode.ThingsMac
```

If MCP is new to you, [what is MCP](https://flaviocopes.com/what-is-mcp/) explains servers, clients and tools.

## The prompt

That folder belongs to Things, and macOS 15 protects it. When another program opens a file inside it, macOS stops and asks whether that program may "access data from other apps".

It's a good protection. The server was reading another app's private database, and macOS was right to check with me.

The trouble is how often it asked. An agent starts its MCP servers when a session starts, and I start many sessions a day. The prompt kept coming back, in the middle of work that had nothing to do with my tasks.

So I looked for a way to read Things without touching its files.

## What the CLI does instead

Things has an AppleScript dictionary. You can ask the running app for a project and its to-dos, and Things answers from its own process. Nothing reads the database from outside, so the "data from other apps" prompt never shows up.

On September 7 I built [Things CLI](https://flaviocopes.com/apps/things-cli/) around that idea. Reads and updates go through `osascript`, and creating things goes through the URL scheme, like the MCP server did.

```mermaid
flowchart LR
  accTitle: Two ways for an agent to reach Things
  accDescr: The MCP server reads the Things SQLite database on disk, which is inside a folder macOS protects, so macOS asks for permission. The CLI sends Apple events to the running Things app, and Things reads its own database.
  A[Coding agent]
  A -- MCP tool call --> S[things-mcp server]
  S -- opens the file --> D[(Things database)]
  D -. macOS asks .-> P[Permission prompt]
  A -- shell command --> C[things CLI]
  C -- Apple events --> T[Things app]
  T --> D
```

macOS still asks once, the first time, whether your terminal may control Things. You click OK and it remembers.

The whole tool is 598 lines of JavaScript with no dependencies. This is all it takes to add a to-do to a project:

```bash
things add "Renew the domain" --project "Website" --when today --tags work
```

Read commands take `--json`, so an agent gets structured data when it wants it.

## How the agent knows about it

An MCP server announces its tools to the agent. A CLI doesn't, so the agent needs to be told it exists.

I use a [skill](https://flaviocopes.com/agent-skills/) for that. It's one Markdown file. Its description is 264 characters, and that's the only part the agent sees when a session starts. The body is 2,223 characters, and the agent loads it when a task involves Things.

The CLI also describes itself. Every CLI I build has a `capabilities` command that prints what the tool can do, with one example per task:

```bash
things capabilities
```

```txt
things 0.1.1
Add, read and update projects, to-dos and notes in Things 3.

What it can do:
  Add a to-do to a project with notes, schedule and tags
    $ things add "Renew the domain" --project "Website" --when today --tags work
  Create a new project in an area
    $ things project "Launch newsletter redesign" --area "flaviocopes.com" --notes "Ship by October"
  Import a whole project from a Markdown or JSON file
    $ things import sponsors.md
```

The list goes on for six more tasks. An agent that has never seen the tool reads this once and knows which command to run.

## A real session

This post came out of the CLI. This morning I typed this in Claude Code:

```txt
which post could I write today? see in Things list BLOG POSTS
```

The agent loaded the skill, then ran one command:

```bash
things show "BLOG POSTS"
```

```txt
# BLOG POSTS

If I end up without ideas:
- transcribe a podcast and write 4-5 posts about the topics

- Tailscale
- Cloudflare containers
- build a desktop version of Sendy
- Workerd open source, self host
```

That's the start of the output, with a few lines left out. The agent read the ideas and checked which ones I had already written, and this post came out of that conversation.

On a small project the full output fits on one screen. Completed to-dos, due dates and tags come along:

![The things show command in a terminal, printing a project's notes and its seven to-dos](https://flaviocopes.com/images/things-cli/screenshot-project.png)

## MCP server vs CLI, for this job

I measured both on my Mac. The MCP column is things-mcp 0.6.0, the CLI column is Things CLI 0.1.1.

| | things-mcp | Things CLI |
|---|---|---|
| What the agent sees at startup | 22 tool definitions | a 264-character skill description |
| Size of the instructions | 5,363 characters of names, signatures and docstrings | 2,223 characters, loaded on demand |
| Runs when | every session, as a background process | only when a command runs |
| How it reads | opens the Things SQLite database | asks the Things app with Apple events |
| macOS permission | "access data from other apps", again and again | "control Things", once |
| Needs | Python, uv, a virtual environment | Node.js 20 |
| Things has to be open | no | yes, macOS opens it |

The 5,363 characters is what I counted in the server's source. The client wraps each tool in a JSON schema, so the agent receives more than that. It's still small. Railway's MCP server costs me 37,264 tokens, and this one would be a fraction.

So context was never the problem here. The process that runs all day and reads another app's files was.

## What I lost

The CLI is slower on big reads. My BLOG POSTS project is long, because I paste notes under the ideas. Reading it took 9.7 seconds and printed 10,469 lines:

```bash
time things show "BLOG POSTS"
```

```txt
1.38s user 1.74s system 32% cpu 9.717 total
```

Reading the database file directly is faster, and that's what I gave up. The same read with `--json` is 905 KB, which is far too much to hand to an agent in one go. The agent this morning piped it through `head`.

Things also has to be running, and reads go through whatever the AppleScript dictionary exposes. The database has more in it.

I can live with all three. Most of what my agents do with Things is small: add a reminder, create a project for a plan, mark a step done.

## When I keep the MCP server

I didn't remove MCP from my setup. [Borumi](https://borumi.com/), my video editor, has an MCP server with 49 tools, and my agents [edit my videos](https://flaviocopes.com/agent-video-editing/) through it. That one stays.

The difference is state. Editing a video is a long conversation with a running app: open a transaction, read the timeline, change segments, commit. A server that stays connected fits that well.

Things is the opposite. Each request stands alone and finishes in one call, so a command that starts, prints and exits is enough.

My rule now is short. If the agent needs a long, stateful session with an app, or a remote service with its own login, I use an MCP server. If one shell command can do it, I write a CLI and a skill.

[The MCP servers connected to my editor](https://flaviocopes.com/mcp-servers-i-use/) has the full inventory of what I kept. The other command line tools I use every day are in [the CLI tools I use](https://flaviocopes.com/cli-tools-i-use/), and the Things walkthrough is [Add tasks and projects to Things 3 from the terminal](https://flaviocopes.com/apps/tutorials/things-3-command-line/).
