records-mcp 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.
@@ -0,0 +1,112 @@
1
+ # Project-specific
2
+ .worktrees/
3
+ .worktree/
4
+ .assets
5
+ .docs
6
+ .env
7
+ .web
8
+ .orion
9
+ *.zip
10
+
11
+ # Claude / AI assistant artifacts
12
+ .superpowers/
13
+ docs/superpowers/
14
+ docs/plans/
15
+
16
+ # webui (monorepo frontend)
17
+ webui/node_modules/
18
+ webui/dist/
19
+ webui/coverage/
20
+ webui/.vite/
21
+ webui/public/brand/
22
+ *.tsbuildinfo
23
+
24
+ # WhatsApp bridge frontend/backend helper
25
+ bridge/node_modules/
26
+ bridge/dist/
27
+
28
+ # Python bytecode & caches
29
+ *.pyc
30
+ *.pyo
31
+ *.pyd
32
+ *.pyw
33
+ *.pyz
34
+ __pycache__/
35
+ *.egg-info/
36
+ *.egg
37
+ .venv/
38
+ venv/
39
+ .pytest_cache/
40
+ .mypy_cache/
41
+ .ruff_cache/
42
+ .pytype/
43
+ .dmypy.json
44
+ dmypy.json
45
+ .tox/
46
+ .nox/
47
+ .hypothesis/
48
+
49
+ # Build & packaging
50
+ dist/
51
+ dist-build-*/
52
+ build/
53
+ installer/tools/
54
+ *.manifest
55
+ *.spec
56
+ !Nanobot.spec
57
+ pip-wheel-metadata/
58
+ share/python-wheels/
59
+
60
+ # Test & coverage
61
+ .coverage
62
+ .coverage.*
63
+ htmlcov/
64
+ coverage.xml
65
+ *.cover
66
+
67
+ # Lock files (project policy)
68
+ poetry.lock
69
+ uv.lock
70
+
71
+ # Jupyter
72
+ .ipynb_checkpoints/
73
+
74
+ # macOS
75
+ .DS_Store
76
+ .AppleDouble
77
+ .LSOverride
78
+
79
+ # Windows
80
+ Thumbs.db
81
+ ehthumbs.db
82
+ Desktop.ini
83
+
84
+ # Linux
85
+ .directory
86
+
87
+ # Editors & IDEs (local workspace / user settings)
88
+ .vscode/
89
+ .cursor/
90
+ .idea/
91
+ .fleet/
92
+ *.code-workspace
93
+ *.sublime-project
94
+ *.sublime-workspace
95
+ *.swp
96
+ *.swo
97
+ *~
98
+ nano.*.save
99
+
100
+ # Environment & secrets (keep examples tracked if needed)
101
+ .env.*
102
+ !.env.example
103
+
104
+ # Logs & temp
105
+ *.log
106
+ logs/
107
+ tmp/
108
+ temp/
109
+ *.tmp
110
+
111
+ # Mini Harness demo outputs(运行产物,不提交)
112
+ demo_outputs/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nanobot Team
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.
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: records-mcp
3
+ Version: 0.1.0
4
+ Summary: SQLite CRUD MCP server for teaching — exposes records database via MCP protocol
5
+ Author: Nanobot Team
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Keywords: mcp,records,sqlite,teaching
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Requires-Python: >=3.11
16
+ Requires-Dist: mcp>=1.0.0
17
+ Description-Content-Type: text/markdown
18
+
19
+ # records-mcp
20
+
21
+ SQLite CRUD MCP server for teaching. Exposes a records database via the MCP protocol.
22
+
23
+ ## Tools
24
+
25
+ | Tool | Description |
26
+ |---|---|
27
+ | `get_database_overview` | Record counts, status distribution, models, devices, and time range |
28
+ | `search_records` | Search records with optional filters |
29
+ | `get_record` | Get one record by ID |
30
+ | `create_record` | Create a new record |
31
+ | `update_record` | Update an existing record |
32
+ | `delete_record` | Delete a record |
33
+
34
+ ## Usage
35
+
36
+ ```bash
37
+ # stdio mode (default, for MCP client)
38
+ uvx records-mcp --db records.db --transport stdio
39
+
40
+ # SSE mode (network server)
41
+ uvx records-mcp --db records.db --transport sse --host 0.0.0.0 --port 8766
42
+
43
+ # streamable-http mode
44
+ uvx records-mcp --db records.db --transport streamable-http --host 0.0.0.0 --port 8766
45
+ ```
46
+
47
+ ## mcp_servers.json
48
+
49
+ ```json
50
+ {
51
+ "mcpServers": {
52
+ "records": {
53
+ "enabled": true,
54
+ "type": "stdio",
55
+ "command": "uvx",
56
+ "args": ["records-mcp", "--db", "records.db", "--transport", "stdio"]
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ ## 发布与更新版本
63
+
64
+ > PyPI **不允许重复上传同一版本号**:传过的版本号(如 `0.1.0`)被永久占用,传错或想改代码只能 bump 到新版本号,不能覆盖。所以每次发布前先改版本号。
65
+
66
+ ### 一次性配置
67
+
68
+ 1. 账号开启 **2FA**(认证器 App)——PyPI 强制:没开 2FA 的账号**禁止上传**,会报 `403 Invalid API Token: current user does not match user restriction`。
69
+ 2. 在 https://pypi.org/manage/account/token/ 建一个 **API token**,scope 选 `Entire account`。token 只显示一次,记好。
70
+ 3.(可选)把凭证写进 `~/.pypirc`,省得每次输:
71
+
72
+ ```ini
73
+ [pypi]
74
+ username = __token__
75
+ password = pypi-你的token
76
+ ```
77
+
78
+ > 别把 `.pypirc` 提交进 git。
79
+
80
+ ### 发新版本流程
81
+
82
+ ```bash
83
+ # 1. 改版本号:编辑 pyproject.toml,version = "0.1.0" → 例如 "0.1.1"
84
+ # (语义化版本:修 bug → 0.1.x,加功能 → 0.x.0,不兼容改动 → x.0.0)
85
+
86
+ # 2. 清掉旧产物,避免把旧 wheel / 旧 sdist 一起传上去
87
+ rm -rf dist build *.egg-info
88
+
89
+ # 3. 构建 wheel + sdist
90
+ python -m build # 或 uv build
91
+
92
+ # 4. 检查 metadata 没问题(期望两条 PASSED)
93
+ twine check dist/*
94
+
95
+ # 5.(可选)先发 TestPyPI 验证一遍,确认无误再发真的
96
+ twine upload --repository-url https://test.pypi.org/legacy/ dist/*
97
+
98
+ # 6. 发到真 PyPI
99
+ twine upload dist/* # 或:uv publish --token <token> dist/*
100
+
101
+ # 7. 验证:用全新环境拉一遍,确认入口命令可用
102
+ uvx records-mcp --help
103
+ ```
104
+
105
+ ### 常见坑
106
+
107
+ - **版本号不可重用**:`0.1.0` 传过一次就锁死,删了项目也不能再用这个号传;改代码必须 bump 版本号。
108
+ - **没开 2FA 传不上去**:报 `403 ... current user does not match user restriction`,去 Account settings 把 2FA(认证器 App)开了再传——只生成恢复码还不够。
109
+ - **token 泄露立刻撤**:在 token 页 Remove 旧 token、重建一个。
@@ -0,0 +1,91 @@
1
+ # records-mcp
2
+
3
+ SQLite CRUD MCP server for teaching. Exposes a records database via the MCP protocol.
4
+
5
+ ## Tools
6
+
7
+ | Tool | Description |
8
+ |---|---|
9
+ | `get_database_overview` | Record counts, status distribution, models, devices, and time range |
10
+ | `search_records` | Search records with optional filters |
11
+ | `get_record` | Get one record by ID |
12
+ | `create_record` | Create a new record |
13
+ | `update_record` | Update an existing record |
14
+ | `delete_record` | Delete a record |
15
+
16
+ ## Usage
17
+
18
+ ```bash
19
+ # stdio mode (default, for MCP client)
20
+ uvx records-mcp --db records.db --transport stdio
21
+
22
+ # SSE mode (network server)
23
+ uvx records-mcp --db records.db --transport sse --host 0.0.0.0 --port 8766
24
+
25
+ # streamable-http mode
26
+ uvx records-mcp --db records.db --transport streamable-http --host 0.0.0.0 --port 8766
27
+ ```
28
+
29
+ ## mcp_servers.json
30
+
31
+ ```json
32
+ {
33
+ "mcpServers": {
34
+ "records": {
35
+ "enabled": true,
36
+ "type": "stdio",
37
+ "command": "uvx",
38
+ "args": ["records-mcp", "--db", "records.db", "--transport", "stdio"]
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ ## 发布与更新版本
45
+
46
+ > PyPI **不允许重复上传同一版本号**:传过的版本号(如 `0.1.0`)被永久占用,传错或想改代码只能 bump 到新版本号,不能覆盖。所以每次发布前先改版本号。
47
+
48
+ ### 一次性配置
49
+
50
+ 1. 账号开启 **2FA**(认证器 App)——PyPI 强制:没开 2FA 的账号**禁止上传**,会报 `403 Invalid API Token: current user does not match user restriction`。
51
+ 2. 在 https://pypi.org/manage/account/token/ 建一个 **API token**,scope 选 `Entire account`。token 只显示一次,记好。
52
+ 3.(可选)把凭证写进 `~/.pypirc`,省得每次输:
53
+
54
+ ```ini
55
+ [pypi]
56
+ username = __token__
57
+ password = pypi-你的token
58
+ ```
59
+
60
+ > 别把 `.pypirc` 提交进 git。
61
+
62
+ ### 发新版本流程
63
+
64
+ ```bash
65
+ # 1. 改版本号:编辑 pyproject.toml,version = "0.1.0" → 例如 "0.1.1"
66
+ # (语义化版本:修 bug → 0.1.x,加功能 → 0.x.0,不兼容改动 → x.0.0)
67
+
68
+ # 2. 清掉旧产物,避免把旧 wheel / 旧 sdist 一起传上去
69
+ rm -rf dist build *.egg-info
70
+
71
+ # 3. 构建 wheel + sdist
72
+ python -m build # 或 uv build
73
+
74
+ # 4. 检查 metadata 没问题(期望两条 PASSED)
75
+ twine check dist/*
76
+
77
+ # 5.(可选)先发 TestPyPI 验证一遍,确认无误再发真的
78
+ twine upload --repository-url https://test.pypi.org/legacy/ dist/*
79
+
80
+ # 6. 发到真 PyPI
81
+ twine upload dist/* # 或:uv publish --token <token> dist/*
82
+
83
+ # 7. 验证:用全新环境拉一遍,确认入口命令可用
84
+ uvx records-mcp --help
85
+ ```
86
+
87
+ ### 常见坑
88
+
89
+ - **版本号不可重用**:`0.1.0` 传过一次就锁死,删了项目也不能再用这个号传;改代码必须 bump 版本号。
90
+ - **没开 2FA 传不上去**:报 `403 ... current user does not match user restriction`,去 Account settings 把 2FA(认证器 App)开了再传——只生成恢复码还不够。
91
+ - **token 泄露立刻撤**:在 token 页 Remove 旧 token、重建一个。
Binary file
Binary file
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "records-mcp"
7
+ version = "0.1.0"
8
+ description = "SQLite CRUD MCP server for teaching — exposes records database via MCP protocol"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.11"
12
+ authors = [
13
+ { name = "Nanobot Team" }
14
+ ]
15
+ keywords = ["mcp", "sqlite", "records", "teaching"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ ]
24
+ dependencies = [
25
+ "mcp>=1.0.0",
26
+ ]
27
+
28
+ [project.scripts]
29
+ records-mcp = "records_mcp_server:main"
30
+
31
+ [tool.hatch.build.targets.wheel]
32
+ force-include = { "records_mcp_server.py" = "records_mcp_server.py" }
@@ -0,0 +1,291 @@
1
+ #!/usr/bin/env python3
2
+ """SQLite CRUD MCP server used by the mini Harness lessons."""
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+ import sqlite3
8
+ import sys
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from mcp.server.fastmcp import FastMCP
14
+
15
+
16
+ def _connect_database(db_path: Path) -> sqlite3.Connection:
17
+ """Open the teaching database used by both read and write tools."""
18
+ connection = sqlite3.connect(db_path)
19
+ connection.row_factory = sqlite3.Row
20
+ return connection
21
+
22
+
23
+ def _json(data: Any) -> str:
24
+ return json.dumps(data, ensure_ascii=False, indent=2)
25
+
26
+
27
+ def _log(event: str, **details: Any) -> None:
28
+ """Write teaching logs to stderr; stdout is reserved for MCP stdio messages."""
29
+ suffix = " ".join(f"{key}={value!r}" for key, value in details.items())
30
+ print(f"[records-mcp] {event} {suffix}".rstrip(), file=sys.stderr, flush=True)
31
+
32
+
33
+ def create_server(db_path: Path, host: str, port: int) -> FastMCP:
34
+ """Create a server whose tools are bound to one SQLite database."""
35
+ mcp = FastMCP(
36
+ "records-database",
37
+ instructions="Create, read, update, and delete records in the teaching dataset.",
38
+ host=host,
39
+ port=port,
40
+ )
41
+
42
+ @mcp.tool()
43
+ def get_database_overview() -> str:
44
+ """Return record counts, status distribution, models, devices, and time range."""
45
+ _log("call", tool="get_database_overview")
46
+ with _connect_database(db_path) as connection:
47
+ summary = connection.execute(
48
+ """
49
+ SELECT
50
+ COUNT(*) AS total_records,
51
+ COUNT(DISTINCT deviceSN) AS device_count,
52
+ COUNT(DISTINCT modelName) AS model_count,
53
+ MIN(captureTime) AS earliest_capture,
54
+ MAX(captureTime) AS latest_capture
55
+ FROM records
56
+ """
57
+ ).fetchone()
58
+ statuses = connection.execute(
59
+ "SELECT status, COUNT(*) AS count FROM records GROUP BY status ORDER BY count DESC"
60
+ ).fetchall()
61
+ models = connection.execute(
62
+ "SELECT modelName, COUNT(*) AS count FROM records "
63
+ "GROUP BY modelName ORDER BY count DESC, modelName"
64
+ ).fetchall()
65
+ _log("result", tool="get_database_overview", total=summary["total_records"])
66
+ return _json(
67
+ {
68
+ "database": str(db_path),
69
+ "summary": dict(summary),
70
+ "status_counts": [dict(row) for row in statuses],
71
+ "model_counts": [dict(row) for row in models],
72
+ }
73
+ )
74
+
75
+ @mcp.tool()
76
+ def search_records(
77
+ device_sn: str | None = None,
78
+ model_name: str | None = None,
79
+ status: str | None = None,
80
+ start_time: str | None = None,
81
+ end_time: str | None = None,
82
+ limit: int = 20,
83
+ ) -> str:
84
+ """Search records with optional exact filters and an inclusive capture-time range."""
85
+ _log(
86
+ "call",
87
+ tool="search_records",
88
+ device_sn=device_sn,
89
+ model_name=model_name,
90
+ status=status,
91
+ limit=limit,
92
+ )
93
+ if not 1 <= limit <= 100:
94
+ raise ValueError("limit must be between 1 and 100")
95
+
96
+ clauses: list[str] = []
97
+ parameters: list[Any] = []
98
+ filters = (
99
+ ("deviceSN = ?", device_sn),
100
+ ("modelName = ?", model_name),
101
+ ("status = ?", status),
102
+ ("captureTime >= ?", start_time),
103
+ ("captureTime <= ?", end_time),
104
+ )
105
+ for clause, value in filters:
106
+ if value is not None:
107
+ clauses.append(clause)
108
+ parameters.append(value)
109
+
110
+ where = f" WHERE {' AND '.join(clauses)}" if clauses else ""
111
+ sql = (
112
+ "SELECT id, deviceSN, modelName, modelVersion, captureTime, imageUrl, status, "
113
+ "error, orig_path, draw_path, updated_at FROM records"
114
+ f"{where} ORDER BY captureTime DESC, id DESC LIMIT ?"
115
+ )
116
+ parameters.append(limit)
117
+ with _connect_database(db_path) as connection:
118
+ rows = connection.execute(sql, parameters).fetchall()
119
+ _log("result", tool="search_records", count=len(rows))
120
+ return _json(
121
+ {
122
+ "count": len(rows),
123
+ "limit": limit,
124
+ "records": [dict(row) for row in rows],
125
+ }
126
+ )
127
+
128
+ @mcp.tool()
129
+ def get_record(record_id: int) -> str:
130
+ """Return one record by its integer primary key."""
131
+ _log("call", tool="get_record", record_id=record_id)
132
+ with _connect_database(db_path) as connection:
133
+ row = connection.execute(
134
+ "SELECT * FROM records WHERE id = ?", (record_id,)
135
+ ).fetchone()
136
+ if row is None:
137
+ _log("result", tool="get_record", record_id=record_id, found=False)
138
+ return _json({"found": False, "record": None})
139
+ _log("result", tool="get_record", record_id=record_id, found=True)
140
+ return _json({"found": True, "record": dict(row)})
141
+
142
+ @mcp.tool()
143
+ def create_record(
144
+ device_sn: str,
145
+ model_name: str,
146
+ model_version: str = "",
147
+ capture_time: str = "",
148
+ image_url: str = "",
149
+ status: str = "ignored",
150
+ error: str = "",
151
+ orig_path: str = "",
152
+ draw_path: str = "",
153
+ ) -> str:
154
+ """Create a record and return it. Production code should validate values and authorize writes."""
155
+ _log(
156
+ "call",
157
+ tool="create_record",
158
+ device_sn=device_sn,
159
+ model_name=model_name,
160
+ status=status,
161
+ )
162
+ now = datetime.now().isoformat(sep=" ", timespec="seconds")
163
+ with _connect_database(db_path) as connection:
164
+ cursor = connection.execute(
165
+ """
166
+ INSERT INTO records (
167
+ deviceSN, modelName, modelVersion, captureTime, imageUrl,
168
+ status, error, orig_path, draw_path, updated_at
169
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
170
+ """,
171
+ (
172
+ device_sn,
173
+ model_name,
174
+ model_version,
175
+ capture_time or now,
176
+ image_url,
177
+ status,
178
+ error,
179
+ orig_path,
180
+ draw_path,
181
+ now,
182
+ ),
183
+ )
184
+ row = connection.execute(
185
+ "SELECT * FROM records WHERE id = ?", (cursor.lastrowid,)
186
+ ).fetchone()
187
+ _log("result", tool="create_record", record_id=cursor.lastrowid)
188
+ return _json({"created": dict(row)})
189
+
190
+ @mcp.tool()
191
+ def update_record(
192
+ record_id: int,
193
+ device_sn: str | None = None,
194
+ model_name: str | None = None,
195
+ model_version: str | None = None,
196
+ capture_time: str | None = None,
197
+ image_url: str | None = None,
198
+ status: str | None = None,
199
+ error: str | None = None,
200
+ orig_path: str | None = None,
201
+ draw_path: str | None = None,
202
+ ) -> str:
203
+ """Update supplied fields. Production code should add authorization, validation, and concurrency checks."""
204
+ values = {
205
+ "deviceSN": device_sn,
206
+ "modelName": model_name,
207
+ "modelVersion": model_version,
208
+ "captureTime": capture_time,
209
+ "imageUrl": image_url,
210
+ "status": status,
211
+ "error": error,
212
+ "orig_path": orig_path,
213
+ "draw_path": draw_path,
214
+ }
215
+ changes = {column: value for column, value in values.items() if value is not None}
216
+ _log("call", tool="update_record", record_id=record_id, fields=list(changes))
217
+ if not changes:
218
+ _log("result", tool="update_record", record_id=record_id, updated=False)
219
+ return _json({"error": "no fields were supplied for update"})
220
+
221
+ with _connect_database(db_path) as connection:
222
+ before = connection.execute(
223
+ "SELECT * FROM records WHERE id = ?", (record_id,)
224
+ ).fetchone()
225
+ if before is None:
226
+ _log("result", tool="update_record", record_id=record_id, found=False)
227
+ return _json({"error": f"record {record_id} was not found"})
228
+
229
+ changes["updated_at"] = datetime.now().isoformat(sep=" ", timespec="seconds")
230
+ assignments = ", ".join(f"{column} = ?" for column in changes)
231
+ connection.execute(
232
+ f"UPDATE records SET {assignments} WHERE id = ?",
233
+ [*changes.values(), record_id],
234
+ )
235
+ after = connection.execute(
236
+ "SELECT * FROM records WHERE id = ?", (record_id,)
237
+ ).fetchone()
238
+ _log("result", tool="update_record", record_id=record_id, updated=True)
239
+ return _json({"before": dict(before), "after": dict(after)})
240
+
241
+ @mcp.tool()
242
+ def delete_record(record_id: int) -> str:
243
+ """Permanently delete a record. Production code should prefer authorization and soft deletion."""
244
+ _log("call", tool="delete_record", record_id=record_id)
245
+ with _connect_database(db_path) as connection:
246
+ row = connection.execute(
247
+ "SELECT * FROM records WHERE id = ?", (record_id,)
248
+ ).fetchone()
249
+ if row is None:
250
+ _log("result", tool="delete_record", record_id=record_id, found=False)
251
+ return _json({"error": f"record {record_id} was not found"})
252
+ connection.execute("DELETE FROM records WHERE id = ?", (record_id,))
253
+ _log("result", tool="delete_record", record_id=record_id, deleted=True)
254
+ return _json({"deleted": dict(row)})
255
+
256
+ return mcp
257
+
258
+
259
+ def _parse_args() -> argparse.Namespace:
260
+ parser = argparse.ArgumentParser(description=__doc__)
261
+ parser.add_argument(
262
+ "--db",
263
+ type=Path,
264
+ default=os.getenv("RECORDS_DB_PATH"),
265
+ help="path to records.db (or set RECORDS_DB_PATH)",
266
+ )
267
+ parser.add_argument(
268
+ "--transport",
269
+ choices=("stdio", "sse", "streamable-http"),
270
+ default="stdio",
271
+ )
272
+ parser.add_argument("--host", default="127.0.0.1")
273
+ parser.add_argument("--port", type=int, default=8766)
274
+ return parser.parse_args()
275
+
276
+
277
+ def main() -> None:
278
+ args = _parse_args()
279
+ if args.db is None:
280
+ raise SystemExit("--db or RECORDS_DB_PATH is required")
281
+ db_path = args.db.expanduser().resolve()
282
+ if not db_path.is_file():
283
+ raise SystemExit(f"database not found: {db_path}")
284
+ _log("starting", transport=args.transport, database=str(db_path))
285
+ if args.transport == "stdio":
286
+ _log("waiting", message="stdio server is waiting for MCP JSON-RPC on stdin")
287
+ create_server(db_path, args.host, args.port).run(transport=args.transport)
288
+
289
+
290
+ if __name__ == "__main__":
291
+ main()