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/cli.py
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import signal
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from .storage import default_home, get_db_path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
DEFAULT_HOST = "127.0.0.1"
|
|
16
|
+
DEFAULT_PORT = 8000
|
|
17
|
+
DEFAULT_SERVER_NAME = "MemTask"
|
|
18
|
+
PID_FILE = "memtask.pid"
|
|
19
|
+
LOG_FILE = "memtask.log"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _runtime_dir() -> Path:
|
|
23
|
+
path = default_home()
|
|
24
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
return path
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _pid_path() -> Path:
|
|
29
|
+
return _runtime_dir() / PID_FILE
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _log_path() -> Path:
|
|
33
|
+
return _runtime_dir() / LOG_FILE
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _pid_is_running(pid: int) -> bool:
|
|
37
|
+
if pid <= 0:
|
|
38
|
+
return False
|
|
39
|
+
try:
|
|
40
|
+
os.kill(pid, 0)
|
|
41
|
+
except ProcessLookupError:
|
|
42
|
+
return False
|
|
43
|
+
except PermissionError:
|
|
44
|
+
return True
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _read_pid_record() -> dict[str, object] | None:
|
|
49
|
+
path = _pid_path()
|
|
50
|
+
if not path.exists():
|
|
51
|
+
return None
|
|
52
|
+
try:
|
|
53
|
+
return json.loads(path.read_text())
|
|
54
|
+
except (OSError, json.JSONDecodeError):
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _write_pid_record(record: dict[str, object]) -> None:
|
|
59
|
+
_pid_path().write_text(json.dumps(record, indent=2, sort_keys=True))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _clear_pid_record() -> None:
|
|
63
|
+
try:
|
|
64
|
+
_pid_path().unlink()
|
|
65
|
+
except FileNotFoundError:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _mcp_url(host: str, port: int) -> str:
|
|
70
|
+
return f"http://{host}:{port}/mcp"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _stdio_config_text() -> str:
|
|
74
|
+
return json.dumps(
|
|
75
|
+
{
|
|
76
|
+
"mcpServers": {
|
|
77
|
+
"memtask": {
|
|
78
|
+
"command": "memtask",
|
|
79
|
+
"args": ["start", "--transport", "stdio"],
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
indent=2,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _http_config_text(host: str, port: int) -> str:
|
|
88
|
+
return json.dumps(
|
|
89
|
+
{
|
|
90
|
+
"mcpServers": {
|
|
91
|
+
"memtask": {
|
|
92
|
+
"url": _mcp_url(host, port),
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
indent=2,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _print_stdio_guidance() -> None:
|
|
101
|
+
print("MemTask starting over stdio.", file=sys.stderr)
|
|
102
|
+
print("For MCP clients that launch stdio servers, configure:", file=sys.stderr)
|
|
103
|
+
print(_stdio_config_text(), file=sys.stderr)
|
|
104
|
+
print(f"SQLite state: {get_db_path()}", file=sys.stderr)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _print_http_guidance(host: str, port: int, pid: int | None = None) -> None:
|
|
108
|
+
print("MemTask HTTP server started.")
|
|
109
|
+
if pid is not None:
|
|
110
|
+
print(f"PID: {pid}")
|
|
111
|
+
print(f"URL: {_mcp_url(host, port)}")
|
|
112
|
+
print(f"SQLite state: {get_db_path()}")
|
|
113
|
+
print(f"Log file: {_log_path()}")
|
|
114
|
+
print()
|
|
115
|
+
print("For MCP clients that connect to HTTP servers, configure:")
|
|
116
|
+
print(_http_config_text(host, port))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _print_install_help(host: str = DEFAULT_HOST, port: int = DEFAULT_PORT) -> None:
|
|
120
|
+
print("MemTask MCP configuration examples")
|
|
121
|
+
print()
|
|
122
|
+
print("Stdio launch configuration:")
|
|
123
|
+
print(_stdio_config_text())
|
|
124
|
+
print()
|
|
125
|
+
print("HTTP mode:")
|
|
126
|
+
print(f" memtask start --transport http --host {host} --port {port}")
|
|
127
|
+
print()
|
|
128
|
+
print("HTTP configuration:")
|
|
129
|
+
print(_http_config_text(host, port))
|
|
130
|
+
print()
|
|
131
|
+
print(f"Default SQLite state: {get_db_path()}")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _run_server(**kwargs) -> None:
|
|
135
|
+
from .app import run_server
|
|
136
|
+
|
|
137
|
+
run_server(**kwargs)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _start_stdio(args: argparse.Namespace) -> None:
|
|
141
|
+
_print_stdio_guidance()
|
|
142
|
+
_run_server(
|
|
143
|
+
transport="stdio",
|
|
144
|
+
host=args.host,
|
|
145
|
+
port=args.port,
|
|
146
|
+
server_name=args.server_name,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _start_http(args: argparse.Namespace) -> None:
|
|
151
|
+
record = _read_pid_record()
|
|
152
|
+
if record is not None:
|
|
153
|
+
pid = int(record.get("pid", 0))
|
|
154
|
+
if _pid_is_running(pid):
|
|
155
|
+
print("MemTask HTTP server is already running.")
|
|
156
|
+
print(f"PID: {pid}")
|
|
157
|
+
print(f"URL: {record.get('url', _mcp_url(args.host, args.port))}")
|
|
158
|
+
print(f"Stop it with: memtask stop")
|
|
159
|
+
return
|
|
160
|
+
_clear_pid_record()
|
|
161
|
+
|
|
162
|
+
log_handle = _log_path().open("ab")
|
|
163
|
+
command = [
|
|
164
|
+
sys.executable,
|
|
165
|
+
"-m",
|
|
166
|
+
"memtask.app",
|
|
167
|
+
"--transport",
|
|
168
|
+
"streamable-http",
|
|
169
|
+
"--host",
|
|
170
|
+
args.host,
|
|
171
|
+
"--port",
|
|
172
|
+
str(args.port),
|
|
173
|
+
"--server-name",
|
|
174
|
+
args.server_name,
|
|
175
|
+
]
|
|
176
|
+
popen_kwargs = {
|
|
177
|
+
"stdin": subprocess.DEVNULL,
|
|
178
|
+
"stdout": log_handle,
|
|
179
|
+
"stderr": log_handle,
|
|
180
|
+
"close_fds": True,
|
|
181
|
+
}
|
|
182
|
+
if os.name == "posix":
|
|
183
|
+
popen_kwargs["start_new_session"] = True
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
process = subprocess.Popen(command, **popen_kwargs)
|
|
187
|
+
finally:
|
|
188
|
+
log_handle.close()
|
|
189
|
+
time.sleep(0.2)
|
|
190
|
+
if process.poll() is not None:
|
|
191
|
+
raise RuntimeError(f"MemTask server exited during startup. See log: {_log_path()}")
|
|
192
|
+
|
|
193
|
+
record = {
|
|
194
|
+
"pid": process.pid,
|
|
195
|
+
"transport": "streamable-http",
|
|
196
|
+
"host": args.host,
|
|
197
|
+
"port": args.port,
|
|
198
|
+
"url": _mcp_url(args.host, args.port),
|
|
199
|
+
"log_file": str(_log_path()),
|
|
200
|
+
"db_path": str(get_db_path()),
|
|
201
|
+
"started_at": time.time(),
|
|
202
|
+
}
|
|
203
|
+
_write_pid_record(record)
|
|
204
|
+
_print_http_guidance(args.host, args.port, pid=process.pid)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _start(args: argparse.Namespace) -> None:
|
|
208
|
+
if args.transport == "stdio":
|
|
209
|
+
_start_stdio(args)
|
|
210
|
+
return
|
|
211
|
+
_start_http(args)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _stop(_: argparse.Namespace) -> None:
|
|
215
|
+
record = _read_pid_record()
|
|
216
|
+
if record is None:
|
|
217
|
+
print("No MemTask background server is registered.")
|
|
218
|
+
print("If you are using stdio mode, your MCP client starts and stops MemTask itself.")
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
pid = int(record.get("pid", 0))
|
|
222
|
+
if not _pid_is_running(pid):
|
|
223
|
+
_clear_pid_record()
|
|
224
|
+
print("Removed stale MemTask PID file.")
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
os.kill(pid, signal.SIGTERM)
|
|
228
|
+
deadline = time.time() + 5
|
|
229
|
+
while time.time() < deadline:
|
|
230
|
+
if not _pid_is_running(pid):
|
|
231
|
+
break
|
|
232
|
+
time.sleep(0.1)
|
|
233
|
+
|
|
234
|
+
_clear_pid_record()
|
|
235
|
+
print("MemTask HTTP server stopped.")
|
|
236
|
+
print("For stdio MCP clients, no stop command is needed; the client owns the server process.")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _status(_: argparse.Namespace) -> None:
|
|
240
|
+
record = _read_pid_record()
|
|
241
|
+
if record is None:
|
|
242
|
+
print("MemTask background HTTP server is not running.")
|
|
243
|
+
print(f"SQLite state: {get_db_path()}")
|
|
244
|
+
print("For MCP client setup, run: memtask install-help")
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
pid = int(record.get("pid", 0))
|
|
248
|
+
if not _pid_is_running(pid):
|
|
249
|
+
_clear_pid_record()
|
|
250
|
+
print("MemTask background HTTP server is not running. Removed stale PID file.")
|
|
251
|
+
print(f"SQLite state: {get_db_path()}")
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
print("MemTask background HTTP server is running.")
|
|
255
|
+
print(f"PID: {pid}")
|
|
256
|
+
print(f"URL: {record.get('url')}")
|
|
257
|
+
print(f"SQLite state: {record.get('db_path', get_db_path())}")
|
|
258
|
+
print(f"Log file: {record.get('log_file', _log_path())}")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
262
|
+
parser = argparse.ArgumentParser(prog="memtask")
|
|
263
|
+
subcommands = parser.add_subparsers(dest="command", required=True)
|
|
264
|
+
|
|
265
|
+
start = subcommands.add_parser("start", help="Start the MemTask MCP server")
|
|
266
|
+
start.add_argument(
|
|
267
|
+
"--transport",
|
|
268
|
+
choices=("stdio", "http", "streamable-http"),
|
|
269
|
+
default="stdio",
|
|
270
|
+
help="Use stdio in the foreground or HTTP as a background server.",
|
|
271
|
+
)
|
|
272
|
+
start.add_argument("--host", default=DEFAULT_HOST)
|
|
273
|
+
start.add_argument("--port", type=int, default=DEFAULT_PORT)
|
|
274
|
+
start.add_argument("--server-name", default=DEFAULT_SERVER_NAME)
|
|
275
|
+
start.set_defaults(handler=_start)
|
|
276
|
+
|
|
277
|
+
stop = subcommands.add_parser("stop", help="Stop the background HTTP server")
|
|
278
|
+
stop.set_defaults(handler=_stop)
|
|
279
|
+
|
|
280
|
+
status = subcommands.add_parser("status", help="Show background server status")
|
|
281
|
+
status.set_defaults(handler=_status)
|
|
282
|
+
|
|
283
|
+
install_help = subcommands.add_parser(
|
|
284
|
+
"install-help",
|
|
285
|
+
help="Print MCP client configuration examples",
|
|
286
|
+
)
|
|
287
|
+
install_help.add_argument("--host", default=DEFAULT_HOST)
|
|
288
|
+
install_help.add_argument("--port", type=int, default=DEFAULT_PORT)
|
|
289
|
+
install_help.set_defaults(
|
|
290
|
+
handler=lambda args: _print_install_help(host=args.host, port=args.port)
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
return parser
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def main(argv: list[str] | None = None) -> None:
|
|
297
|
+
parser = build_parser()
|
|
298
|
+
args = parser.parse_args(argv)
|
|
299
|
+
args.handler(args)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
if __name__ == "__main__":
|
|
303
|
+
main()
|