model_context_protocol_demo 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.
- model_context_protocol_demo/__init__.py +0 -0
- model_context_protocol_demo/main.py +7 -0
- model_context_protocol_demo/server.py +443 -0
- model_context_protocol_demo-0.1.0.dist-info/METADATA +7 -0
- model_context_protocol_demo-0.1.0.dist-info/RECORD +8 -0
- model_context_protocol_demo-0.1.0.dist-info/WHEEL +5 -0
- model_context_protocol_demo-0.1.0.dist-info/entry_points.txt +2 -0
- model_context_protocol_demo-0.1.0.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import glob as glob_module
|
|
3
|
+
import logging
|
|
4
|
+
import logging.handlers
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import shlex
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
import uuid
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Optional
|
|
15
|
+
|
|
16
|
+
from fastmcp import FastMCP
|
|
17
|
+
|
|
18
|
+
SANDBOX_ROOT = Path(os.environ.get("MCP_SANDBOX_ROOT", os.getcwd())).resolve()
|
|
19
|
+
CMD_TIMEOUT = float(os.environ.get("MCP_CMD_TIMEOUT", "30"))
|
|
20
|
+
MAX_OUTPUT_BYTES = int(os.environ.get("MCP_MAX_OUTPUT_BYTES", "200000"))
|
|
21
|
+
LOG_DIR = Path(os.environ.get("MCP_LOG_DIR", "./logs")).resolve()
|
|
22
|
+
LOG_LEVEL = os.environ.get("MCP_LOG_LEVEL", "INFO").upper()
|
|
23
|
+
|
|
24
|
+
SANDBOX_ROOT.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger("mcp_tool_server")
|
|
28
|
+
logger.setLevel(LOG_LEVEL)
|
|
29
|
+
logger.propagate = False
|
|
30
|
+
|
|
31
|
+
mcp = FastMCP()
|
|
32
|
+
|
|
33
|
+
if not logger.handlers:
|
|
34
|
+
fmt = logging.Formatter(
|
|
35
|
+
"%(asctime)s | %(levelname)-8s | %(name)s | %(message)s"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
console_handler = logging.StreamHandler(sys.stderr)
|
|
39
|
+
console_handler.setFormatter(fmt)
|
|
40
|
+
logger.addHandler(console_handler)
|
|
41
|
+
|
|
42
|
+
file_handler = logging.handlers.RotatingFileHandler(
|
|
43
|
+
LOG_DIR / "mcp_tool_server.log",
|
|
44
|
+
maxBytes=10 * 1024 * 1024,
|
|
45
|
+
backupCount=5,
|
|
46
|
+
encoding="utf-8",
|
|
47
|
+
)
|
|
48
|
+
file_handler.setFormatter(fmt)
|
|
49
|
+
logger.addHandler(file_handler)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def new_request_id() -> str:
|
|
53
|
+
return uuid.uuid4().hex[:12]
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class ToolResult:
|
|
57
|
+
ok: bool
|
|
58
|
+
output: str = ""
|
|
59
|
+
error: Optional[str] = None
|
|
60
|
+
meta: dict[str, Any] = field(default_factory=dict)
|
|
61
|
+
|
|
62
|
+
def to_dict(self) -> dict[str, Any]:
|
|
63
|
+
return {
|
|
64
|
+
"ok": self.ok,
|
|
65
|
+
"output": self.output,
|
|
66
|
+
"error": self.error,
|
|
67
|
+
"meta": self.meta,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _truncate(text: str, limit: int = MAX_OUTPUT_BYTES) -> tuple[str, bool]:
|
|
72
|
+
encoded = text.encode("utf-8", errors="replace")
|
|
73
|
+
if len(encoded) <= limit:
|
|
74
|
+
return text, False
|
|
75
|
+
truncated = encoded[:limit].decode("utf-8", errors="ignore")
|
|
76
|
+
return truncated, True
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class PathSecurityError(Exception):
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def resolve_safe_path(user_path: str) -> Path:
|
|
84
|
+
"""Resolve `user_path` (relative or absolute) against SANDBOX_ROOT and
|
|
85
|
+
guarantee the result stays inside it. Raises PathSecurityError otherwise.
|
|
86
|
+
"""
|
|
87
|
+
candidate = (SANDBOX_ROOT / user_path).resolve() if not os.path.isabs(user_path) \
|
|
88
|
+
else Path(user_path).resolve()
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
candidate.relative_to(SANDBOX_ROOT)
|
|
92
|
+
except ValueError:
|
|
93
|
+
raise PathSecurityError(
|
|
94
|
+
f"Path '{user_path}' resolves outside the sandbox root "
|
|
95
|
+
f"'{SANDBOX_ROOT}'. Operation blocked."
|
|
96
|
+
)
|
|
97
|
+
return candidate
|
|
98
|
+
|
|
99
|
+
def run_subprocess(
|
|
100
|
+
args: list[str] | str,
|
|
101
|
+
*,
|
|
102
|
+
shell: bool,
|
|
103
|
+
timeout: float,
|
|
104
|
+
cwd: Path,
|
|
105
|
+
request_id: str,
|
|
106
|
+
) -> ToolResult:
|
|
107
|
+
start = time.monotonic()
|
|
108
|
+
try:
|
|
109
|
+
proc = subprocess.run(
|
|
110
|
+
args,
|
|
111
|
+
shell=shell,
|
|
112
|
+
cwd=str(cwd),
|
|
113
|
+
stdout=subprocess.PIPE,
|
|
114
|
+
stderr=subprocess.PIPE,
|
|
115
|
+
timeout=timeout,
|
|
116
|
+
)
|
|
117
|
+
elapsed = round(time.monotonic() - start, 3)
|
|
118
|
+
stdout, out_trunc = _truncate(proc.stdout.decode("utf-8", errors="replace"))
|
|
119
|
+
stderr, err_trunc = _truncate(proc.stderr.decode("utf-8", errors="replace"))
|
|
120
|
+
|
|
121
|
+
meta = {
|
|
122
|
+
"request_id": request_id,
|
|
123
|
+
"return_code": proc.returncode,
|
|
124
|
+
"elapsed_seconds": elapsed,
|
|
125
|
+
"stdout_truncated": out_trunc,
|
|
126
|
+
"stderr_truncated": err_trunc,
|
|
127
|
+
"cwd": str(cwd),
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if proc.returncode == 0:
|
|
131
|
+
logger.info("[%s] command succeeded in %ss", request_id, elapsed)
|
|
132
|
+
return ToolResult(ok=True, output=stdout, error=stderr or None, meta=meta)
|
|
133
|
+
else:
|
|
134
|
+
logger.warning(
|
|
135
|
+
"[%s] command exited %s in %ss", request_id, proc.returncode, elapsed
|
|
136
|
+
)
|
|
137
|
+
return ToolResult(
|
|
138
|
+
ok=False,
|
|
139
|
+
output=stdout,
|
|
140
|
+
error=stderr or f"Process exited with code {proc.returncode}",
|
|
141
|
+
meta=meta,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
except subprocess.TimeoutExpired:
|
|
145
|
+
elapsed = round(time.monotonic() - start, 3)
|
|
146
|
+
logger.error("[%s] command timed out after %ss", request_id, elapsed)
|
|
147
|
+
return ToolResult(
|
|
148
|
+
ok=False,
|
|
149
|
+
error=f"Command timed out after {timeout} seconds.",
|
|
150
|
+
meta={"request_id": request_id, "elapsed_seconds": elapsed},
|
|
151
|
+
)
|
|
152
|
+
except FileNotFoundError as e:
|
|
153
|
+
logger.error("[%s] executable not found: %s", request_id, e)
|
|
154
|
+
return ToolResult(
|
|
155
|
+
ok=False, error=f"Executable not found: {e}", meta={"request_id": request_id}
|
|
156
|
+
)
|
|
157
|
+
except Exception as e: # noqa: BLE001 - last-resort guard, never leak raw tracebacks
|
|
158
|
+
logger.exception("[%s] unexpected error running command", request_id)
|
|
159
|
+
return ToolResult(
|
|
160
|
+
ok=False, error=f"Unexpected error: {e}", meta={"request_id": request_id}
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
@mcp.tool()
|
|
164
|
+
def bash(command: str, timeout: Optional[float] = None) -> dict:
|
|
165
|
+
"""Execute a bash command inside the sandboxed working directory and
|
|
166
|
+
return a structured result: {ok, output, error, meta}.
|
|
167
|
+
|
|
168
|
+
Blocked: attempts to escape the sandbox root via path traversal in
|
|
169
|
+
obviously destructive patterns (e.g. `rm -rf /`) are not specially
|
|
170
|
+
parsed — the sandbox is enforced at the cwd/path-tool level, not by
|
|
171
|
+
command inspection. Treat this tool as trusted-user-only.
|
|
172
|
+
"""
|
|
173
|
+
request_id = new_request_id()
|
|
174
|
+
logger.info("[%s] bash: %s", request_id, command)
|
|
175
|
+
|
|
176
|
+
if not command or not command.strip():
|
|
177
|
+
return ToolResult(ok=False, error="Empty command.").to_dict()
|
|
178
|
+
|
|
179
|
+
result = run_subprocess(
|
|
180
|
+
command,
|
|
181
|
+
shell=True,
|
|
182
|
+
timeout=timeout or CMD_TIMEOUT,
|
|
183
|
+
cwd=SANDBOX_ROOT,
|
|
184
|
+
request_id=request_id,
|
|
185
|
+
)
|
|
186
|
+
return result.to_dict()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@mcp.tool()
|
|
190
|
+
def python_code(code: str, timeout: Optional[float] = None) -> dict:
|
|
191
|
+
"""Execute a Python code snippet in a subprocess and return a structured
|
|
192
|
+
result: {ok, output, error, meta}.
|
|
193
|
+
"""
|
|
194
|
+
request_id = new_request_id()
|
|
195
|
+
logger.info("[%s] python_code: %d chars", request_id, len(code or ""))
|
|
196
|
+
|
|
197
|
+
if not code or not code.strip():
|
|
198
|
+
return ToolResult(ok=False, error="Empty code.").to_dict()
|
|
199
|
+
|
|
200
|
+
result = run_subprocess(
|
|
201
|
+
[sys.executable, "-c", code],
|
|
202
|
+
shell=False,
|
|
203
|
+
timeout=timeout or CMD_TIMEOUT,
|
|
204
|
+
cwd=SANDBOX_ROOT,
|
|
205
|
+
request_id=request_id,
|
|
206
|
+
)
|
|
207
|
+
return result.to_dict()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@mcp.tool()
|
|
211
|
+
def python_file(file: str, args: Optional[list[str]] = None, timeout: Optional[float] = None) -> dict:
|
|
212
|
+
"""Execute a Python file (path relative to the sandbox root, or an
|
|
213
|
+
absolute path inside it) and return a structured result.
|
|
214
|
+
"""
|
|
215
|
+
request_id = new_request_id()
|
|
216
|
+
try:
|
|
217
|
+
safe_path = resolve_safe_path(file)
|
|
218
|
+
except PathSecurityError as e:
|
|
219
|
+
logger.warning("[%s] blocked path: %s", request_id, e)
|
|
220
|
+
return ToolResult(ok=False, error=str(e)).to_dict()
|
|
221
|
+
|
|
222
|
+
if not safe_path.is_file():
|
|
223
|
+
return ToolResult(ok=False, error=f"File not found: {file}").to_dict()
|
|
224
|
+
|
|
225
|
+
cmd = [sys.executable, str(safe_path), *(args or [])]
|
|
226
|
+
logger.info("[%s] python_file: %s", request_id, " ".join(shlex.quote(c) for c in cmd))
|
|
227
|
+
|
|
228
|
+
result = run_subprocess(
|
|
229
|
+
cmd,
|
|
230
|
+
shell=False,
|
|
231
|
+
timeout=timeout or CMD_TIMEOUT,
|
|
232
|
+
cwd=SANDBOX_ROOT,
|
|
233
|
+
request_id=request_id,
|
|
234
|
+
)
|
|
235
|
+
return result.to_dict()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@mcp.tool()
|
|
239
|
+
def glob(pattern: str, recursive: bool = True) -> dict:
|
|
240
|
+
"""Return files matching `pattern` within the sandbox root.
|
|
241
|
+
`pattern` is interpreted relative to the sandbox root unless absolute
|
|
242
|
+
(absolute paths outside the sandbox are rejected).
|
|
243
|
+
"""
|
|
244
|
+
request_id = new_request_id()
|
|
245
|
+
try:
|
|
246
|
+
base = pattern if os.path.isabs(pattern) else str(SANDBOX_ROOT / pattern)
|
|
247
|
+
matches = glob_module.glob(base, recursive=recursive)
|
|
248
|
+
|
|
249
|
+
safe_matches = []
|
|
250
|
+
for m in matches:
|
|
251
|
+
try:
|
|
252
|
+
Path(m).resolve().relative_to(SANDBOX_ROOT)
|
|
253
|
+
safe_matches.append(m)
|
|
254
|
+
except ValueError:
|
|
255
|
+
continue # silently drop anything outside the sandbox
|
|
256
|
+
|
|
257
|
+
logger.info("[%s] glob(%s) -> %d matches", request_id, pattern, len(safe_matches))
|
|
258
|
+
return ToolResult(ok=True, output="\n".join(safe_matches), meta={
|
|
259
|
+
"request_id": request_id, "count": len(safe_matches)
|
|
260
|
+
}).to_dict()
|
|
261
|
+
except Exception as e: # noqa: BLE001
|
|
262
|
+
logger.exception("[%s] glob failed", request_id)
|
|
263
|
+
return ToolResult(ok=False, error=str(e)).to_dict()
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@mcp.tool()
|
|
267
|
+
def grep(pattern: str, file: str, ignore_case: bool = False, max_matches: int = 500) -> dict:
|
|
268
|
+
"""Search for a regex `pattern` in `file` (relative to sandbox root) and
|
|
269
|
+
return matching lines with their line numbers. Cross-platform (pure
|
|
270
|
+
Python regex, not shell-dependent like findstr/grep).
|
|
271
|
+
"""
|
|
272
|
+
request_id = new_request_id()
|
|
273
|
+
try:
|
|
274
|
+
safe_path = resolve_safe_path(file)
|
|
275
|
+
except PathSecurityError as e:
|
|
276
|
+
return ToolResult(ok=False, error=str(e)).to_dict()
|
|
277
|
+
|
|
278
|
+
if not safe_path.is_file():
|
|
279
|
+
return ToolResult(ok=False, error=f"File not found: {file}").to_dict()
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
flags = re.IGNORECASE if ignore_case else 0
|
|
283
|
+
regex = re.compile(pattern, flags)
|
|
284
|
+
except re.error as e:
|
|
285
|
+
return ToolResult(ok=False, error=f"Invalid regex: {e}").to_dict()
|
|
286
|
+
|
|
287
|
+
matches = []
|
|
288
|
+
try:
|
|
289
|
+
with safe_path.open("r", encoding="utf-8", errors="replace") as f:
|
|
290
|
+
for lineno, line in enumerate(f, start=1):
|
|
291
|
+
if regex.search(line):
|
|
292
|
+
matches.append(f"{lineno}:{line.rstrip(chr(10))}")
|
|
293
|
+
if len(matches) >= max_matches:
|
|
294
|
+
break
|
|
295
|
+
except Exception as e: # noqa: BLE001
|
|
296
|
+
logger.exception("[%s] grep failed", request_id)
|
|
297
|
+
return ToolResult(ok=False, error=str(e)).to_dict()
|
|
298
|
+
|
|
299
|
+
logger.info("[%s] grep(%s, %s) -> %d matches", request_id, pattern, file, len(matches))
|
|
300
|
+
return ToolResult(
|
|
301
|
+
ok=True,
|
|
302
|
+
output="\n".join(matches),
|
|
303
|
+
meta={"request_id": request_id, "count": len(matches)},
|
|
304
|
+
).to_dict()
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
@mcp.tool()
|
|
308
|
+
def read_file(file: str, max_bytes: Optional[int] = None) -> dict:
|
|
309
|
+
"""Read a file's contents (path relative to sandbox root)."""
|
|
310
|
+
request_id = new_request_id()
|
|
311
|
+
try:
|
|
312
|
+
safe_path = resolve_safe_path(file)
|
|
313
|
+
except PathSecurityError as e:
|
|
314
|
+
return ToolResult(ok=False, error=str(e)).to_dict()
|
|
315
|
+
|
|
316
|
+
if not safe_path.is_file():
|
|
317
|
+
return ToolResult(ok=False, error=f"File not found: {file}").to_dict()
|
|
318
|
+
|
|
319
|
+
try:
|
|
320
|
+
limit = max_bytes or MAX_OUTPUT_BYTES
|
|
321
|
+
with safe_path.open("rb") as f:
|
|
322
|
+
raw = f.read(limit + 1)
|
|
323
|
+
truncated = len(raw) > limit
|
|
324
|
+
text = raw[:limit].decode("utf-8", errors="replace")
|
|
325
|
+
return ToolResult(
|
|
326
|
+
ok=True, output=text,
|
|
327
|
+
meta={"request_id": request_id, "truncated": truncated, "size_bytes": safe_path.stat().st_size},
|
|
328
|
+
).to_dict()
|
|
329
|
+
except Exception as e: # noqa: BLE001
|
|
330
|
+
logger.exception("[%s] read_file failed", request_id)
|
|
331
|
+
return ToolResult(ok=False, error=str(e)).to_dict()
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@mcp.tool()
|
|
335
|
+
def write_file(file: str, content: str, overwrite: bool = True, append: bool = False) -> dict:
|
|
336
|
+
"""Write `content` to `file` (relative to sandbox root). Creates parent
|
|
337
|
+
directories as needed. Set overwrite=False to fail if the file already
|
|
338
|
+
exists; set append=True to append instead of replacing.
|
|
339
|
+
"""
|
|
340
|
+
request_id = new_request_id()
|
|
341
|
+
try:
|
|
342
|
+
safe_path = resolve_safe_path(file)
|
|
343
|
+
except PathSecurityError as e:
|
|
344
|
+
return ToolResult(ok=False, error=str(e)).to_dict()
|
|
345
|
+
|
|
346
|
+
if safe_path.exists() and not overwrite and not append:
|
|
347
|
+
return ToolResult(ok=False, error=f"File already exists: {file}").to_dict()
|
|
348
|
+
|
|
349
|
+
try:
|
|
350
|
+
safe_path.parent.mkdir(parents=True, exist_ok=True)
|
|
351
|
+
mode = "a" if append else "w"
|
|
352
|
+
with safe_path.open(mode, encoding="utf-8") as f:
|
|
353
|
+
f.write(content)
|
|
354
|
+
logger.info("[%s] wrote %d chars to %s", request_id, len(content), safe_path)
|
|
355
|
+
return ToolResult(
|
|
356
|
+
ok=True,
|
|
357
|
+
output=f"Content written to {file} successfully.",
|
|
358
|
+
meta={"request_id": request_id, "bytes_written": len(content.encode('utf-8'))},
|
|
359
|
+
).to_dict()
|
|
360
|
+
except Exception as e: # noqa: BLE001
|
|
361
|
+
logger.exception("[%s] write_file failed", request_id)
|
|
362
|
+
return ToolResult(ok=False, error=str(e)).to_dict()
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
@mcp.tool()
|
|
366
|
+
def create_folder(folder: str) -> dict:
|
|
367
|
+
"""Create a folder (relative to sandbox root), including parents."""
|
|
368
|
+
request_id = new_request_id()
|
|
369
|
+
try:
|
|
370
|
+
safe_path = resolve_safe_path(folder)
|
|
371
|
+
except PathSecurityError as e:
|
|
372
|
+
return ToolResult(ok=False, error=str(e)).to_dict()
|
|
373
|
+
|
|
374
|
+
try:
|
|
375
|
+
if safe_path.exists():
|
|
376
|
+
return ToolResult(
|
|
377
|
+
ok=True, output=f"Folder '{folder}' already exists.",
|
|
378
|
+
meta={"request_id": request_id, "created": False},
|
|
379
|
+
).to_dict()
|
|
380
|
+
safe_path.mkdir(parents=True, exist_ok=False)
|
|
381
|
+
logger.info("[%s] created folder %s", request_id, safe_path)
|
|
382
|
+
return ToolResult(
|
|
383
|
+
ok=True, output=f"Folder '{folder}' created successfully.",
|
|
384
|
+
meta={"request_id": request_id, "created": True},
|
|
385
|
+
).to_dict()
|
|
386
|
+
except Exception as e: # noqa: BLE001
|
|
387
|
+
logger.exception("[%s] create_folder failed", request_id)
|
|
388
|
+
return ToolResult(ok=False, error=str(e)).to_dict()
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
@mcp.tool()
|
|
392
|
+
def delete_folder(folder: str, confirm: bool = False) -> dict:
|
|
393
|
+
"""Delete a folder (relative to sandbox root) and everything in it.
|
|
394
|
+
Requires confirm=True as a safety guard against accidental calls.
|
|
395
|
+
Refuses to delete the sandbox root itself.
|
|
396
|
+
"""
|
|
397
|
+
request_id = new_request_id()
|
|
398
|
+
if not confirm:
|
|
399
|
+
return ToolResult(
|
|
400
|
+
ok=False,
|
|
401
|
+
error="Destructive operation requires confirm=True.",
|
|
402
|
+
).to_dict()
|
|
403
|
+
|
|
404
|
+
try:
|
|
405
|
+
safe_path = resolve_safe_path(folder)
|
|
406
|
+
except PathSecurityError as e:
|
|
407
|
+
return ToolResult(ok=False, error=str(e)).to_dict()
|
|
408
|
+
|
|
409
|
+
if safe_path == SANDBOX_ROOT:
|
|
410
|
+
return ToolResult(ok=False, error="Refusing to delete the sandbox root.").to_dict()
|
|
411
|
+
|
|
412
|
+
if not safe_path.exists():
|
|
413
|
+
return ToolResult(ok=True, output=f"Folder '{folder}' does not exist.",
|
|
414
|
+
meta={"request_id": request_id, "deleted": False}).to_dict()
|
|
415
|
+
|
|
416
|
+
try:
|
|
417
|
+
import shutil
|
|
418
|
+
shutil.rmtree(safe_path)
|
|
419
|
+
logger.warning("[%s] deleted folder %s", request_id, safe_path)
|
|
420
|
+
return ToolResult(
|
|
421
|
+
ok=True, output=f"Folder '{folder}' deleted successfully.",
|
|
422
|
+
meta={"request_id": request_id, "deleted": True},
|
|
423
|
+
).to_dict()
|
|
424
|
+
except Exception as e: # noqa: BLE001
|
|
425
|
+
logger.exception("[%s] delete_folder failed", request_id)
|
|
426
|
+
return ToolResult(ok=False, error=str(e)).to_dict()
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
@mcp.tool()
|
|
430
|
+
def health_check() -> dict:
|
|
431
|
+
"""Report server health/config — useful for monitoring and smoke tests."""
|
|
432
|
+
return ToolResult(
|
|
433
|
+
ok=True,
|
|
434
|
+
output="healthy",
|
|
435
|
+
meta={
|
|
436
|
+
"sandbox_root": str(SANDBOX_ROOT),
|
|
437
|
+
"cmd_timeout": CMD_TIMEOUT,
|
|
438
|
+
"max_output_bytes": MAX_OUTPUT_BYTES,
|
|
439
|
+
"log_dir": str(LOG_DIR),
|
|
440
|
+
"log_level": LOG_LEVEL,
|
|
441
|
+
"pid": os.getpid(),
|
|
442
|
+
},
|
|
443
|
+
).to_dict()
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
model_context_protocol_demo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
model_context_protocol_demo/main.py,sha256=WyO8xAYhuSdFZX3P-thRC9KAOFob4UzskO17evwTOuM,140
|
|
3
|
+
model_context_protocol_demo/server.py,sha256=v18w-ohLMnO3rWmDBD4KUPla7wAPMm1TY0xzoRevz1Y,15968
|
|
4
|
+
model_context_protocol_demo-0.1.0.dist-info/METADATA,sha256=utLTydgjBJj30TfHtiO0YPYvZOp4bAysQ0jhBK8u9V8,234
|
|
5
|
+
model_context_protocol_demo-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
6
|
+
model_context_protocol_demo-0.1.0.dist-info/entry_points.txt,sha256=N4sVKjXhgXSQo3NfQjhWtzGAyFbHHlH8LhuKm3InBBY,86
|
|
7
|
+
model_context_protocol_demo-0.1.0.dist-info/top_level.txt,sha256=MV4JNeSMOA9KqkJni8mrdVdDwgCzpdvAnC6U2LTbud4,28
|
|
8
|
+
model_context_protocol_demo-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
model_context_protocol_demo
|