todosforagents 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. todosforagents-0.1.0/.gitignore +14 -0
  2. todosforagents-0.1.0/LICENSE +22 -0
  3. todosforagents-0.1.0/PKG-INFO +123 -0
  4. todosforagents-0.1.0/README.md +95 -0
  5. todosforagents-0.1.0/hatch_build.py +32 -0
  6. todosforagents-0.1.0/pyproject.toml +114 -0
  7. todosforagents-0.1.0/src/mcp_todos/SKILL.md +63 -0
  8. todosforagents-0.1.0/src/mcp_todos/__init__.py +1 -0
  9. todosforagents-0.1.0/src/mcp_todos/__main__.py +31 -0
  10. todosforagents-0.1.0/src/mcp_todos/db/__init__.py +30 -0
  11. todosforagents-0.1.0/src/mcp_todos/db/dialect.py +62 -0
  12. todosforagents-0.1.0/src/mcp_todos/db/store.py +1731 -0
  13. todosforagents-0.1.0/src/mcp_todos/db/tables.py +223 -0
  14. todosforagents-0.1.0/src/mcp_todos/icon.png +0 -0
  15. todosforagents-0.1.0/src/mcp_todos/ids.py +35 -0
  16. todosforagents-0.1.0/src/mcp_todos/keys.py +129 -0
  17. todosforagents-0.1.0/src/mcp_todos/models.py +273 -0
  18. todosforagents-0.1.0/src/mcp_todos/runtime.py +180 -0
  19. todosforagents-0.1.0/src/mcp_todos/server.py +1241 -0
  20. todosforagents-0.1.0/src/mcp_todos/ui.py +53 -0
  21. todosforagents-0.1.0/src/mcp_todos_local/__init__.py +5 -0
  22. todosforagents-0.1.0/src/mcp_todos_local/__main__.py +81 -0
  23. todosforagents-0.1.0/src/mcp_todos_local/auth.py +99 -0
  24. todosforagents-0.1.0/src/mcp_todos_local/config.py +42 -0
  25. todosforagents-0.1.0/src/mcp_todos_local/http.py +72 -0
  26. todosforagents-0.1.0/src/mcp_todos_local/migrations/env.py +16 -0
  27. todosforagents-0.1.0/src/mcp_todos_local/migrations/script.py.mako +27 -0
  28. todosforagents-0.1.0/src/mcp_todos_local/migrations/versions/0001_initial.py +125 -0
  29. todosforagents-0.1.0/src/mcp_todos_local/migrations/versions/0002_board_keys.py +82 -0
  30. todosforagents-0.1.0/src/mcp_todos_local/runtime.py +100 -0
  31. todosforagents-0.1.0/src/mcp_todos_local/schema.py +87 -0
  32. todosforagents-0.1.0/src/mcp_todos_local/sqlite.py +79 -0
  33. todosforagents-0.1.0/ui/dist/index.html +431 -0
