pi-codemcp 0.1.0
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.
- package/.python-version +1 -0
- package/LICENSE +21 -0
- package/README.md +143 -0
- package/extensions/index.ts +188 -0
- package/package.json +91 -0
- package/sidecar/__init__.py +1 -0
- package/sidecar/catalog_cache.py +89 -0
- package/sidecar/chains.py +316 -0
- package/sidecar/executor.py +591 -0
- package/sidecar/gateway.py +893 -0
- package/sidecar/json_types.py +11 -0
- package/sidecar/mcp_config.py +278 -0
- package/sidecar/models.py +92 -0
- package/sidecar/pyproject.toml +144 -0
- package/sidecar/settings.py +59 -0
- package/sidecar/tool_catalog.py +838 -0
- package/sidecar/uv.lock +1775 -0
- package/src/chains.ts +452 -0
- package/src/config.ts +58 -0
- package/src/errors.ts +9 -0
- package/src/execution-rendering.ts +183 -0
- package/src/json-file.ts +54 -0
- package/src/lifecycle.ts +59 -0
- package/src/mcp-client.ts +303 -0
- package/src/modal.ts +1233 -0
- package/src/output.ts +52 -0
- package/src/settings.ts +144 -0
- package/src/tools.ts +332 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import time
|
|
8
|
+
from contextlib import contextmanager, suppress
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING, Literal, NamedTuple
|
|
11
|
+
from uuid import uuid4
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
14
|
+
|
|
15
|
+
from .json_types import JSON_OBJECT_ADAPTER, JsonObject
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from collections.abc import Iterator
|
|
19
|
+
|
|
20
|
+
CHAIN_NAME_PATTERN = r"^[a-z][a-z0-9_]{0,63}$"
|
|
21
|
+
CHAIN_STORE_VERSION: Literal[1] = 1
|
|
22
|
+
type ChainScope = Literal["global", "project"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ChainDependency(BaseModel):
|
|
26
|
+
model_config = ConfigDict(extra="forbid", strict=True)
|
|
27
|
+
|
|
28
|
+
kind: Literal["mcp_tool", "saved_chain"]
|
|
29
|
+
name: str
|
|
30
|
+
call: str
|
|
31
|
+
server: str
|
|
32
|
+
schema_fingerprint: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SavedChainManifest(BaseModel):
|
|
36
|
+
model_config = ConfigDict(extra="forbid", strict=True)
|
|
37
|
+
|
|
38
|
+
version: Literal[1] = CHAIN_STORE_VERSION
|
|
39
|
+
id: str
|
|
40
|
+
name: str = Field(pattern=CHAIN_NAME_PATTERN)
|
|
41
|
+
description: str = Field(min_length=1, max_length=1000)
|
|
42
|
+
code: str = Field(min_length=1)
|
|
43
|
+
input_schema: JsonObject
|
|
44
|
+
output_schema: JsonObject
|
|
45
|
+
enabled: bool = True
|
|
46
|
+
dependencies: list[ChainDependency] = Field(default_factory=list)
|
|
47
|
+
schema_fingerprint: str
|
|
48
|
+
created_at: float
|
|
49
|
+
updated_at: float
|
|
50
|
+
validated_at: float
|
|
51
|
+
|
|
52
|
+
@field_validator("code")
|
|
53
|
+
@classmethod
|
|
54
|
+
def code_must_not_be_blank(cls, value: str) -> str:
|
|
55
|
+
if not value.strip():
|
|
56
|
+
raise ValueError("code must not be blank")
|
|
57
|
+
return value
|
|
58
|
+
|
|
59
|
+
@field_validator("input_schema")
|
|
60
|
+
@classmethod
|
|
61
|
+
def input_schema_must_be_object(cls, value: JsonObject) -> JsonObject:
|
|
62
|
+
if value.get("type") != "object":
|
|
63
|
+
raise ValueError("input_schema.type must be object")
|
|
64
|
+
return value
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def public_name(self) -> str:
|
|
68
|
+
return f"chain_{self.name}"
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def call(self) -> str:
|
|
72
|
+
return f"chains.{self.name}"
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def native_tool(self) -> str:
|
|
76
|
+
return f"mcp_chain_{self.name}"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ChainEnabledChange(BaseModel):
|
|
80
|
+
model_config = ConfigDict(extra="forbid", strict=True)
|
|
81
|
+
|
|
82
|
+
name: str = Field(pattern=CHAIN_NAME_PATTERN)
|
|
83
|
+
scope: ChainScope
|
|
84
|
+
enabled: bool
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ChainStatusView(BaseModel):
|
|
88
|
+
model_config = ConfigDict(extra="forbid", strict=True)
|
|
89
|
+
|
|
90
|
+
chain: SavedChainManifest
|
|
91
|
+
scope: ChainScope
|
|
92
|
+
status: Literal["ready", "disabled", "stale", "shadowed"]
|
|
93
|
+
stale_dependencies: list[str] = Field(default_factory=list)
|
|
94
|
+
called_by: list[str] = Field(default_factory=list)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class ChainListResponse(BaseModel):
|
|
98
|
+
model_config = ConfigDict(extra="forbid", strict=True)
|
|
99
|
+
|
|
100
|
+
chains: list[ChainStatusView]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class SaveChainResponse(BaseModel):
|
|
104
|
+
model_config = ConfigDict(extra="forbid", strict=True)
|
|
105
|
+
|
|
106
|
+
chain: ChainStatusView
|
|
107
|
+
created: bool
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class ChainStore:
|
|
111
|
+
def __init__(self, directory: Path) -> None:
|
|
112
|
+
self.directory = Path(directory)
|
|
113
|
+
|
|
114
|
+
def load_all(self) -> list[SavedChainManifest]:
|
|
115
|
+
if not self.directory.exists():
|
|
116
|
+
return []
|
|
117
|
+
manifests: list[SavedChainManifest] = []
|
|
118
|
+
for path in sorted(self.directory.glob("*.json")):
|
|
119
|
+
try:
|
|
120
|
+
manifests.append(
|
|
121
|
+
SavedChainManifest.model_validate_json(path.read_text(encoding="utf-8"))
|
|
122
|
+
)
|
|
123
|
+
except (OSError, ValueError) as error:
|
|
124
|
+
raise ValueError(f"Invalid saved chain manifest {path}: {error}") from error
|
|
125
|
+
return manifests
|
|
126
|
+
|
|
127
|
+
def enabled(self) -> list[SavedChainManifest]:
|
|
128
|
+
return [chain for chain in self.load_all() if chain.enabled]
|
|
129
|
+
|
|
130
|
+
def contains(self, name: str) -> bool:
|
|
131
|
+
self._validate_name(name)
|
|
132
|
+
return self._path(name).is_file()
|
|
133
|
+
|
|
134
|
+
def get(self, name: str) -> SavedChainManifest:
|
|
135
|
+
self._validate_name(name)
|
|
136
|
+
path = self._path(name)
|
|
137
|
+
try:
|
|
138
|
+
return SavedChainManifest.model_validate_json(path.read_text(encoding="utf-8"))
|
|
139
|
+
except FileNotFoundError as error:
|
|
140
|
+
raise ValueError(f"Unknown saved chain: {name}") from error
|
|
141
|
+
except (OSError, ValueError) as error:
|
|
142
|
+
raise ValueError(f"Invalid saved chain manifest {path}: {error}") from error
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def build(
|
|
146
|
+
*,
|
|
147
|
+
name: str,
|
|
148
|
+
description: str,
|
|
149
|
+
code: str,
|
|
150
|
+
input_schema: JsonObject,
|
|
151
|
+
output_schema: JsonObject,
|
|
152
|
+
dependencies: list[ChainDependency],
|
|
153
|
+
previous: SavedChainManifest | None = None,
|
|
154
|
+
) -> SavedChainManifest:
|
|
155
|
+
now = time.time()
|
|
156
|
+
validated_input_schema = JSON_OBJECT_ADAPTER.validate_python(input_schema)
|
|
157
|
+
validated_output_schema = JSON_OBJECT_ADAPTER.validate_python(output_schema)
|
|
158
|
+
schema_fingerprint = _fingerprint({
|
|
159
|
+
"input_schema": validated_input_schema,
|
|
160
|
+
"output_schema": validated_output_schema,
|
|
161
|
+
})
|
|
162
|
+
return SavedChainManifest(
|
|
163
|
+
id=previous.id if previous is not None else uuid4().hex,
|
|
164
|
+
name=name,
|
|
165
|
+
description=description,
|
|
166
|
+
code=code,
|
|
167
|
+
input_schema=validated_input_schema,
|
|
168
|
+
output_schema=validated_output_schema,
|
|
169
|
+
enabled=previous.enabled if previous is not None else True,
|
|
170
|
+
dependencies=dependencies,
|
|
171
|
+
schema_fingerprint=schema_fingerprint,
|
|
172
|
+
created_at=previous.created_at if previous is not None else now,
|
|
173
|
+
updated_at=now,
|
|
174
|
+
validated_at=now,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def save(self, chain: SavedChainManifest) -> None:
|
|
178
|
+
self.directory.mkdir(parents=True, exist_ok=True)
|
|
179
|
+
with suppress(OSError):
|
|
180
|
+
self.directory.chmod(0o700)
|
|
181
|
+
path = self._path(chain.name)
|
|
182
|
+
temporary = self.directory / f".{chain.name}.{os.getpid()}.{time.time_ns()}.tmp"
|
|
183
|
+
temporary.write_text(
|
|
184
|
+
f"{chain.model_dump_json(indent=2)}\n",
|
|
185
|
+
encoding="utf-8",
|
|
186
|
+
)
|
|
187
|
+
temporary.chmod(0o600)
|
|
188
|
+
temporary.replace(path)
|
|
189
|
+
|
|
190
|
+
def delete(self, name: str) -> None:
|
|
191
|
+
self._validate_name(name)
|
|
192
|
+
try:
|
|
193
|
+
self._path(name).unlink()
|
|
194
|
+
except FileNotFoundError as error:
|
|
195
|
+
raise ValueError(f"Unknown saved chain: {name}") from error
|
|
196
|
+
|
|
197
|
+
def _path(self, name: str) -> Path:
|
|
198
|
+
self._validate_name(name)
|
|
199
|
+
return self.directory / f"{name}.json"
|
|
200
|
+
|
|
201
|
+
@staticmethod
|
|
202
|
+
def _validate_name(name: str) -> None:
|
|
203
|
+
if re.fullmatch(CHAIN_NAME_PATTERN, name) is None:
|
|
204
|
+
raise ValueError(
|
|
205
|
+
"Saved chain name must start with a lowercase letter and contain only "
|
|
206
|
+
"lowercase letters, digits, and underscores (maximum 64 characters)"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class ScopedChain(NamedTuple):
|
|
211
|
+
scope: ChainScope
|
|
212
|
+
chain: SavedChainManifest
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class ScopedChainStore:
|
|
216
|
+
def __init__(self, global_directory: Path, project_directory: Path | None) -> None:
|
|
217
|
+
self.global_store = ChainStore(global_directory)
|
|
218
|
+
self.project_store = (
|
|
219
|
+
ChainStore(project_directory) if project_directory is not None else None
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def load_all(self) -> list[ScopedChain]:
|
|
223
|
+
chains = [
|
|
224
|
+
ScopedChain(scope="global", chain=chain) for chain in self.global_store.load_all()
|
|
225
|
+
]
|
|
226
|
+
if self.project_store is not None:
|
|
227
|
+
chains.extend(
|
|
228
|
+
ScopedChain(scope="project", chain=chain) for chain in self.project_store.load_all()
|
|
229
|
+
)
|
|
230
|
+
return sorted(
|
|
231
|
+
chains,
|
|
232
|
+
key=lambda item: (item.chain.name, item.scope != "project"),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
def effective(self) -> list[ScopedChain]:
|
|
236
|
+
effective: dict[str, ScopedChain] = {}
|
|
237
|
+
for item in self.load_all():
|
|
238
|
+
current = effective.get(item.chain.name)
|
|
239
|
+
if current is None or item.scope == "project":
|
|
240
|
+
effective[item.chain.name] = item
|
|
241
|
+
return [effective[name] for name in sorted(effective)]
|
|
242
|
+
|
|
243
|
+
def enabled(self) -> list[SavedChainManifest]:
|
|
244
|
+
return [item.chain for item in self.effective() if item.chain.enabled]
|
|
245
|
+
|
|
246
|
+
def get(self, name: str, scope: ChainScope | None = None) -> ScopedChain:
|
|
247
|
+
if scope is not None:
|
|
248
|
+
return ScopedChain(scope=scope, chain=self._store(scope).get(name))
|
|
249
|
+
if self.project_store is not None and self.project_store.contains(name):
|
|
250
|
+
return ScopedChain(scope="project", chain=self.project_store.get(name))
|
|
251
|
+
return ScopedChain(scope="global", chain=self.global_store.get(name))
|
|
252
|
+
|
|
253
|
+
def contains(self, scope: ChainScope, name: str) -> bool:
|
|
254
|
+
return self._store(scope).contains(name)
|
|
255
|
+
|
|
256
|
+
def save(self, scope: ChainScope, chain: SavedChainManifest) -> None:
|
|
257
|
+
self._store(scope).save(chain)
|
|
258
|
+
|
|
259
|
+
@contextmanager
|
|
260
|
+
def enabled_transaction(self, changes: list[ChainEnabledChange]) -> Iterator[None]:
|
|
261
|
+
keys = [(change.scope, change.name) for change in changes]
|
|
262
|
+
if len(keys) != len(set(keys)):
|
|
263
|
+
raise ValueError("Duplicate scoped saved-chain enable change")
|
|
264
|
+
previous = [self.get(change.name, change.scope) for change in changes]
|
|
265
|
+
applied: list[ScopedChain] = []
|
|
266
|
+
try:
|
|
267
|
+
self._apply_enabled_changes(changes, previous, applied)
|
|
268
|
+
yield
|
|
269
|
+
except BaseException:
|
|
270
|
+
self._restore(applied)
|
|
271
|
+
raise
|
|
272
|
+
|
|
273
|
+
def _apply_enabled_changes(
|
|
274
|
+
self,
|
|
275
|
+
changes: list[ChainEnabledChange],
|
|
276
|
+
previous: list[ScopedChain],
|
|
277
|
+
applied: list[ScopedChain],
|
|
278
|
+
) -> None:
|
|
279
|
+
for change, current in zip(changes, previous, strict=True):
|
|
280
|
+
if current.chain.enabled == change.enabled:
|
|
281
|
+
continue
|
|
282
|
+
updated = current.chain.model_copy(
|
|
283
|
+
update={"enabled": change.enabled, "updated_at": time.time()}
|
|
284
|
+
)
|
|
285
|
+
self.save(change.scope, updated)
|
|
286
|
+
applied.append(current)
|
|
287
|
+
|
|
288
|
+
def _restore(self, chains: list[ScopedChain]) -> None:
|
|
289
|
+
for item in reversed(chains):
|
|
290
|
+
self.save(item.scope, item.chain)
|
|
291
|
+
|
|
292
|
+
def delete(self, scope: ChainScope, name: str) -> None:
|
|
293
|
+
self._store(scope).delete(name)
|
|
294
|
+
|
|
295
|
+
def is_shadowed(self, item: ScopedChain) -> bool:
|
|
296
|
+
return (
|
|
297
|
+
item.scope == "global"
|
|
298
|
+
and self.project_store is not None
|
|
299
|
+
and self.project_store.contains(item.chain.name)
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
def _store(self, scope: ChainScope) -> ChainStore:
|
|
303
|
+
if scope == "global":
|
|
304
|
+
return self.global_store
|
|
305
|
+
if self.project_store is None:
|
|
306
|
+
raise ValueError("Project saved-chain scope is unavailable for this session")
|
|
307
|
+
return self.project_store
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def schema_fingerprint(input_schema: JsonObject, output_schema: JsonObject) -> str:
|
|
311
|
+
return _fingerprint({"input_schema": input_schema, "output_schema": output_schema})
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _fingerprint(value: JsonObject) -> str:
|
|
315
|
+
encoded = json.dumps(value, sort_keys=True, separators=(",", ":"), default=str)
|
|
316
|
+
return hashlib.sha256(encoded.encode("utf-8")).hexdigest()
|