mcp-server-pixeltable-developer 0.2.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.
- mcp_server_pixeltable_developer/__init__.py +11 -0
- mcp_server_pixeltable_developer/__main__.py +29 -0
- mcp_server_pixeltable_developer/_worker.py +51 -0
- mcp_server_pixeltable_developer/canvas.py +403 -0
- mcp_server_pixeltable_developer/conformance.py +45 -0
- mcp_server_pixeltable_developer/models.py +192 -0
- mcp_server_pixeltable_developer/prompts.py +97 -0
- mcp_server_pixeltable_developer/resources.py +134 -0
- mcp_server_pixeltable_developer/runtime.py +324 -0
- mcp_server_pixeltable_developer/server.py +125 -0
- mcp_server_pixeltable_developer/tools.py +700 -0
- mcp_server_pixeltable_developer/unsafe.py +441 -0
- mcp_server_pixeltable_developer-0.2.1.data/data/share/mcp-server-pixeltable-developer/canvas.html +243 -0
- mcp_server_pixeltable_developer-0.2.1.dist-info/METADATA +303 -0
- mcp_server_pixeltable_developer-0.2.1.dist-info/RECORD +21 -0
- mcp_server_pixeltable_developer-0.2.1.dist-info/WHEEL +4 -0
- mcp_server_pixeltable_developer-0.2.1.dist-info/entry_points.txt +2 -0
- mcp_server_pixeltable_developer-0.2.1.dist-info/licenses/LICENSE +201 -0
- mcp_server_pixeltable_stio/__init__.py +14 -0
- mcp_server_pixeltable_stio/__main__.py +6 -0
- mcp_server_pixeltable_stio/server.py +5 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"""Process configuration, path validation, and subprocess execution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import shutil
|
|
9
|
+
import signal
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from collections.abc import Sequence
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Literal
|
|
16
|
+
|
|
17
|
+
import anyio
|
|
18
|
+
from anyio.abc import ByteReceiveStream, ByteSendStream, Process
|
|
19
|
+
from mcp.server.mcpserver.exceptions import ToolError
|
|
20
|
+
from pydantic import JsonValue
|
|
21
|
+
|
|
22
|
+
Transport = Literal["stdio", "streamable-http"]
|
|
23
|
+
|
|
24
|
+
_CATALOG_COMPONENT = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
25
|
+
_HOSTED_TARGET = re.compile(r"^pxt://[A-Za-z0-9_.-]+:[A-Za-z0-9_.-]+(?:/[A-Za-z_][A-Za-z0-9_]*)*$")
|
|
26
|
+
_SECRET_ASSIGNMENT = re.compile(r"(?i)(api[_-]?key|password|secret|token)(\s*[=:]\s*)([^\s,;]+|\"[^\"]*\"|'[^']*')")
|
|
27
|
+
_URL_PASSWORD = re.compile(r"(://[^:/\s]+:)[^@/\s]+(@)")
|
|
28
|
+
_OPENAI_TOKEN = re.compile(r"\bsk-[A-Za-z0-9_-]{8,}\b")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def find_pxt_executable() -> str:
|
|
32
|
+
"""Prefer the Pixeltable CLI installed beside the running server."""
|
|
33
|
+
|
|
34
|
+
environment_pxt = os.environ.get("PIXELTABLE_MCP_PXT")
|
|
35
|
+
if environment_pxt:
|
|
36
|
+
return environment_pxt
|
|
37
|
+
sibling_pxt = Path(sys.executable).with_name("pxt")
|
|
38
|
+
if sibling_pxt.is_file():
|
|
39
|
+
return str(sibling_pxt)
|
|
40
|
+
return shutil.which("pxt") or "pxt"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def redact_secrets(text: str) -> str:
|
|
44
|
+
"""Remove common secret forms from diagnostic text."""
|
|
45
|
+
|
|
46
|
+
text = _SECRET_ASSIGNMENT.sub(r"\1\2***", text)
|
|
47
|
+
text = _URL_PASSWORD.sub(r"\1***\2", text)
|
|
48
|
+
return _OPENAI_TOKEN.sub("sk-***", text)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True, slots=True)
|
|
52
|
+
class ServerConfig:
|
|
53
|
+
"""Immutable configuration fixed when the MCP process starts."""
|
|
54
|
+
|
|
55
|
+
project_root: Path
|
|
56
|
+
pixeltable_home: Path
|
|
57
|
+
transport: Transport = "stdio"
|
|
58
|
+
unsafe_enabled: bool = False
|
|
59
|
+
pxt_executable: str = "pxt"
|
|
60
|
+
command_timeout_seconds: float = 120.0
|
|
61
|
+
max_output_bytes: int = 1_000_000
|
|
62
|
+
|
|
63
|
+
def __post_init__(self) -> None:
|
|
64
|
+
project_root = self.project_root.expanduser().resolve()
|
|
65
|
+
if not project_root.is_dir():
|
|
66
|
+
raise ValueError(f"Project root is not a directory: {project_root}")
|
|
67
|
+
pixeltable_home = self.pixeltable_home.expanduser().resolve()
|
|
68
|
+
if self.transport != "stdio" and self.unsafe_enabled:
|
|
69
|
+
raise ValueError("Unsafe tools can only be enabled over stdio")
|
|
70
|
+
if self.command_timeout_seconds <= 0:
|
|
71
|
+
raise ValueError("command_timeout_seconds must be positive")
|
|
72
|
+
if self.max_output_bytes <= 0:
|
|
73
|
+
raise ValueError("max_output_bytes must be positive")
|
|
74
|
+
object.__setattr__(self, "project_root", project_root)
|
|
75
|
+
object.__setattr__(self, "pixeltable_home", pixeltable_home)
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def from_env(cls, *, transport: Transport = "stdio") -> ServerConfig:
|
|
79
|
+
"""Load startup-only settings from the environment."""
|
|
80
|
+
|
|
81
|
+
project_root = Path(os.environ.get("PIXELTABLE_MCP_PROJECT_ROOT", os.getcwd()))
|
|
82
|
+
pixeltable_home = Path(os.environ.get("PIXELTABLE_HOME", "~/.pixeltable"))
|
|
83
|
+
unsafe_enabled = os.environ.get("PIXELTABLE_MCP_ENABLE_UNSAFE") == "1"
|
|
84
|
+
pxt_executable = find_pxt_executable()
|
|
85
|
+
return cls(
|
|
86
|
+
project_root=project_root,
|
|
87
|
+
pixeltable_home=pixeltable_home,
|
|
88
|
+
transport=transport,
|
|
89
|
+
unsafe_enabled=unsafe_enabled,
|
|
90
|
+
pxt_executable=pxt_executable,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def apply_environment(self) -> None:
|
|
94
|
+
"""Pin Pixeltable to this process's catalog before it is imported."""
|
|
95
|
+
|
|
96
|
+
os.environ["PIXELTABLE_HOME"] = str(self.pixeltable_home)
|
|
97
|
+
os.environ.setdefault("PIXELTABLE_DISABLE_STDOUT", "1")
|
|
98
|
+
|
|
99
|
+
def command_environment(self) -> dict[str, str]:
|
|
100
|
+
env = os.environ.copy()
|
|
101
|
+
env["PIXELTABLE_HOME"] = str(self.pixeltable_home)
|
|
102
|
+
env.setdefault("PIXELTABLE_DISABLE_STDOUT", "1")
|
|
103
|
+
return env
|
|
104
|
+
|
|
105
|
+
def resolve_project_file(self, value: str, *, must_exist: bool = True) -> Path:
|
|
106
|
+
"""Resolve an application path and reject project-root escapes."""
|
|
107
|
+
|
|
108
|
+
if not value.strip() or "\x00" in value:
|
|
109
|
+
raise ToolError("File path must be a non-empty project path")
|
|
110
|
+
raw = Path(value).expanduser()
|
|
111
|
+
candidate = (raw if raw.is_absolute() else self.project_root / raw).resolve(strict=False)
|
|
112
|
+
try:
|
|
113
|
+
candidate.relative_to(self.project_root)
|
|
114
|
+
except ValueError as exc:
|
|
115
|
+
raise ToolError("File path must stay inside the configured project root") from exc
|
|
116
|
+
if must_exist and not candidate.is_file():
|
|
117
|
+
raise ToolError(f"Project file does not exist: {value}")
|
|
118
|
+
return candidate
|
|
119
|
+
|
|
120
|
+
def resolve_scaffold_output(self, value: str) -> Path:
|
|
121
|
+
"""Validate a new project-relative scaffold path."""
|
|
122
|
+
|
|
123
|
+
raw = Path(value)
|
|
124
|
+
if raw.is_absolute():
|
|
125
|
+
raise ToolError("Scaffold output must be relative to the project root")
|
|
126
|
+
candidate = self.resolve_project_file(value, must_exist=False)
|
|
127
|
+
if candidate.exists():
|
|
128
|
+
raise ToolError(f"Refusing to overwrite existing path: {value}")
|
|
129
|
+
if not candidate.parent.is_dir():
|
|
130
|
+
raise ToolError(f"Scaffold output directory does not exist: {candidate.parent}")
|
|
131
|
+
return candidate
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def validate_catalog_path(value: str, *, allow_empty: bool = False) -> str:
|
|
135
|
+
"""Validate a local Pixeltable catalog path."""
|
|
136
|
+
|
|
137
|
+
value = value.strip().strip("/")
|
|
138
|
+
if not value and allow_empty:
|
|
139
|
+
return ""
|
|
140
|
+
if not value or any(not _CATALOG_COMPONENT.fullmatch(part) for part in value.split("/")):
|
|
141
|
+
raise ToolError(f"Invalid Pixeltable catalog path: {value!r}")
|
|
142
|
+
return value
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def validate_target(value: str) -> str:
|
|
146
|
+
"""Validate a local catalog path or an explicit hosted database URI."""
|
|
147
|
+
|
|
148
|
+
value = value.strip().rstrip("/")
|
|
149
|
+
if value.startswith("pxt://"):
|
|
150
|
+
if not _HOSTED_TARGET.fullmatch(value):
|
|
151
|
+
raise ToolError(f"Invalid hosted Pixeltable target: {value!r}")
|
|
152
|
+
return value
|
|
153
|
+
return ServerConfig.validate_catalog_path(value)
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
def validate_identifier(value: str, *, label: str = "identifier") -> str:
|
|
157
|
+
value = value.strip()
|
|
158
|
+
if not _CATALOG_COMPONENT.fullmatch(value):
|
|
159
|
+
raise ToolError(f"Invalid {label}: {value!r}")
|
|
160
|
+
return value
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass(frozen=True, slots=True)
|
|
164
|
+
class ProcessResult:
|
|
165
|
+
exit_code: int
|
|
166
|
+
stdout: str
|
|
167
|
+
stderr: str
|
|
168
|
+
data: JsonValue
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def pending(self) -> bool:
|
|
172
|
+
return self.exit_code == 2
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class CommandRunner:
|
|
176
|
+
"""Run fixed argument arrays with captured, redacted output."""
|
|
177
|
+
|
|
178
|
+
def __init__(self, config: ServerConfig):
|
|
179
|
+
self.config = config
|
|
180
|
+
|
|
181
|
+
async def pxt(
|
|
182
|
+
self,
|
|
183
|
+
arguments: Sequence[str],
|
|
184
|
+
*,
|
|
185
|
+
allowed_exit_codes: frozenset[int] = frozenset({0}),
|
|
186
|
+
parse_json: bool = True,
|
|
187
|
+
) -> ProcessResult:
|
|
188
|
+
return await self.run(
|
|
189
|
+
[self.config.pxt_executable, *arguments],
|
|
190
|
+
allowed_exit_codes=allowed_exit_codes,
|
|
191
|
+
parse_json=parse_json,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
async def worker(self, operation: str, payload: object) -> ProcessResult:
|
|
195
|
+
worker_path = Path(__file__).with_name("_worker.py")
|
|
196
|
+
return await self.run(
|
|
197
|
+
[sys.executable, str(worker_path), operation],
|
|
198
|
+
input_bytes=json.dumps(payload).encode(),
|
|
199
|
+
allowed_exit_codes=frozenset({0}),
|
|
200
|
+
parse_json=True,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
async def run(
|
|
204
|
+
self,
|
|
205
|
+
arguments: Sequence[str],
|
|
206
|
+
*,
|
|
207
|
+
input_bytes: bytes | None = None,
|
|
208
|
+
allowed_exit_codes: frozenset[int],
|
|
209
|
+
parse_json: bool,
|
|
210
|
+
) -> ProcessResult:
|
|
211
|
+
"""Execute a known command without a shell and normalize its result."""
|
|
212
|
+
|
|
213
|
+
process: Process | None = None
|
|
214
|
+
stdout_buffer = bytearray()
|
|
215
|
+
stderr_buffer = bytearray()
|
|
216
|
+
output_limit_exceeded = False
|
|
217
|
+
|
|
218
|
+
def terminate_process() -> None:
|
|
219
|
+
if process is None or process.returncode is not None:
|
|
220
|
+
return
|
|
221
|
+
try:
|
|
222
|
+
if os.name == "posix":
|
|
223
|
+
os.killpg(process.pid, signal.SIGKILL)
|
|
224
|
+
else:
|
|
225
|
+
process.kill()
|
|
226
|
+
except OSError:
|
|
227
|
+
pass
|
|
228
|
+
|
|
229
|
+
async def drain_stream(stream: ByteReceiveStream, destination: bytearray) -> None:
|
|
230
|
+
nonlocal output_limit_exceeded
|
|
231
|
+
while True:
|
|
232
|
+
try:
|
|
233
|
+
chunk = await stream.receive()
|
|
234
|
+
except anyio.EndOfStream:
|
|
235
|
+
return
|
|
236
|
+
remaining = self.config.max_output_bytes + 1 - len(destination)
|
|
237
|
+
if remaining > 0:
|
|
238
|
+
destination.extend(chunk[:remaining])
|
|
239
|
+
if len(destination) > self.config.max_output_bytes:
|
|
240
|
+
output_limit_exceeded = True
|
|
241
|
+
terminate_process()
|
|
242
|
+
return
|
|
243
|
+
|
|
244
|
+
async def send_input(stream: ByteSendStream, value: bytes) -> None:
|
|
245
|
+
try:
|
|
246
|
+
await stream.send(value)
|
|
247
|
+
except (anyio.BrokenResourceError, anyio.ClosedResourceError):
|
|
248
|
+
if not output_limit_exceeded:
|
|
249
|
+
raise
|
|
250
|
+
finally:
|
|
251
|
+
await stream.aclose()
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
started_process = await anyio.open_process(
|
|
255
|
+
list(arguments),
|
|
256
|
+
stdin=subprocess.PIPE if input_bytes is not None else subprocess.DEVNULL,
|
|
257
|
+
stdout=subprocess.PIPE,
|
|
258
|
+
stderr=subprocess.PIPE,
|
|
259
|
+
cwd=self.config.project_root,
|
|
260
|
+
env=self.config.command_environment(),
|
|
261
|
+
start_new_session=os.name == "posix",
|
|
262
|
+
)
|
|
263
|
+
process = started_process
|
|
264
|
+
with anyio.fail_after(self.config.command_timeout_seconds):
|
|
265
|
+
async with anyio.create_task_group() as task_group:
|
|
266
|
+
if started_process.stdout is not None:
|
|
267
|
+
task_group.start_soon(drain_stream, started_process.stdout, stdout_buffer)
|
|
268
|
+
if started_process.stderr is not None:
|
|
269
|
+
task_group.start_soon(drain_stream, started_process.stderr, stderr_buffer)
|
|
270
|
+
if started_process.stdin is not None and input_bytes is not None:
|
|
271
|
+
task_group.start_soon(send_input, started_process.stdin, input_bytes)
|
|
272
|
+
await started_process.wait()
|
|
273
|
+
except TimeoutError as exc:
|
|
274
|
+
raise ToolError(
|
|
275
|
+
f"Pixeltable command timed out after {self.config.command_timeout_seconds:g} seconds"
|
|
276
|
+
) from exc
|
|
277
|
+
except FileNotFoundError as exc:
|
|
278
|
+
raise ToolError(f"Pixeltable executable was not found: {arguments[0]}") from exc
|
|
279
|
+
except OSError as exc:
|
|
280
|
+
raise ToolError(f"Could not start Pixeltable command: {exc}") from exc
|
|
281
|
+
finally:
|
|
282
|
+
if process is not None:
|
|
283
|
+
terminate_process()
|
|
284
|
+
with anyio.CancelScope(shield=True):
|
|
285
|
+
with anyio.move_on_after(5):
|
|
286
|
+
await process.wait()
|
|
287
|
+
await process.aclose()
|
|
288
|
+
|
|
289
|
+
if output_limit_exceeded:
|
|
290
|
+
raise ToolError("Pixeltable command output exceeded the configured limit")
|
|
291
|
+
assert process is not None and process.returncode is not None
|
|
292
|
+
stdout_bytes = bytes(stdout_buffer)
|
|
293
|
+
stderr_bytes = bytes(stderr_buffer)
|
|
294
|
+
stdout = redact_secrets(stdout_bytes.decode(errors="replace").strip())
|
|
295
|
+
stderr = redact_secrets(stderr_bytes.decode(errors="replace").strip())
|
|
296
|
+
if process.returncode not in allowed_exit_codes:
|
|
297
|
+
detail = stderr or stdout or f"exit code {process.returncode}"
|
|
298
|
+
raise ToolError(f"Pixeltable command failed: {detail}")
|
|
299
|
+
|
|
300
|
+
data: JsonValue = None
|
|
301
|
+
if parse_json:
|
|
302
|
+
data = self._parse_json(stdout)
|
|
303
|
+
elif stdout:
|
|
304
|
+
data = stdout
|
|
305
|
+
return ProcessResult(exit_code=process.returncode, stdout=stdout, stderr=stderr, data=data)
|
|
306
|
+
|
|
307
|
+
@staticmethod
|
|
308
|
+
def _parse_json(stdout: str) -> JsonValue:
|
|
309
|
+
if not stdout:
|
|
310
|
+
return None
|
|
311
|
+
try:
|
|
312
|
+
return json.loads(stdout)
|
|
313
|
+
except json.JSONDecodeError:
|
|
314
|
+
decoder = json.JSONDecoder()
|
|
315
|
+
for index, character in enumerate(stdout):
|
|
316
|
+
if character not in "[{":
|
|
317
|
+
continue
|
|
318
|
+
try:
|
|
319
|
+
value, end = decoder.raw_decode(stdout[index:])
|
|
320
|
+
except json.JSONDecodeError:
|
|
321
|
+
continue
|
|
322
|
+
if not stdout[index + end :].strip():
|
|
323
|
+
return value
|
|
324
|
+
raise ToolError("Pixeltable command returned invalid JSON")
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""MCP 2.2 server construction for Pixeltable developer workflows."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
from collections.abc import AsyncIterator
|
|
9
|
+
from contextlib import asynccontextmanager
|
|
10
|
+
from dataclasses import dataclass, replace
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from mcp.server.caching import CacheHint
|
|
14
|
+
from mcp.server.mcpserver import MCPServer
|
|
15
|
+
|
|
16
|
+
from . import __version__
|
|
17
|
+
from .prompts import register_prompts
|
|
18
|
+
from .resources import register_resources
|
|
19
|
+
from .runtime import CommandRunner, ServerConfig
|
|
20
|
+
from .tools import register_default_tools
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
SERVER_INSTRUCTIONS = """Pixeltable developer server for source-controlled multimodal applications.
|
|
25
|
+
Read pixeltable://guidance/app before scaffolding or changing an application. Prefer read-only describe,
|
|
26
|
+
check, and diff calls before updates. Schema and service updates may affect hosted targets; prune tools
|
|
27
|
+
default to dry runs. Keep tables, computed columns, and HTTP routes in the application file. Unsafe local
|
|
28
|
+
Python, package-install, and display tools are absent unless PIXELTABLE_MCP_ENABLE_UNSAFE=1 over stdio."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, slots=True)
|
|
32
|
+
class RuntimeState:
|
|
33
|
+
"""Dependencies fixed for one MCP server lifetime."""
|
|
34
|
+
|
|
35
|
+
config: ServerConfig
|
|
36
|
+
runner: CommandRunner
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def create_server(config: ServerConfig | None = None) -> MCPServer[RuntimeState]:
|
|
40
|
+
"""Build a fully registered MCP server without starting a transport."""
|
|
41
|
+
|
|
42
|
+
config = config or ServerConfig.from_env(transport="stdio")
|
|
43
|
+
if config.unsafe_enabled and os.environ.get("PIXELTABLE_MCP_ENABLE_UNSAFE") != "1":
|
|
44
|
+
config = replace(config, unsafe_enabled=False)
|
|
45
|
+
config.apply_environment()
|
|
46
|
+
runner = CommandRunner(config)
|
|
47
|
+
state = RuntimeState(config=config, runner=runner)
|
|
48
|
+
unsafe_runtime: list[Any] = []
|
|
49
|
+
|
|
50
|
+
@asynccontextmanager
|
|
51
|
+
async def lifespan(_: MCPServer[RuntimeState]) -> AsyncIterator[RuntimeState]:
|
|
52
|
+
config.apply_environment()
|
|
53
|
+
try:
|
|
54
|
+
yield state
|
|
55
|
+
finally:
|
|
56
|
+
for runtime in reversed(unsafe_runtime):
|
|
57
|
+
close = getattr(runtime, "close", None)
|
|
58
|
+
if close is None:
|
|
59
|
+
continue
|
|
60
|
+
result = close()
|
|
61
|
+
if inspect.isawaitable(result):
|
|
62
|
+
await result
|
|
63
|
+
|
|
64
|
+
server: MCPServer[RuntimeState] = MCPServer(
|
|
65
|
+
"pixeltable-developer",
|
|
66
|
+
title="Pixeltable Developer",
|
|
67
|
+
description="Local developer tools for Pixeltable application and service workflows.",
|
|
68
|
+
instructions=SERVER_INSTRUCTIONS,
|
|
69
|
+
website_url="https://github.com/pixeltable/mcp-server-pixeltable-developer",
|
|
70
|
+
version=__version__,
|
|
71
|
+
lifespan=lifespan,
|
|
72
|
+
cache_hints={
|
|
73
|
+
"server/discover": CacheHint(ttl_ms=300_000, scope="private"),
|
|
74
|
+
"tools/list": CacheHint(ttl_ms=300_000, scope="private"),
|
|
75
|
+
"resources/list": CacheHint(ttl_ms=300_000, scope="private"),
|
|
76
|
+
"prompts/list": CacheHint(ttl_ms=300_000, scope="private"),
|
|
77
|
+
"resources/read": CacheHint(ttl_ms=0, scope="private"),
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
register_default_tools(server, config, runner)
|
|
81
|
+
register_resources(server, config, runner)
|
|
82
|
+
register_prompts(server)
|
|
83
|
+
|
|
84
|
+
if config.unsafe_enabled:
|
|
85
|
+
from .unsafe import register_unsafe_tools
|
|
86
|
+
|
|
87
|
+
unsafe_runtime.append(register_unsafe_tools(server, config))
|
|
88
|
+
|
|
89
|
+
return server
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def create_conformance_app(config: ServerConfig | None = None) -> Any:
|
|
93
|
+
"""Create a stateless Streamable HTTP app for local conformance tests only."""
|
|
94
|
+
|
|
95
|
+
if config is None:
|
|
96
|
+
env_config = ServerConfig.from_env(transport="streamable-http")
|
|
97
|
+
config = ServerConfig(
|
|
98
|
+
project_root=env_config.project_root,
|
|
99
|
+
pixeltable_home=env_config.pixeltable_home,
|
|
100
|
+
transport="streamable-http",
|
|
101
|
+
unsafe_enabled=False,
|
|
102
|
+
pxt_executable=env_config.pxt_executable,
|
|
103
|
+
command_timeout_seconds=env_config.command_timeout_seconds,
|
|
104
|
+
max_output_bytes=env_config.max_output_bytes,
|
|
105
|
+
)
|
|
106
|
+
elif config.transport != "streamable-http":
|
|
107
|
+
raise ValueError("Conformance app requires transport='streamable-http'")
|
|
108
|
+
server = create_server(config)
|
|
109
|
+
from .conformance import register_conformance_diagnostics
|
|
110
|
+
|
|
111
|
+
register_conformance_diagnostics(server)
|
|
112
|
+
return server.streamable_http_app(stateless_http=True, json_response=True, host="127.0.0.1")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
mcp = create_server()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def main() -> None:
|
|
119
|
+
"""Run the production stdio server and propagate startup failures."""
|
|
120
|
+
|
|
121
|
+
logger.info("Starting Pixeltable Developer MCP %s", __version__)
|
|
122
|
+
mcp.run("stdio")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
__all__ = ["RuntimeState", "create_conformance_app", "create_server", "main", "mcp"]
|