docx-cli 0.1.0__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.
- docx_cli/__init__.py +8 -0
- docx_cli/__main__.py +7 -0
- docx_cli/_templates/hodatajstvo.json +60 -0
- docx_cli/_templates/iskovoe_zayavlenie.json +75 -0
- docx_cli/_templates/otzyv_na_isk.json +69 -0
- docx_cli/_templates/soyu_rayon.json +61 -0
- docx_cli/_templates/vs_sud.json +65 -0
- docx_cli/agent_reader.py +87 -0
- docx_cli/blocks.py +333 -0
- docx_cli/builder.py +116 -0
- docx_cli/cli.py +641 -0
- docx_cli/errors.py +21 -0
- docx_cli/fonts_normalize.py +119 -0
- docx_cli/inject_scan.py +369 -0
- docx_cli/mcp_server.py +380 -0
- docx_cli/md_court.py +665 -0
- docx_cli/profiles.py +272 -0
- docx_cli/spec.py +233 -0
- docx_cli/validate.py +786 -0
- docx_cli-0.1.0.dist-info/METADATA +95 -0
- docx_cli-0.1.0.dist-info/RECORD +25 -0
- docx_cli-0.1.0.dist-info/WHEEL +4 -0
- docx_cli-0.1.0.dist-info/entry_points.txt +3 -0
- docx_cli-0.1.0.dist-info/licenses/LICENSE +199 -0
- docx_cli-0.1.0.dist-info/licenses/NOTICE +1 -0
docx_cli/mcp_server.py
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""docx-cli MCP stdio server: JSON-RPC 2.0 tool surface (v0.1.0, A-05).
|
|
2
|
+
|
|
3
|
+
Паттерн sud-cli (mcp_server.py): tools/list, tools/call, единый content-конверт
|
|
4
|
+
с isError; алиас-толерантность параметров через _pick (два имени с разными
|
|
5
|
+
значениями — явная ошибка, не молчаливый выбор). Межрепозиторийной зависимости
|
|
6
|
+
нет — это наша реализация того же контракта.
|
|
7
|
+
|
|
8
|
+
Права (sud W6a): каждый инструмент несёт поле permission read|write (и
|
|
9
|
+
permission-поле в каждой inputSchema); write-тулы (docx_build, docx_normalize,
|
|
10
|
+
docx_md_build) требуют явный confirm: true — без него status=needs_confirmation
|
|
11
|
+
и НИКАКИХ побочных эффектов (файл не пишется). needs_confirmation — данные,
|
|
12
|
+
не транспортная ошибка (isError=False).
|
|
13
|
+
|
|
14
|
+
Схемы: type object, required минимальный, additionalProperties:false —
|
|
15
|
+
неизвестный параметр явная ошибка, а не молчаливое игнорирование (fail-closed).
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import io
|
|
21
|
+
import json
|
|
22
|
+
import sys
|
|
23
|
+
import tempfile
|
|
24
|
+
from contextlib import redirect_stdout
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
import docx_cli
|
|
29
|
+
from docx_cli import __version__ as _PKG_VERSION
|
|
30
|
+
|
|
31
|
+
CONFIRM_PROPERTY = {
|
|
32
|
+
"type": "boolean",
|
|
33
|
+
"description": "write-инструмент: без confirm=true вернёт "
|
|
34
|
+
"status=needs_confirmation, запись не производится",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _schema(properties: dict, required: list[str] | None = None,
|
|
39
|
+
write: bool = False) -> dict:
|
|
40
|
+
props = dict(properties)
|
|
41
|
+
props["permission"] = dict(PROPERTY_PERMISSION_DEFAULT)
|
|
42
|
+
if write:
|
|
43
|
+
props["confirm"] = dict(CONFIRM_PROPERTY)
|
|
44
|
+
return {"type": "object", "properties": props,
|
|
45
|
+
"required": required or [], "additionalProperties": False}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# permission-поле объявлено в КАЖДОЙ схеме (контракт §1); значение сверяется
|
|
49
|
+
# с декларируемым правом тула при диспатче (fail-closed на mismatch).
|
|
50
|
+
PROPERTY_PERMISSION_DEFAULT = {
|
|
51
|
+
"type": "string",
|
|
52
|
+
"enum": ["read", "write"],
|
|
53
|
+
"description": "декларируемое право: read|write; несовпадение с правом "
|
|
54
|
+
"тула — явная ошибка",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
_PATH = {"type": "string", "description": "путь к файлу"}
|
|
58
|
+
|
|
59
|
+
TOOLS: list[dict[str, Any]] = [
|
|
60
|
+
{"name": "docx_build", "permission": "write",
|
|
61
|
+
"description": "Собрать .docx из DocSpec (машинный контракт профиля "
|
|
62
|
+
"gost|compact|court|vs_sud|soyu_rayon) по указанному пути. "
|
|
63
|
+
"write: требует confirm=true.",
|
|
64
|
+
"schema": _schema({
|
|
65
|
+
"spec": {"type": "object",
|
|
66
|
+
"description": "DocSpec JSON (profile/output/header/title/body)"},
|
|
67
|
+
"out": {**_PATH, "description": "путь итогового .docx; CLI-алиас: output"},
|
|
68
|
+
"output": {**_PATH, "description": "CLI-алиас out"},
|
|
69
|
+
}, required=["spec"], write=True)},
|
|
70
|
+
{"name": "docx_validate", "permission": "read",
|
|
71
|
+
"description": "Проверить существующий .docx валидатором оформления "
|
|
72
|
+
"(gost|review). Только чтение.",
|
|
73
|
+
"schema": _schema({
|
|
74
|
+
"file": {**_PATH, "description": "путь к .docx; CLI-алиас: path"},
|
|
75
|
+
"path": {**_PATH, "description": "CLI-алиас file"},
|
|
76
|
+
"mode": {"type": "string", "enum": ["gost", "review"], "default": "gost"},
|
|
77
|
+
"strict": {"type": "boolean", "default": False,
|
|
78
|
+
"description": "w/skip -> warnings_present semantics (as CLI --strict)"}},
|
|
79
|
+
required=["file"])},
|
|
80
|
+
{"name": "docx_scan", "permission": "read",
|
|
81
|
+
"description": "Аудит входящего .docx на скрытые LLM-векторы "
|
|
82
|
+
"(inject_scan как есть: vanish/микрошрифт/tracked/…). "
|
|
83
|
+
"Только чтение.",
|
|
84
|
+
"schema": _schema({
|
|
85
|
+
"file": {**_PATH, "description": "путь к .docx; CLI-алиас: path"},
|
|
86
|
+
"path": {**_PATH, "description": "CLI-алиас file"},
|
|
87
|
+
"a2a_args": {"type": "boolean", "default": False,
|
|
88
|
+
"description": "добавить bus-free a2a_command в конверт"}})},
|
|
89
|
+
{"name": "docx_normalize", "permission": "write",
|
|
90
|
+
"description": "Нормализовать шрифты/интервалы .docx к Times New Roman. "
|
|
91
|
+
"write: требует confirm=true; каталог — только yes=true.",
|
|
92
|
+
"schema": _schema({
|
|
93
|
+
"path": {**_PATH, "description": "файл или каталог .docx"},
|
|
94
|
+
"file": {**_PATH, "description": "CLI-алиас path"},
|
|
95
|
+
"body_size": {"type": "integer", "default": 12},
|
|
96
|
+
"header_size": {"type": "integer", "default": 11},
|
|
97
|
+
"dry_run": {"type": "boolean", "default": False,
|
|
98
|
+
"description": "показать изменения без записи"},
|
|
99
|
+
"backup": {"type": "boolean", "default": True},
|
|
100
|
+
"yes": {"type": "boolean", "default": False,
|
|
101
|
+
"description": "подтвердить каталожный режим"}},
|
|
102
|
+
required=["path"], write=True)},
|
|
103
|
+
{"name": "docx_md_build", "permission": "write",
|
|
104
|
+
"description": "Собрать процессуальный .docx из Markdown (шапка, титул, "
|
|
105
|
+
"**bold**, вложенные списки и pipe-таблицы v0.10). "
|
|
106
|
+
"write: требует confirm=true.",
|
|
107
|
+
"schema": _schema({
|
|
108
|
+
"md": {"type": "string",
|
|
109
|
+
"description": "текст markdown; альтернатива md_path"},
|
|
110
|
+
"md_path": {**_PATH, "description": "путь к .md (вместо md)"},
|
|
111
|
+
"out": {**_PATH, "description": "путь итогового .docx; CLI-алиас: output"},
|
|
112
|
+
"output": {**_PATH, "description": "CLI-алиас out"},
|
|
113
|
+
"author": {"type": "string", "default": "colleague"}},
|
|
114
|
+
required=["out"], write=True)},
|
|
115
|
+
{"name": "docx_doctor", "permission": "read",
|
|
116
|
+
"description": "Проверить окружение (python, python-docx, lxml, pydantic, "
|
|
117
|
+
"запись cwd). Только чтение.",
|
|
118
|
+
"schema": _schema({
|
|
119
|
+
"verify_signature": {"type": "boolean", "default": False},
|
|
120
|
+
"dist": {"type": "string", "default": "dist"}})},
|
|
121
|
+
{"name": "docx_agent_read", "permission": "read",
|
|
122
|
+
"description": "Ограниченное JSON-чтение .docx (окно + курсор). Только "
|
|
123
|
+
"чтение.",
|
|
124
|
+
"schema": _schema({
|
|
125
|
+
"file": {**_PATH, "description": "путь к .docx; CLI-алиас: path"},
|
|
126
|
+
"path": {**_PATH, "description": "CLI-алиас file"},
|
|
127
|
+
"start": {"type": "integer", "default": 0},
|
|
128
|
+
"max_chars": {"type": "integer", "default": 4000,
|
|
129
|
+
"description": "CLI-алиас max"},
|
|
130
|
+
"max": {"type": "integer", "description": "CLI-алиас max_chars"}})},
|
|
131
|
+
{"name": "docx_verify_forbidden", "permission": "read",
|
|
132
|
+
"description": "Проверить .docx (+опц. .md) на запрещённые строки "
|
|
133
|
+
"(desync-guard). Только чтение.",
|
|
134
|
+
"schema": _schema({
|
|
135
|
+
"docx": {**_PATH, "description": "путь к .docx; CLI-алиас: path"},
|
|
136
|
+
"path": {**_PATH, "description": "CLI-алиас docx"},
|
|
137
|
+
"md": {**_PATH, "description": "опциональный .md к проверке"},
|
|
138
|
+
"forbid": {"type": "array", "items": {"type": "string"},
|
|
139
|
+
"description": "запрещённые подстроки"},
|
|
140
|
+
"forbid_file": {**_PATH, "description": "файл списка паттернов"}},
|
|
141
|
+
required=["docx"])},
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
TOOLS_BY_NAME = {t["name"]: t for t in TOOLS}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _pick(values: dict, names: tuple[str, ...], label: str):
|
|
148
|
+
"""Значение под любым из допустимых имён (CLI-алиас ↔ имя движка).
|
|
149
|
+
Два имени с РАЗНЫМИ значениями — явная ошибка (паттерн sud)."""
|
|
150
|
+
found = {n: values.pop(n) for n in names if n in values}
|
|
151
|
+
if not found:
|
|
152
|
+
return None
|
|
153
|
+
distinct = {repr(v) for v in found.values()}
|
|
154
|
+
if len(distinct) > 1:
|
|
155
|
+
raise ValueError(f"conflicting values for {label}: {sorted(found)} — "
|
|
156
|
+
"передайте только одно имя")
|
|
157
|
+
return next(iter(found.values()))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _run_captured(func, ns: argparse.Namespace) -> dict:
|
|
161
|
+
"""Запуск cmd_* из cli.py с перехватом stdout-конверта: MCP не дублирует
|
|
162
|
+
логику CLI, а переиспользует ровно тот же код и ровно тот же конверт."""
|
|
163
|
+
from docx_cli import cli as _cli
|
|
164
|
+
buf = io.StringIO()
|
|
165
|
+
with redirect_stdout(buf):
|
|
166
|
+
exit_code = func(ns)
|
|
167
|
+
text = buf.getvalue().strip()
|
|
168
|
+
payload = json.loads(text) if text else {}
|
|
169
|
+
payload.setdefault("exit_code", exit_code)
|
|
170
|
+
return payload
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _call(name: str, args: dict) -> dict:
|
|
174
|
+
"""Диспатч инструментов. Ошибки — исключения (handle_request конвертит
|
|
175
|
+
в isError=True), сайд-эффекты только после confirm-гейта в _dispatch_gated."""
|
|
176
|
+
values = dict(args or {})
|
|
177
|
+
if name == "docx_build":
|
|
178
|
+
from docx_cli.builder import build_document
|
|
179
|
+
from docx_cli.spec import DocSpec
|
|
180
|
+
out = _pick(values, ("out", "output"), "out/output")
|
|
181
|
+
if out is None:
|
|
182
|
+
raise ValueError("out is required (путь итогового .docx)")
|
|
183
|
+
spec = DocSpec.from_dict(values["spec"])
|
|
184
|
+
return build_document(spec, str(out))
|
|
185
|
+
if name == "docx_validate":
|
|
186
|
+
from docx_cli import cli as _cli
|
|
187
|
+
file = _pick(values, ("file", "path"), "file/path")
|
|
188
|
+
if file is None:
|
|
189
|
+
raise ValueError("file is required")
|
|
190
|
+
return _run_captured(_cli.cmd_validate, argparse.Namespace(
|
|
191
|
+
file=str(file), json=True,
|
|
192
|
+
strict=bool(values.get("strict", False)),
|
|
193
|
+
mode=str(values.get("mode", "gost"))))
|
|
194
|
+
if name == "docx_scan":
|
|
195
|
+
from docx_cli import cli as _cli
|
|
196
|
+
file = _pick(values, ("file", "path"), "file/path")
|
|
197
|
+
if file is None:
|
|
198
|
+
raise ValueError("file is required")
|
|
199
|
+
return _run_captured(_cli.cmd_scan, argparse.Namespace(
|
|
200
|
+
file=str(file), json=True,
|
|
201
|
+
a2a_args=bool(values.get("a2a_args", False))))
|
|
202
|
+
if name == "docx_normalize":
|
|
203
|
+
from docx_cli.fonts_normalize import normalize_path
|
|
204
|
+
path = _pick(values, ("path", "file"), "path/file")
|
|
205
|
+
if path is None:
|
|
206
|
+
raise ValueError("path is required")
|
|
207
|
+
p = Path(str(path))
|
|
208
|
+
if p.is_dir() and not bool(values.get("yes", False)):
|
|
209
|
+
return {"ok": False, "status": "invalid_usage",
|
|
210
|
+
"reason": "directory mode requires yes=true"}
|
|
211
|
+
res = normalize_path(str(p),
|
|
212
|
+
body_size=int(values.get("body_size", 12)),
|
|
213
|
+
header_size=int(values.get("header_size", 11)),
|
|
214
|
+
dry_run=bool(values.get("dry_run", False)),
|
|
215
|
+
backup=bool(values.get("backup", True)),
|
|
216
|
+
yes=bool(values.get("yes", False)))
|
|
217
|
+
res.update({"ok": True, "status": "ok"})
|
|
218
|
+
return res
|
|
219
|
+
if name == "docx_md_build":
|
|
220
|
+
from docx_cli.md_court import md_build
|
|
221
|
+
out = _pick(values, ("out", "output"), "out/output")
|
|
222
|
+
if out is None:
|
|
223
|
+
raise ValueError("out is required")
|
|
224
|
+
md_text = values.get("md")
|
|
225
|
+
md_path = values.get("md_path")
|
|
226
|
+
if md_text is not None and md_path is not None:
|
|
227
|
+
raise ValueError("pass only one of md / md_path")
|
|
228
|
+
if md_text is None and md_path is None:
|
|
229
|
+
raise ValueError("md or md_path is required")
|
|
230
|
+
if md_text is not None:
|
|
231
|
+
tmp = Path(tempfile.mkdtemp(prefix="docx_mcp_md_")) / "input.md"
|
|
232
|
+
tmp.write_text(str(md_text), encoding="utf-8")
|
|
233
|
+
source = str(tmp)
|
|
234
|
+
else:
|
|
235
|
+
source = str(md_path)
|
|
236
|
+
res = md_build(source, str(out), str(values.get("author", "colleague")))
|
|
237
|
+
res.update({"ok": True, "status": "ok"})
|
|
238
|
+
return res
|
|
239
|
+
if name == "docx_doctor":
|
|
240
|
+
from docx_cli import cli as _cli
|
|
241
|
+
return _run_captured(_cli.cmd_doctor, argparse.Namespace(
|
|
242
|
+
json=True,
|
|
243
|
+
verify_signature=bool(values.get("verify_signature", False)),
|
|
244
|
+
dist=str(values.get("dist", "dist"))))
|
|
245
|
+
if name == "docx_agent_read":
|
|
246
|
+
from docx_cli import cli as _cli
|
|
247
|
+
file = _pick(values, ("file", "path"), "file/path")
|
|
248
|
+
if file is None:
|
|
249
|
+
raise ValueError("file is required")
|
|
250
|
+
max_chars = _pick(values, ("max_chars", "max"), "max_chars/max")
|
|
251
|
+
if max_chars is None:
|
|
252
|
+
max_chars = 4000
|
|
253
|
+
return _run_captured(_cli.cmd_agent_read, argparse.Namespace(
|
|
254
|
+
file=str(file), start=int(values.get("start", 0)),
|
|
255
|
+
max_chars=int(max_chars)))
|
|
256
|
+
if name == "docx_verify_forbidden":
|
|
257
|
+
from docx_cli import cli as _cli
|
|
258
|
+
docx_path = _pick(values, ("docx", "path"), "docx/path")
|
|
259
|
+
if docx_path is None:
|
|
260
|
+
raise ValueError("docx is required")
|
|
261
|
+
return _run_captured(_cli.cmd_verify_forbidden, argparse.Namespace(
|
|
262
|
+
docx=str(docx_path), md=values.get("md"),
|
|
263
|
+
forbid=list(values.get("forbid", [])),
|
|
264
|
+
forbid_file=values.get("forbid_file"), json=True))
|
|
265
|
+
raise KeyError(f"unknown tool: {name}")
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _check_arguments(tool: dict, args: dict) -> dict:
|
|
269
|
+
"""fail-closed валидация аргументов ДО движка: unknown fields и
|
|
270
|
+
permission-mismatch не смещаются confirm-гейтом."""
|
|
271
|
+
if not isinstance(args, dict):
|
|
272
|
+
raise TypeError("arguments must be an object")
|
|
273
|
+
values = dict(args)
|
|
274
|
+
schema = tool["schema"]
|
|
275
|
+
allowed = set((schema.get("properties") or {}).keys())
|
|
276
|
+
unknown = set(values) - allowed
|
|
277
|
+
if unknown:
|
|
278
|
+
raise ValueError(f"unknown fields: {sorted(unknown)}")
|
|
279
|
+
perm = values.pop("permission", None)
|
|
280
|
+
if perm is not None and perm != tool["permission"]:
|
|
281
|
+
raise ValueError(
|
|
282
|
+
f"permission_mismatch: tool {tool['name']} declares "
|
|
283
|
+
f"'{tool['permission']}', got '{perm}'")
|
|
284
|
+
for field in schema.get("required", []):
|
|
285
|
+
if field not in values:
|
|
286
|
+
raise ValueError(f"missing required field: {field}")
|
|
287
|
+
confirm = None
|
|
288
|
+
if tool["permission"] == "write":
|
|
289
|
+
confirm = values.pop("confirm", None)
|
|
290
|
+
return {"values": values, "confirm": confirm}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _dispatch_gated(name: str, args: dict) -> Any:
|
|
294
|
+
tool = TOOLS_BY_NAME.get(name)
|
|
295
|
+
if tool is None:
|
|
296
|
+
raise KeyError(f"unknown tool: {name}")
|
|
297
|
+
prepared = _check_arguments(tool, args)
|
|
298
|
+
if tool["permission"] == "write" and prepared["confirm"] is not True:
|
|
299
|
+
# needs_confirmation — ДАННЫЕ, не ошибка транспорта; запись не
|
|
300
|
+
# производилась ни байта (сайд-эффекты только в _call ниже).
|
|
301
|
+
return {"status": "needs_confirmation", "permission": "write",
|
|
302
|
+
"tool": name,
|
|
303
|
+
"detail": "write-инструмент требует явный confirm: true — "
|
|
304
|
+
"запись не произведена"}
|
|
305
|
+
return _call(name, prepared["values"])
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def tool_list() -> dict:
|
|
309
|
+
return {"tools": [{"name": t["name"], "description": t["description"],
|
|
310
|
+
"inputSchema": t["schema"], "permission": t["permission"]}
|
|
311
|
+
for t in TOOLS]}
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def handle_request(request: dict) -> dict | None:
|
|
315
|
+
method = request.get("method")
|
|
316
|
+
request_id = request.get("id")
|
|
317
|
+
if method == "initialize":
|
|
318
|
+
result = {"protocolVersion": "2024-11-05",
|
|
319
|
+
"capabilities": {"tools": {}},
|
|
320
|
+
"serverInfo": {"name": "docx-cli", "version": _PKG_VERSION}}
|
|
321
|
+
elif method == "notifications/initialized":
|
|
322
|
+
return None
|
|
323
|
+
elif method == "ping":
|
|
324
|
+
result = {}
|
|
325
|
+
elif method == "tools/list":
|
|
326
|
+
result = tool_list()
|
|
327
|
+
elif method == "tools/call":
|
|
328
|
+
try:
|
|
329
|
+
params = request.get("params", {})
|
|
330
|
+
value = _dispatch_gated(params["name"], params.get("arguments", {}))
|
|
331
|
+
if hasattr(value, "to_dict"):
|
|
332
|
+
value = value.to_dict()
|
|
333
|
+
result = {"content": [{"type": "text",
|
|
334
|
+
"text": json.dumps(value, ensure_ascii=False,
|
|
335
|
+
default=str)}],
|
|
336
|
+
"isError": False}
|
|
337
|
+
except (KeyError, TypeError, ValueError, OSError) as exc:
|
|
338
|
+
result = {"content": [{"type": "text", "text": str(exc)}],
|
|
339
|
+
"isError": True}
|
|
340
|
+
except Exception as exc: # доменные ошибки: stdio не умирает
|
|
341
|
+
result = {"content": [{"type": "text",
|
|
342
|
+
"text": f"unexpected: {type(exc).__name__}: {exc}"}],
|
|
343
|
+
"isError": True}
|
|
344
|
+
else:
|
|
345
|
+
return {"jsonrpc": "2.0", "id": request_id,
|
|
346
|
+
"error": {"code": -32601, "message": "Method not found"}}
|
|
347
|
+
return {"jsonrpc": "2.0", "id": request_id, "result": result}
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def serve_stdio(input_stream=None, output_stream=None) -> None:
|
|
351
|
+
input_stream = input_stream or sys.stdin
|
|
352
|
+
output_stream = output_stream or sys.stdout
|
|
353
|
+
for line in input_stream:
|
|
354
|
+
if not line.strip():
|
|
355
|
+
continue
|
|
356
|
+
try:
|
|
357
|
+
frame = json.loads(line)
|
|
358
|
+
except json.JSONDecodeError as exc:
|
|
359
|
+
response = {"jsonrpc": "2.0", "id": None,
|
|
360
|
+
"error": {"code": -32700, "message": f"Parse error: {exc}"}}
|
|
361
|
+
else:
|
|
362
|
+
if not isinstance(frame, dict):
|
|
363
|
+
response = {"jsonrpc": "2.0", "id": None,
|
|
364
|
+
"error": {"code": -32600,
|
|
365
|
+
"message": "Invalid Request: not an object"}}
|
|
366
|
+
else:
|
|
367
|
+
response = handle_request(frame)
|
|
368
|
+
if response is not None:
|
|
369
|
+
output_stream.write(json.dumps(response, ensure_ascii=False) + "\n")
|
|
370
|
+
output_stream.flush()
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def run_tools_dump() -> int:
|
|
374
|
+
print(json.dumps(tool_list(), ensure_ascii=False))
|
|
375
|
+
return 0
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def run_stdio_server() -> int:
|
|
379
|
+
serve_stdio()
|
|
380
|
+
return 0
|