memtask 0.0.1__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.
@@ -0,0 +1,2 @@
1
+ include README.md
2
+ include docs/assets/memtask-logo.png
memtask-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,104 @@
1
+ Metadata-Version: 2.4
2
+ Name: memtask
3
+ Version: 0.0.1
4
+ Summary: A local MCP server for durable agent task state and lightweight memory.
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: mcp
8
+ Provides-Extra: test
9
+ Requires-Dist: pytest>=8; extra == "test"
10
+
11
+ <p align="center">
12
+ <img src="docs/assets/memtask-logo.png" alt="MemTask logo" width="220">
13
+ </p>
14
+
15
+ <h1 align="center">MemTask</h1>
16
+
17
+ MemTask is a local MCP server for developer-built agents that need durable task state and lightweight memory. It combines task planning, dependency tracking, active work selection, completion history, and scoped memories in a small SQLite-backed service. The goal is to give agents a structured place to manage agency: what to do next, what depends on what, and what context should persist across sessions.
18
+
19
+ ## What It Provides
20
+
21
+ MemTask exposes two local primitives over MCP:
22
+
23
+ - Tasks: pending work, stable task references, parent/child dependency edges, active task selection, and completed task state.
24
+ - Memories: scoped pieces of context with confidence scores, optional parent memory relationships, tags, and task-memory references.
25
+
26
+ The server is intentionally local-first. State lives in SQLite, the tool surface is small, and the manager layer can be tested directly without running an MCP transport.
27
+
28
+ ## Quickstart
29
+
30
+ Install MemTask:
31
+
32
+ ```bash
33
+ pip install MemTask
34
+ ```
35
+
36
+ Then ask MemTask for the MCP config to add to your agent:
37
+
38
+ ```bash
39
+ memtask install-help
40
+ ```
41
+
42
+ Most MCP clients should launch MemTask over stdio:
43
+
44
+ ```bash
45
+ memtask start --transport stdio
46
+ ```
47
+
48
+ If your client connects to a running HTTP server instead, start it in the background:
49
+
50
+ ```bash
51
+ memtask start --transport http --host 127.0.0.1 --port 8000
52
+ ```
53
+
54
+ For local development from this repo:
55
+
56
+ ```bash
57
+ PYTHONPATH=src python -m memtask start --transport stdio
58
+ ```
59
+
60
+ Useful HTTP commands: `memtask status` and `memtask stop`.
61
+
62
+ ## Storage
63
+
64
+ Runtime state in this repo is stored in `data/tasks.sqlite`.
65
+
66
+ When installed outside this repo, MemTask uses `~/.memtask/tasks.sqlite` by default. Set `MEMTASK_DB_PATH` to choose a specific SQLite path.
67
+
68
+ The server creates the required SQLite tables on startup using `CREATE TABLE IF NOT EXISTS`. There is no migration framework.
69
+
70
+ ## Tools
71
+
72
+ Task tools:
73
+
74
+ - `list_tasks`
75
+ - `get_task`
76
+ - `add_task`
77
+ - `add_batch_tasks`
78
+ - `complete_task`
79
+ - `remove_task`
80
+ - `remove_all_tasks`
81
+ - `current_tasks`
82
+ - `set_current_task`
83
+
84
+ Memory tools:
85
+
86
+ - `remember`
87
+ - `recall`
88
+ - `get_memory`
89
+ - `update_memory`
90
+ - `delete_memory`
91
+
92
+ ## Development
93
+
94
+ Run tests:
95
+
96
+ ```bash
97
+ python -m pytest
98
+ ```
99
+
100
+ Compile-check the package:
101
+
102
+ ```bash
103
+ python -m py_compile src/memtask/*.py tests/*.py
104
+ ```
@@ -0,0 +1,94 @@
1
+ <p align="center">
2
+ <img src="docs/assets/memtask-logo.png" alt="MemTask logo" width="220">
3
+ </p>
4
+
5
+ <h1 align="center">MemTask</h1>
6
+
7
+ MemTask is a local MCP server for developer-built agents that need durable task state and lightweight memory. It combines task planning, dependency tracking, active work selection, completion history, and scoped memories in a small SQLite-backed service. The goal is to give agents a structured place to manage agency: what to do next, what depends on what, and what context should persist across sessions.
8
+
9
+ ## What It Provides
10
+
11
+ MemTask exposes two local primitives over MCP:
12
+
13
+ - Tasks: pending work, stable task references, parent/child dependency edges, active task selection, and completed task state.
14
+ - Memories: scoped pieces of context with confidence scores, optional parent memory relationships, tags, and task-memory references.
15
+
16
+ The server is intentionally local-first. State lives in SQLite, the tool surface is small, and the manager layer can be tested directly without running an MCP transport.
17
+
18
+ ## Quickstart
19
+
20
+ Install MemTask:
21
+
22
+ ```bash
23
+ pip install MemTask
24
+ ```
25
+
26
+ Then ask MemTask for the MCP config to add to your agent:
27
+
28
+ ```bash
29
+ memtask install-help
30
+ ```
31
+
32
+ Most MCP clients should launch MemTask over stdio:
33
+
34
+ ```bash
35
+ memtask start --transport stdio
36
+ ```
37
+
38
+ If your client connects to a running HTTP server instead, start it in the background:
39
+
40
+ ```bash
41
+ memtask start --transport http --host 127.0.0.1 --port 8000
42
+ ```
43
+
44
+ For local development from this repo:
45
+
46
+ ```bash
47
+ PYTHONPATH=src python -m memtask start --transport stdio
48
+ ```
49
+
50
+ Useful HTTP commands: `memtask status` and `memtask stop`.
51
+
52
+ ## Storage
53
+
54
+ Runtime state in this repo is stored in `data/tasks.sqlite`.
55
+
56
+ When installed outside this repo, MemTask uses `~/.memtask/tasks.sqlite` by default. Set `MEMTASK_DB_PATH` to choose a specific SQLite path.
57
+
58
+ The server creates the required SQLite tables on startup using `CREATE TABLE IF NOT EXISTS`. There is no migration framework.
59
+
60
+ ## Tools
61
+
62
+ Task tools:
63
+
64
+ - `list_tasks`
65
+ - `get_task`
66
+ - `add_task`
67
+ - `add_batch_tasks`
68
+ - `complete_task`
69
+ - `remove_task`
70
+ - `remove_all_tasks`
71
+ - `current_tasks`
72
+ - `set_current_task`
73
+
74
+ Memory tools:
75
+
76
+ - `remember`
77
+ - `recall`
78
+ - `get_memory`
79
+ - `update_memory`
80
+ - `delete_memory`
81
+
82
+ ## Development
83
+
84
+ Run tests:
85
+
86
+ ```bash
87
+ python -m pytest
88
+ ```
89
+
90
+ Compile-check the package:
91
+
92
+ ```bash
93
+ python -m py_compile src/memtask/*.py tests/*.py
94
+ ```
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "memtask"
7
+ version = "0.0.1"
8
+ description = "A local MCP server for durable agent task state and lightweight memory."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ "mcp",
13
+ ]
14
+
15
+ [project.optional-dependencies]
16
+ test = [
17
+ "pytest>=8",
18
+ ]
19
+
20
+ [project.scripts]
21
+ memtask = "memtask.cli:main"
22
+
23
+ [tool.setuptools.packages.find]
24
+ where = ["src"]
25
+
26
+ [tool.pytest.ini_options]
27
+ pythonpath = ["src"]
28
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """Memory-backed task manager MCP server."""
2
+
3
+ __all__ = ["__version__"]
4
+
5
+ __version__ = "0.0.1"
@@ -0,0 +1,5 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -0,0 +1,328 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ from . import manager
7
+
8
+
9
+ SERVER_INSTRUCTIONS = (
10
+ "Prefer `task_ref` over numeric task ids for follow-up calls. "
11
+ "Numeric ids can change after task mutations because pending tasks are displayed with ephemeral ids."
12
+ )
13
+ HELP_OVERVIEW = """# Local MCP Help
14
+
15
+ This server exposes a local SQLite-backed task and memory manager.
16
+
17
+ ## Core Rule
18
+
19
+ Prefer `task_ref` for all follow-up task calls. It is the stable UUID alias returned by task-listing and task-fetching calls.
20
+ Use `pending_id` only as a temporary display id for humans.
21
+
22
+ ## Common Workflow
23
+
24
+ 1. Call `list_tasks`
25
+ 2. Select a task's `task_ref`
26
+ 3. Call `set_current_task(task_ref)` to start work
27
+ 4. Call `complete_task(task_ref)` when finished
28
+ 5. Call `remember(...)` to persist useful context
29
+ 6. Recall with `recall(...)` and attach results with `memory_refs` when adding follow-up tasks
30
+
31
+ ## Notes
32
+
33
+ - `current_tasks` only shows started pending tasks
34
+ - `complete_task` moves a task out of the pending list, so numeric ids can shift afterward
35
+ - `add_task` and `add_batch_tasks` return the new task records; capture their `task_ref` immediately if you plan to mutate them later
36
+ - `remove_all_tasks` deletes every currently listed pending task and is destructive.
37
+ - `remember` stores context with `memory_scope`, `kind`, and `confidence` (0-100)
38
+ - `recall` supports filtering by `memory_scope`, `kind`, minimum confidence, and text search via `query`
39
+
40
+ ## Dependency Model
41
+
42
+ Tasks can optionally include `parent_task_refs` when created. Those are dependency edges used by the
43
+ manager for completion checks.
44
+
45
+ ## Memory Graph
46
+
47
+ Memories can be organized with a single `parent_memory_id`.
48
+ Tasks can couple to memory via `memory_refs` for persistent context.
49
+ """.strip()
50
+
51
+
52
+ def _json_text(value: object) -> str:
53
+ return json.dumps(value, indent=2, sort_keys=True, default=str)
54
+
55
+
56
+ def _best_practice_for_tool(name: str) -> str | None:
57
+ if name in {"get_task", "set_current_task", "complete_task", "remove_task"}:
58
+ return "Prefer `task_ref`/UUID over numeric ids because pending ids can change after task mutations."
59
+ if name in {"list_tasks", "current_tasks"}:
60
+ return "Use this call to discover stable `task_ref` values before follow-up mutations."
61
+ if name in {"add_task", "add_batch_tasks"}:
62
+ return "Capture the returned `task_ref` values immediately if you will mutate these tasks later."
63
+ if name in {"remember", "recall", "update_memory"}:
64
+ return "Use `memory_scope` for context boundaries and `confidence` to represent belief strength."
65
+ if name == "remove_all_tasks":
66
+ return "This tool is destructive and removes all pending tasks returned by `list_tasks`."
67
+ return None
68
+
69
+
70
+ def _example_for_tool(name: str) -> dict[str, Any] | list[dict[str, Any]] | None:
71
+ if name == "list_tasks":
72
+ return {}
73
+ if name == "current_tasks":
74
+ return {}
75
+ if name == "get_task":
76
+ return {"task_id": "task_ref-from-list_tasks"}
77
+ if name == "add_task":
78
+ return {"description": "Write release notes", "project": "ops", "tags": ["docs"]}
79
+ if name == "add_batch_tasks":
80
+ return {
81
+ "descriptions": ["Draft agenda", "Send recap"],
82
+ "project": "meetings",
83
+ "tags": ["team"],
84
+ "parent_task_refs": ["parent-task-ref"],
85
+ }
86
+ if name == "remember":
87
+ return {
88
+ "content": "Release notes should include API migration commands first.",
89
+ "memory_scope": "projects",
90
+ "kind": "fact",
91
+ "confidence": 90,
92
+ }
93
+ if name == "recall":
94
+ return {
95
+ "query": "release notes",
96
+ "memory_scope": "projects",
97
+ "min_confidence": 70,
98
+ "limit": 5,
99
+ }
100
+ if name == "get_memory":
101
+ return {"memory_id": "memory-ref"}
102
+ if name == "update_memory":
103
+ return {
104
+ "memory_id": "memory-ref",
105
+ "confidence": 95,
106
+ "tags": ["evidence", "release"],
107
+ }
108
+ if name == "delete_memory":
109
+ return {"memory_id": "memory-ref"}
110
+ if name == "set_current_task":
111
+ return {"task_id": "task_ref-from-list_tasks"}
112
+ if name == "complete_task":
113
+ return {"task_id": "task_ref-from-current_tasks"}
114
+ if name == "remove_task":
115
+ return {"task_id": "task_ref-to-delete"}
116
+ if name == "remove_all_tasks":
117
+ return {}
118
+ return None
119
+
120
+
121
+ async def _tool_help_payload(mcp: Any, name: str) -> dict[str, Any]:
122
+ tools = await mcp.list_tools()
123
+ for tool in tools:
124
+ if tool.name != name:
125
+ continue
126
+
127
+ payload: dict[str, Any] = {
128
+ "name": tool.name,
129
+ "description": tool.description,
130
+ "input_schema": tool.inputSchema,
131
+ }
132
+ best_practice = _best_practice_for_tool(tool.name)
133
+ if best_practice is not None:
134
+ payload["best_practice"] = best_practice
135
+ example = _example_for_tool(tool.name)
136
+ if example is not None:
137
+ payload["example_arguments"] = example
138
+ return payload
139
+
140
+ raise ValueError(f"Unknown tool: {name}")
141
+
142
+
143
+ def register(mcp: Any, manager_module: Any = manager) -> Any:
144
+ @mcp.resource(
145
+ "help://overview",
146
+ name="help_overview",
147
+ title="Local MCP Overview",
148
+ description="Overview, invariants, and common workflows for the local task manager MCP server.",
149
+ mime_type="text/markdown",
150
+ )
151
+ def help_overview() -> str:
152
+ return HELP_OVERVIEW
153
+
154
+ @mcp.resource(
155
+ "help://tools",
156
+ name="help_tools",
157
+ title="Local MCP Tool Catalog",
158
+ description="Live catalog of task-manager tools with best-practice notes.",
159
+ mime_type="application/json",
160
+ )
161
+ async def help_tools() -> str:
162
+ tools = await mcp.list_tools()
163
+ payload = []
164
+ for tool in tools:
165
+ payload.append(await _tool_help_payload(mcp, tool.name))
166
+ return _json_text(payload)
167
+
168
+ @mcp.resource(
169
+ "help://tool/{name}",
170
+ name="help_tool",
171
+ title="Local MCP Tool Help",
172
+ description="Detailed help for a specific task-manager tool.",
173
+ mime_type="application/json",
174
+ )
175
+ async def help_tool(name: str) -> str:
176
+ return _json_text(await _tool_help_payload(mcp, name))
177
+
178
+ @mcp.tool()
179
+ def list_tasks() -> str:
180
+ """List pending tasks from Agent Task Manager with stable `task_ref` values."""
181
+ return _json_text(manager_module.list_tasks())
182
+
183
+ @mcp.tool()
184
+ def get_task(task_id: str) -> str:
185
+ """Get a single task by task_ref/UUID or pending numeric id. UUID is preferred."""
186
+ return _json_text(manager_module.get_task(task_id))
187
+
188
+ @mcp.tool()
189
+ def add_task(
190
+ description: str,
191
+ project: str | None = None,
192
+ tags: list[str] | None = None,
193
+ parent_task_refs: list[str] | None = None,
194
+ memory_refs: list[str] | None = None,
195
+ ) -> str:
196
+ """Add a new task to Agent Task Manager."""
197
+ return _json_text(
198
+ manager_module.add_task(
199
+ description=description,
200
+ project=project,
201
+ tags=tags,
202
+ parent_task_refs=parent_task_refs,
203
+ memory_refs=memory_refs,
204
+ )
205
+ )
206
+
207
+ @mcp.tool()
208
+ def add_batch_tasks(
209
+ descriptions: list[str],
210
+ project: str | None = None,
211
+ tags: list[str] | None = None,
212
+ parent_task_refs: list[str] | None = None,
213
+ memory_refs: list[str] | None = None,
214
+ ) -> str:
215
+ """Add multiple tasks to Agent Task Manager."""
216
+ return _json_text(
217
+ manager_module.add_batch_tasks(
218
+ descriptions=descriptions,
219
+ project=project,
220
+ tags=tags,
221
+ parent_task_refs=parent_task_refs,
222
+ memory_refs=memory_refs,
223
+ )
224
+ )
225
+
226
+ @mcp.tool()
227
+ def complete_task(task_id: str) -> str:
228
+ """Complete a task by task_ref/UUID or pending numeric id. UUID is preferred."""
229
+ return _json_text(manager_module.complete_task(task_id))
230
+
231
+ @mcp.tool()
232
+ def remove_task(task_id: str) -> str:
233
+ """Delete a task by task_ref/UUID or pending numeric id. UUID is preferred."""
234
+ return _json_text(manager_module.remove_task(task_id))
235
+
236
+ @mcp.tool()
237
+ def remove_all_tasks() -> str:
238
+ """Delete all pending tasks currently returned by `list_tasks`."""
239
+ return _json_text(manager_module.remove_all_tasks())
240
+
241
+ @mcp.tool()
242
+ def current_tasks() -> str:
243
+ """List currently active tasks with stable `task_ref` values."""
244
+ return _json_text(manager_module.current_tasks())
245
+
246
+ @mcp.tool()
247
+ def set_current_task(task_id: str) -> str:
248
+ """Stop active tasks and mark the specified task as current. UUID is preferred."""
249
+ return _json_text(manager_module.set_current_task(task_id))
250
+
251
+ @mcp.tool()
252
+ def remember(
253
+ content: str,
254
+ memory_scope: str | None = None,
255
+ kind: str | None = None,
256
+ confidence: int = 100,
257
+ parent_memory_id: str | None = None,
258
+ tags: list[str] | None = None,
259
+ ) -> str:
260
+ """Store a memory artifact with optional scope, kind, and confidence."""
261
+ return _json_text(
262
+ manager_module.remember(
263
+ content=content,
264
+ memory_scope=memory_scope,
265
+ kind=kind,
266
+ confidence=confidence,
267
+ parent_memory_id=parent_memory_id,
268
+ tags=tags,
269
+ )
270
+ )
271
+
272
+ @mcp.tool()
273
+ def recall(
274
+ query: str | None = None,
275
+ memory_scope: str | None = None,
276
+ kind: str | None = None,
277
+ min_confidence: int | None = None,
278
+ parent_memory_id: str | None = None,
279
+ limit: int | None = None,
280
+ ) -> str:
281
+ """Recall memory records by scope, kind, and confidence."""
282
+ return _json_text(
283
+ manager_module.recall(
284
+ query=query,
285
+ memory_scope=memory_scope,
286
+ kind=kind,
287
+ min_confidence=min_confidence,
288
+ parent_memory_id=parent_memory_id,
289
+ limit=limit,
290
+ )
291
+ )
292
+
293
+ @mcp.tool()
294
+ def get_memory(memory_id: str) -> str:
295
+ """Get a memory by memory_id."""
296
+ return _json_text(manager_module.get_memory(memory_id))
297
+
298
+ @mcp.tool()
299
+ def update_memory(
300
+ memory_id: str,
301
+ content: str | None = None,
302
+ memory_scope: str | None = None,
303
+ kind: str | None = None,
304
+ confidence: int | None = None,
305
+ parent_memory_id: str | None = None,
306
+ clear_parent: bool = False,
307
+ tags: list[str] | None = None,
308
+ ) -> str:
309
+ """Update a memory with optional field replacements."""
310
+ return _json_text(
311
+ manager_module.update_memory(
312
+ memory_id=memory_id,
313
+ content=content,
314
+ memory_scope=memory_scope,
315
+ kind=kind,
316
+ confidence=confidence,
317
+ parent_memory_id=parent_memory_id,
318
+ clear_parent=clear_parent,
319
+ tags=tags,
320
+ )
321
+ )
322
+
323
+ @mcp.tool()
324
+ def delete_memory(memory_id: str) -> str:
325
+ """Delete a memory by memory_id."""
326
+ return _json_text(manager_module.delete_memory(memory_id))
327
+
328
+ return mcp
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from typing import Any
5
+
6
+ from .api import SERVER_INSTRUCTIONS, register
7
+
8
+
9
+ def _load_mcp() -> Any:
10
+ try:
11
+ from mcp.server.fastmcp import FastMCP
12
+
13
+ return FastMCP
14
+ except ModuleNotFoundError as exc:
15
+ raise RuntimeError(
16
+ "Missing dependency: 'mcp'. Install it in the active Python environment "
17
+ "(for example: pip install mcp) before running this server."
18
+ ) from exc
19
+
20
+
21
+ def create_mcp(server_name: str = "Mini Demo Server") -> Any:
22
+ FastMCP = _load_mcp()
23
+ mcp = FastMCP(name=server_name, instructions=SERVER_INSTRUCTIONS)
24
+ return register(mcp)
25
+
26
+
27
+ def run_server(
28
+ transport: str = "stdio",
29
+ host: str = "127.0.0.1",
30
+ port: int = 8000,
31
+ server_name: str = "Mini Demo Server",
32
+ ) -> None:
33
+ mcp = create_mcp(server_name=server_name)
34
+
35
+ if transport == "stdio":
36
+ mcp.run()
37
+ return
38
+
39
+ if transport != "streamable-http":
40
+ raise ValueError(f"Unsupported transport: {transport}")
41
+
42
+ mcp.settings.host = host
43
+ mcp.settings.port = port
44
+ mcp.settings.json_response = True
45
+ mcp.run(transport="streamable-http")
46
+
47
+
48
+ def main() -> None:
49
+ parser = argparse.ArgumentParser()
50
+ parser.add_argument(
51
+ "--transport",
52
+ choices=("stdio", "streamable-http"),
53
+ default="stdio",
54
+ )
55
+ parser.add_argument("--host", default="127.0.0.1")
56
+ parser.add_argument("--port", type=int, default=8000)
57
+ parser.add_argument(
58
+ "--server-name",
59
+ default="Mini Demo Server",
60
+ help="Override the MCP server name",
61
+ )
62
+ args = parser.parse_args()
63
+
64
+ run_server(
65
+ transport=args.transport,
66
+ host=args.host,
67
+ port=args.port,
68
+ server_name=args.server_name,
69
+ )
70
+
71
+
72
+ if __name__ == "__main__":
73
+ main()