@@ -0,0 +1,14 @@
1
+ # Python
2
+ .venv/
3
+ __pycache__/
4
+ *.pyc
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ # Node
8
+ node_modules/
9
+ # Build output: the wheel and sdist, and the built UI they carry
10
+ dist/
11
+ # The MCP bundle's lock and pack, which a release makes
12
+ mcpb/uv.lock
13
+ *.mcpb
14
+ .env
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NimbleBrain, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.5
2
+ Name: todosforagents
3
+ Version: 0.1.0
4
+ Summary: To-do boards for AI agents, as an MCP server with a board UI. Runs locally on one SQLite file.
5
+ Project-URL: Homepage, https://github.com/NimbleBrainInc/todosforagents
6
+ Project-URL: Repository, https://github.com/NimbleBrainInc/todosforagents.git
7
+ Project-URL: Issues, https://github.com/NimbleBrainInc/todosforagents/issues
8
+ Author-email: NimbleBrain Inc <hello@nimblebrain.ai>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agents,kanban,mcp,mcp-apps,mcp-server,sqlite,to-do,todos
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Requires-Python: >=3.13
19
+ Requires-Dist: aiosqlite>=0.21
20
+ Requires-Dist: alembic>=1.14
21
+ Requires-Dist: fastmcp<5,>=4
22
+ Requires-Dist: mcp<3,>=2
23
+ Requires-Dist: nimblebrain-server-sdk==0.4.1
24
+ Requires-Dist: pgkeyset==0.2.0
25
+ Requires-Dist: pydantic>=2.12
26
+ Requires-Dist: sqlalchemy[asyncio]<2.1,>=2.0.44
27
+ Description-Content-Type: text/markdown
28
+
29
+ # Todos for Agents
30
+
31
+ To-do boards for AI agents, served as an MCP server. An agent lays out a plan, moves each
32
+ to-do through its status, records why one is blocked, and asks what to do next. A person
33
+ sees the same boards in a board UI that renders inside MCP Apps hosts.
34
+
35
+ It runs on your own machine: one SQLite file, no account, no cloud service.
36
+
37
+ ## Install
38
+
39
+ Run it with [uv](https://docs.astral.sh/uv/) (Python 3.13 or later):
40
+
41
+ ```sh
42
+ uvx todosforagents
43
+ ```
44
+
45
+ That serves MCP over stdio. To add it to Claude Code:
46
+
47
+ ```sh
48
+ claude mcp add todosforagents -- uvx todosforagents
49
+ ```
50
+
51
+ For Claude Desktop, the MCP bundle `@nimblebraininc/todosforagents` is available from
52
+ [mpak](https://www.mpak.dev). It runs the same package and asks where to keep the database.
53
+
54
+ Any other MCP client that starts a stdio server can run `uvx todosforagents` the same way.
55
+ To serve HTTP on `127.0.0.1` instead, run `uvx todosforagents --http`; HTTP requires a
56
+ bearer token, which `uvx todosforagents --print-token` prints. See
57
+ [`docs/local.md`](docs/local.md).
58
+
59
+ ## Where your data lives
60
+
61
+ Everything is in one SQLite file, `todos.db`, in your user data directory:
62
+
63
+ | OS | Default location |
64
+ |---|---|
65
+ | macOS | `~/Library/Application Support/todosforagents/todos.db` |
66
+ | Linux | `$XDG_DATA_HOME/todosforagents/todos.db` (`~/.local/share/...` when unset) |
67
+ | Windows | `%LOCALAPPDATA%\todosforagents\todos.db` |
68
+
69
+ Set `TODOS_LOCAL_DB` to use another file. Keep it on a local disk, not a network share.
70
+ Several clients may use the same file at once. Upgrades, backups and the HTTP token are
71
+ covered in [`docs/local.md`](docs/local.md).
72
+
73
+ ## Tools
74
+
75
+ | Tool | What it does |
76
+ |---|---|
77
+ | `create_todo` | Add one to-do (to the default board unless one is named) |
78
+ | `plan_todos` | Add several to-dos in one call, optionally as the steps of a goal |
79
+ | `update_todo` | Change a to-do's title, details, priority, labels, assignee, due date or goal |
80
+ | `start_todo` | Mark a to-do in progress |
81
+ | `complete_todo` | Mark a to-do done |
82
+ | `block_todo` | Mark a to-do blocked, with the reason |
83
+ | `reopen_todo` | Put a to-do back to open |
84
+ | `cancel_todo` | Mark a to-do as won't do |
85
+ | `reorder_todos` | Set the order of one status lane on a board |
86
+ | `archive_todo` | Hide a to-do from default views, status unchanged |
87
+ | `list_todos` | List or search to-dos, with filters and cursor paging |
88
+ | `get_todo` | Read one to-do whole, with its steps and recent activity |
89
+ | `get_next_todo` | The highest-priority open to-do: what to work on next |
90
+ | `get_board_summary` | A board's lane counts and what needs attention |
91
+ | `list_boards` | List the boards |
92
+ | `create_board` | Create a board |
93
+ | `update_board` | Change a board's name, description or lanes |
94
+
95
+ A to-do's status (`open`, `in_progress`, `blocked`, `done`, `cancelled`) changes only through
96
+ the five status verbs. Every result carries a `next_step` telling the agent what to do with
97
+ it, and the server also serves a skill (`skill://todos/SKILL.md`) with the rules for choosing
98
+ a tool.
99
+
100
+ ## Keys
101
+
102
+ Every board has a short key, and every to-do a key made of its board's key and its number
103
+ on that board: `MAG-3` is the third to-do on board `MAG`. The default board's key is `TODO`.
104
+ Any argument that takes a to-do id takes its key, and any argument that takes a board id
105
+ takes the board's key, so an agent can pass on "MAG-3" exactly as a person said it.
106
+
107
+ ## The board UI
108
+
109
+ `get_board_summary` and `get_todo` are bound to the board UI (`ui://todos/main`). A host that
110
+ supports [MCP Apps](https://modelcontextprotocol.io/) shows the board, or the to-do, when the
111
+ agent calls them. The board reads and changes data only through the same tools.
112
+
113
+ ## More
114
+
115
+ - [`docs/architecture.md`](docs/architecture.md): the core, runtimes, data model, the UI
116
+ binding, errors and annotations.
117
+ - [`docs/local.md`](docs/local.md): the database file, several processes, the HTTP token,
118
+ upgrades and backups.
119
+ - [`CONTRIBUTING.md`](CONTRIBUTING.md): building and testing.
120
+
121
+ ## Licence
122
+
123
+ MIT. See [`LICENSE`](LICENSE).
@@ -0,0 +1,95 @@
1
+ # Todos for Agents
2
+
3
+ To-do boards for AI agents, served as an MCP server. An agent lays out a plan, moves each
4
+ to-do through its status, records why one is blocked, and asks what to do next. A person
5
+ sees the same boards in a board UI that renders inside MCP Apps hosts.
6
+
7
+ It runs on your own machine: one SQLite file, no account, no cloud service.
8
+
9
+ ## Install
10
+
11
+ Run it with [uv](https://docs.astral.sh/uv/) (Python 3.13 or later):
12
+
13
+ ```sh
14
+ uvx todosforagents
15
+ ```
16
+
17
+ That serves MCP over stdio. To add it to Claude Code:
18
+
19
+ ```sh
20
+ claude mcp add todosforagents -- uvx todosforagents
21
+ ```
22
+
23
+ For Claude Desktop, the MCP bundle `@nimblebraininc/todosforagents` is available from
24
+ [mpak](https://www.mpak.dev). It runs the same package and asks where to keep the database.
25
+
26
+ Any other MCP client that starts a stdio server can run `uvx todosforagents` the same way.
27
+ To serve HTTP on `127.0.0.1` instead, run `uvx todosforagents --http`; HTTP requires a
28
+ bearer token, which `uvx todosforagents --print-token` prints. See
29
+ [`docs/local.md`](docs/local.md).
30
+
31
+ ## Where your data lives
32
+
33
+ Everything is in one SQLite file, `todos.db`, in your user data directory:
34
+
35
+ | OS | Default location |
36
+ |---|---|
37
+ | macOS | `~/Library/Application Support/todosforagents/todos.db` |
38
+ | Linux | `$XDG_DATA_HOME/todosforagents/todos.db` (`~/.local/share/...` when unset) |
39
+ | Windows | `%LOCALAPPDATA%\todosforagents\todos.db` |
40
+
41
+ Set `TODOS_LOCAL_DB` to use another file. Keep it on a local disk, not a network share.
42
+ Several clients may use the same file at once. Upgrades, backups and the HTTP token are
43
+ covered in [`docs/local.md`](docs/local.md).
44
+
45
+ ## Tools
46
+
47
+ | Tool | What it does |
48
+ |---|---|
49
+ | `create_todo` | Add one to-do (to the default board unless one is named) |
50
+ | `plan_todos` | Add several to-dos in one call, optionally as the steps of a goal |
51
+ | `update_todo` | Change a to-do's title, details, priority, labels, assignee, due date or goal |
52
+ | `start_todo` | Mark a to-do in progress |
53
+ | `complete_todo` | Mark a to-do done |
54
+ | `block_todo` | Mark a to-do blocked, with the reason |
55
+ | `reopen_todo` | Put a to-do back to open |
56
+ | `cancel_todo` | Mark a to-do as won't do |
57
+ | `reorder_todos` | Set the order of one status lane on a board |
58
+ | `archive_todo` | Hide a to-do from default views, status unchanged |
59
+ | `list_todos` | List or search to-dos, with filters and cursor paging |
60
+ | `get_todo` | Read one to-do whole, with its steps and recent activity |
61
+ | `get_next_todo` | The highest-priority open to-do: what to work on next |
62
+ | `get_board_summary` | A board's lane counts and what needs attention |
63
+ | `list_boards` | List the boards |
64
+ | `create_board` | Create a board |
65
+ | `update_board` | Change a board's name, description or lanes |
66
+
67
+ A to-do's status (`open`, `in_progress`, `blocked`, `done`, `cancelled`) changes only through
68
+ the five status verbs. Every result carries a `next_step` telling the agent what to do with
69
+ it, and the server also serves a skill (`skill://todos/SKILL.md`) with the rules for choosing
70
+ a tool.
71
+
72
+ ## Keys
73
+
74
+ Every board has a short key, and every to-do a key made of its board's key and its number
75
+ on that board: `MAG-3` is the third to-do on board `MAG`. The default board's key is `TODO`.
76
+ Any argument that takes a to-do id takes its key, and any argument that takes a board id
77
+ takes the board's key, so an agent can pass on "MAG-3" exactly as a person said it.
78
+
79
+ ## The board UI
80
+
81
+ `get_board_summary` and `get_todo` are bound to the board UI (`ui://todos/main`). A host that
82
+ supports [MCP Apps](https://modelcontextprotocol.io/) shows the board, or the to-do, when the
83
+ agent calls them. The board reads and changes data only through the same tools.
84
+
85
+ ## More
86
+
87
+ - [`docs/architecture.md`](docs/architecture.md): the core, runtimes, data model, the UI
88
+ binding, errors and annotations.
89
+ - [`docs/local.md`](docs/local.md): the database file, several processes, the HTTP token,
90
+ upgrades and backups.
91
+ - [`CONTRIBUTING.md`](CONTRIBUTING.md): building and testing.
92
+
93
+ ## Licence
94
+
95
+ MIT. See [`LICENSE`](LICENSE).
@@ -0,0 +1,32 @@
1
+ """Put the built board UI inside the core's wheel, so every install serves it.
2
+
3
+ The UI is one inlined HTML file built from ``ui/`` (``cd ui && npm ci && npm run build``). A
4
+ wheel carries it as ``mcp_todos/ui_dist/index.html``, where :mod:`mcp_todos.ui` finds it with
5
+ no setting and no source checkout. Building a wheel without it fails, so no wheel ships the
6
+ "UI is not built" placeholder. An editable install reads ``ui/dist`` from the source tree
7
+ instead, so it needs no build hook.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from hatchling.builders.hooks.plugin.interface import BuildHookInterface
16
+
17
+ UI_INDEX = Path("ui") / "dist" / "index.html"
18
+ PACKAGED_AS = "mcp_todos/ui_dist/index.html"
19
+
20
+
21
+ class UIBuildHook(BuildHookInterface):
22
+ PLUGIN_NAME = "custom"
23
+
24
+ def initialize(self, version: str, build_data: dict[str, Any]) -> None:
25
+ if self.target_name != "wheel" or version == "editable":
26
+ return
27
+ index = Path(self.root) / UI_INDEX
28
+ if not index.is_file():
29
+ raise FileNotFoundError(
30
+ f"{index} is missing: build the UI first (cd ui && npm ci && npm run build)"
31
+ )
32
+ build_data["force_include"][str(index)] = PACKAGED_AS
@@ -0,0 +1,114 @@
1
+ [project]
2
+ name = "todosforagents"
3
+ version = "0.1.0"
4
+ description = "To-do boards for AI agents, as an MCP server with a board UI. Runs locally on one SQLite file."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.13"
9
+ authors = [
10
+ { name = "NimbleBrain Inc", email = "hello@nimblebrain.ai" },
11
+ ]
12
+ keywords = ["mcp", "mcp-server", "mcp-apps", "todos", "to-do", "kanban", "agents", "sqlite"]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Intended Audience :: Developers",
16
+ "Operating System :: OS Independent",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Programming Language :: Python :: 3.14",
20
+ ]
21
+ dependencies = [
22
+ # FastMCP 4 on the MCP SDK v2: serves the 2026-07-28 era and the session-based eras from
23
+ # one server. The ceiling excludes the next major, which removes 4.x compatibility shims.
24
+ "fastmcp>=4,<5",
25
+ "mcp>=2,<3",
26
+ "pydantic>=2.12",
27
+ # Below 2.1: 2.1 compiles a LIKE's ESCAPE literal differently on Postgres, which changes the
28
+ # search predicate tests/golden/paging.txt freezes. Moving to 2.1 is a reviewed change.
29
+ "sqlalchemy[asyncio]>=2.0.44,<2.1",
30
+ # Annotations, announcements, listing rows, ids, skills and the error contract: the SDK
31
+ # core only, no extras. Pinned exactly: tests/golden/surface.json gates every bump.
32
+ "nimblebrain-server-sdk==0.4.1",
33
+ # Keyset paging. Pinned exactly: tests/test_paging_golden.py holds the cursors agents
34
+ # already hold, so an upgrade is a deliberate, reviewed change.
35
+ "pgkeyset==0.2.0",
36
+ # The local runtime: SQLite through aiosqlite, and its schema history through Alembic.
37
+ "aiosqlite>=0.21",
38
+ "alembic>=1.14",
39
+ ]
40
+
41
+ [project.urls]
42
+ Homepage = "https://github.com/NimbleBrainInc/todosforagents"
43
+ Repository = "https://github.com/NimbleBrainInc/todosforagents.git"
44
+ Issues = "https://github.com/NimbleBrainInc/todosforagents/issues"
45
+
46
+ [project.scripts]
47
+ todosforagents = "mcp_todos_local.__main__:main"
48
+
49
+ [project.entry-points."mcp_todos.runtimes"]
50
+ local = "mcp_todos_local.runtime:LocalRuntime"
51
+
52
+ [build-system]
53
+ requires = ["hatchling>=1.27"]
54
+ build-backend = "hatchling.build"
55
+
56
+ [tool.hatch.build.targets.wheel]
57
+ packages = ["src/mcp_todos", "src/mcp_todos_local"]
58
+
59
+ # Packs the built UI (ui/dist/index.html) into the wheel as mcp_todos/ui_dist/index.html, and
60
+ # refuses to build a wheel without it.
61
+ [tool.hatch.build.targets.wheel.hooks.custom]
62
+ path = "hatch_build.py"
63
+
64
+ # The sdist carries the built UI, so a wheel built from it (pip, uv) packs the same board.
65
+ # It is a build output (ui/ is gitignored there), so it is named here, not found through git.
66
+ [tool.hatch.build.targets.sdist]
67
+ include = [
68
+ "/src",
69
+ "/ui/dist/index.html",
70
+ "/hatch_build.py",
71
+ "/README.md",
72
+ "/LICENSE",
73
+ ]
74
+ artifacts = ["/ui/dist/index.html"]
75
+
76
+ [tool.ruff]
77
+ target-version = "py313"
78
+ line-length = 100
79
+
80
+ [tool.ruff.lint]
81
+ select = [
82
+ "E", # pycodestyle errors
83
+ "W", # pycodestyle warnings
84
+ "F", # pyflakes
85
+ "I", # isort
86
+ "B", # flake8-bugbear
87
+ "C4", # flake8-comprehensions
88
+ "UP", # pyupgrade
89
+ ]
90
+ ignore = [
91
+ "E501", # line too long - handled by formatter
92
+ "B008", # do not perform function calls in argument defaults
93
+ ]
94
+
95
+ [tool.ruff.lint.isort]
96
+ known-first-party = ["mcp_todos", "mcp_todos_local"]
97
+
98
+ [tool.ruff.format]
99
+ quote-style = "double"
100
+ indent-style = "space"
101
+
102
+ [tool.pytest.ini_options]
103
+ asyncio_mode = "auto"
104
+ asyncio_default_fixture_loop_scope = "function"
105
+ testpaths = ["tests"]
106
+
107
+ [dependency-groups]
108
+ dev = [
109
+ "pytest>=8.4.2",
110
+ "pytest-asyncio>=0.24.0",
111
+ "pyyaml>=6",
112
+ "ruff>=0.13.1",
113
+ "ty>=0.0.1a1",
114
+ ]
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: todos
3
+ description: Manage work on a board. Create and decompose to-dos, move them through the fixed status lifecycle, and read board state.
4
+ metadata:
5
+ nimblebrain:
6
+ loading-strategy: dynamic
7
+ tool-affinity:
8
+ - create_todo
9
+ - plan_todos
10
+ - update_todo
11
+ - list_todos
12
+ - get_todo
13
+ - get_next_todo
14
+ - get_board_summary
15
+ - archive_todo
16
+ ---
17
+
18
+ # To-dos
19
+
20
+ Every tool returns a `next_step`; follow it.
21
+
22
+ ## Status moves only through the verb that names what happened
23
+
24
+ `update_todo` has no status. The five states (`open · in_progress · blocked · done ·
25
+ cancelled`) are a set, not a pipeline, and each move is its own verb, which records its own
26
+ side effects:
27
+
28
+ | What happened | Verb | Records |
29
+ |---|---|---|
30
+ | work began, or its blocker cleared | `start_todo` | clears any block |
31
+ | it is finished | `complete_todo` | `completed_at` |
32
+ | it is waiting on something | `block_todo` with a reason | `blocked_reason` |
33
+ | it is back to do | `reopen_todo` | clears `completed_at` and any block |
34
+ | it will not be done | `cancel_todo` (takes no reason) | `completed_at` |
35
+
36
+ A to-do can also start in any status: `create_todo` and `plan_todos` take `status`, with
37
+ the same side effects (`done` and `cancelled` set `completed_at`; `blocked` needs a
38
+ `blocked_reason`).
39
+
40
+ `update_todo` changes everything else: title, details, priority, labels, assignee, due
41
+ date, parent.
42
+
43
+ **Finished, won't do and out of sight are three different things.** `complete_todo` is
44
+ finished, `cancel_todo` is won't do, and `archive_todo` only hides a to-do from default
45
+ views with its status unchanged, and nothing un-archives it. Never archive to mean done.
46
+
47
+ ## Choosing the rest
48
+
49
+ - One to-do: `create_todo`. Several, or one goal broken into steps: `plan_todos`, which
50
+ adds to the board and never replaces it. Pass an `idempotency_key` so a retry converges.
51
+ - A result flagged as an error carries `error.code` (`not_found`, `invalid_request`,
52
+ `unauthenticated`, `rate_limited`, `internal_error`) and a `next_step`; follow it.
53
+ - "What should I do next": `get_next_todo`, rather than listing and re-deciding.
54
+ - Reporting on the board: start with `get_board_summary`, then name the to-dos behind its
55
+ counts (what is in progress, what is blocked and why) rather than the counts alone.
56
+
57
+ ## Naming a to-do
58
+
59
+ Every to-do has a key, its board's key and its number on that board (`MAG-3`). Name to-dos
60
+ to the person by key. Every argument that takes a to-do id takes the key too, so when the
61
+ person says "MAG-3", pass it as it is. Every `board_id` takes the board's key (`MAG`) too.
62
+ `create_board` picks a board's key, or takes one you pass; it cannot change later, so tell
63
+ the person which key the board got.
@@ -0,0 +1 @@
1
+ """To-dos MCP Server."""
@@ -0,0 +1,31 @@
1
+ """``python -m mcp_todos``: serve stdio on the runtime ``TODOS_RUNTIME`` names.
2
+
3
+ Logs go to stderr only; stdout carries nothing but JSON-RPC. Exits non-zero, before serving,
4
+ when ``TODOS_RUNTIME`` is unset or names no installed runtime.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import sys
11
+
12
+
13
+ def main() -> None:
14
+ logging.basicConfig(
15
+ level=logging.INFO,
16
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
17
+ stream=sys.stderr,
18
+ )
19
+ from mcp_todos import server
20
+ from mcp_todos.runtime import RuntimeSelectionError
21
+
22
+ try:
23
+ server.installed()
24
+ except RuntimeSelectionError as exc:
25
+ print(f"mcp-todos: {exc}", file=sys.stderr)
26
+ raise SystemExit(2) from exc
27
+ server.mcp.run()
28
+
29
+
30
+ if __name__ == "__main__":
31
+ main()
@@ -0,0 +1,30 @@
1
+ """To-dos database layer.
2
+
3
+ Re-exports the SQLAlchemy ``metadata`` and table objects so callers (store,
4
+ migrations, tests) can ``from mcp_todos.db import metadata, todo`` without
5
+ reaching into ``tables`` directly.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from mcp_todos.db.tables import (
11
+ board,
12
+ metadata,
13
+ todo,
14
+ todo_activity,
15
+ uq_board_default,
16
+ uq_board_key,
17
+ uq_todo_idem,
18
+ uq_todo_number,
19
+ )
20
+
21
+ __all__ = [
22
+ "metadata",
23
+ "board",
24
+ "todo",
25
+ "todo_activity",
26
+ "uq_board_default",
27
+ "uq_board_key",
28
+ "uq_todo_idem",
29
+ "uq_todo_number",
30
+ ]
@@ -0,0 +1,62 @@
1
+ """What the tables and store use where Postgres and SQLite differ.
2
+
3
+ The store runs on Postgres (with row-level security) and on SQLite (one principal). Each
4
+ helper here picks the dialect's form of one construct, so the store's queries read the same
5
+ on both.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from datetime import UTC, datetime
11
+ from typing import Any
12
+
13
+ from sqlalchemy import JSON, BigInteger, DateTime, Integer, Table, exists, func, select, type_coerce
14
+ from sqlalchemy.dialects import postgresql, sqlite
15
+ from sqlalchemy.engine import Dialect
16
+ from sqlalchemy.ext.asyncio import AsyncConnection
17
+ from sqlalchemy.types import TypeDecorator
18
+
19
+ __all__ = ["JSONType", "RowId", "UTCDateTime", "insert_for", "json_array_contains"]
20
+
21
+ #: JSONB on Postgres, JSON text on SQLite.
22
+ JSONType = JSON().with_variant(postgresql.JSONB(), "postgresql")
23
+
24
+ #: An auto-incrementing key: SQLite generates one only for an ``INTEGER PRIMARY KEY``.
25
+ RowId = BigInteger().with_variant(Integer(), "sqlite")
26
+
27
+
28
+ class UTCDateTime(TypeDecorator[datetime]):
29
+ """A timestamp that is timezone-aware, in UTC, on every backend.
30
+
31
+ ``timestamptz`` on Postgres. SQLite stores no zone, so the value is stored as UTC and the
32
+ zone re-attached on read. A naive value bound to it is taken as UTC.
33
+ """
34
+
35
+ impl = DateTime(timezone=True)
36
+ cache_ok = True
37
+
38
+ def process_bind_param(self, value: datetime | None, dialect: Dialect) -> datetime | None:
39
+ if value is None:
40
+ return None
41
+ value = value.replace(tzinfo=UTC) if value.tzinfo is None else value.astimezone(UTC)
42
+ return value.replace(tzinfo=None) if dialect.name == "sqlite" else value
43
+
44
+ def process_result_value(self, value: datetime | None, dialect: Dialect) -> datetime | None:
45
+ if value is not None and value.tzinfo is None:
46
+ return value.replace(tzinfo=UTC)
47
+ return value
48
+
49
+
50
+ def insert_for(conn: AsyncConnection, table: Table) -> Any:
51
+ """The dialect's ``INSERT``, which supports ``on_conflict_do_nothing`` with an index
52
+ predicate on both Postgres and SQLite."""
53
+ module = sqlite if conn.dialect.name == "sqlite" else postgresql
54
+ return module.insert(table)
55
+
56
+
57
+ def json_array_contains(conn: AsyncConnection, column: Any, value: str) -> Any:
58
+ """``column`` (a JSON array) holds ``value``: ``@>`` on Postgres, ``json_each`` on SQLite."""
59
+ if conn.dialect.name == "sqlite":
60
+ each = func.json_each(column).table_valued("value")
61
+ return exists(select(1).select_from(each).where(each.c.value == value))
62
+ return type_coerce(column, postgresql.JSONB()).contains([value])