memtask 0.0.1__py3-none-any.whl

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.
memtask/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Memory-backed task manager MCP server."""
2
+
3
+ __all__ = ["__version__"]
4
+
5
+ __version__ = "0.0.1"
memtask/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ main()
memtask/api.py ADDED
@@ -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
memtask/app.py ADDED
@@ -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()