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 +5 -0
- memtask/__main__.py +5 -0
- memtask/api.py +328 -0
- memtask/app.py +73 -0
- memtask/cli.py +303 -0
- memtask/manager.py +744 -0
- memtask/storage.py +150 -0
- memtask-0.0.1.dist-info/METADATA +104 -0
- memtask-0.0.1.dist-info/RECORD +12 -0
- memtask-0.0.1.dist-info/WHEEL +5 -0
- memtask-0.0.1.dist-info/entry_points.txt +2 -0
- memtask-0.0.1.dist-info/top_level.txt +1 -0
memtask/__init__.py
ADDED
memtask/__main__.py
ADDED
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()
|