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.
- todosforagents-0.1.0/.gitignore +14 -0
- todosforagents-0.1.0/LICENSE +22 -0
- todosforagents-0.1.0/PKG-INFO +123 -0
- todosforagents-0.1.0/README.md +95 -0
- todosforagents-0.1.0/hatch_build.py +32 -0
- todosforagents-0.1.0/pyproject.toml +114 -0
- todosforagents-0.1.0/src/mcp_todos/SKILL.md +63 -0
- todosforagents-0.1.0/src/mcp_todos/__init__.py +1 -0
- todosforagents-0.1.0/src/mcp_todos/__main__.py +31 -0
- todosforagents-0.1.0/src/mcp_todos/db/__init__.py +30 -0
- todosforagents-0.1.0/src/mcp_todos/db/dialect.py +62 -0
- todosforagents-0.1.0/src/mcp_todos/db/store.py +1731 -0
- todosforagents-0.1.0/src/mcp_todos/db/tables.py +223 -0
- todosforagents-0.1.0/src/mcp_todos/icon.png +0 -0
- todosforagents-0.1.0/src/mcp_todos/ids.py +35 -0
- todosforagents-0.1.0/src/mcp_todos/keys.py +129 -0
- todosforagents-0.1.0/src/mcp_todos/models.py +273 -0
- todosforagents-0.1.0/src/mcp_todos/runtime.py +180 -0
- todosforagents-0.1.0/src/mcp_todos/server.py +1241 -0
- todosforagents-0.1.0/src/mcp_todos/ui.py +53 -0
- todosforagents-0.1.0/src/mcp_todos_local/__init__.py +5 -0
- todosforagents-0.1.0/src/mcp_todos_local/__main__.py +81 -0
- todosforagents-0.1.0/src/mcp_todos_local/auth.py +99 -0
- todosforagents-0.1.0/src/mcp_todos_local/config.py +42 -0
- todosforagents-0.1.0/src/mcp_todos_local/http.py +72 -0
- todosforagents-0.1.0/src/mcp_todos_local/migrations/env.py +16 -0
- todosforagents-0.1.0/src/mcp_todos_local/migrations/script.py.mako +27 -0
- todosforagents-0.1.0/src/mcp_todos_local/migrations/versions/0001_initial.py +125 -0
- todosforagents-0.1.0/src/mcp_todos_local/migrations/versions/0002_board_keys.py +82 -0
- todosforagents-0.1.0/src/mcp_todos_local/runtime.py +100 -0
- todosforagents-0.1.0/src/mcp_todos_local/schema.py +87 -0
- todosforagents-0.1.0/src/mcp_todos_local/sqlite.py +79 -0
- 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])
|