pi-codemcp 1.3.2 → 1.4.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/README.md +3 -0
- package/package.json +1 -1
- package/sidecar/executor.py +21 -5
- package/sidecar/gateway.py +52 -0
- package/sidecar/sandbox_api.py +1 -1
- package/sidecar/stats.py +3 -0
- package/sidecar/tool_catalog.py +2 -1
- package/src/execution-rendering.ts +6 -0
- package/src/mcp-client.ts +2 -0
- package/src/tools.ts +80 -26
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ Instead of putting every upstream MCP tool definition into the model context, pi
|
|
|
7
7
|
- `codemcp_search` ranks capabilities or pages through a compact inventory without loading full schemas.
|
|
8
8
|
- `codemcp_inspect` returns exact typed SDK stubs only for selected calls.
|
|
9
9
|
- `codemcp_execute` runs one sandboxed Python call graph across one or many MCP servers.
|
|
10
|
+
- `codemcp_edit` applies one exact replacement to the previous execution and reruns it without resending the full code.
|
|
10
11
|
- `codemcp_save_chain` turns a repeated call graph into a reusable native Pi tool.
|
|
11
12
|
- `codemcp_manage_chains` lists chains or performs an explicitly confirmed enable, disable, revalidate, or delete.
|
|
12
13
|
|
|
@@ -177,6 +178,8 @@ Incomplete upstream schemas become recursive `JsonValue`, not `Any`; use the pre
|
|
|
177
178
|
|
|
178
179
|
When an oversized value fits the bounded in-memory refinement cache, the failure also returns an opaque `result_ref` and expiry. Pass that reference back as `inputRef` on one follow-up `codemcp_execute`; the retained JSON is exposed as `input`, so code can filter or aggregate it without repeating upstream calls. References expire after five minutes, are valid only in the originating sidecar, and are never persisted.
|
|
179
180
|
|
|
181
|
+
For a small correction, `codemcp_edit` replaces one uniquely matching `oldText` with `newText` in the most recent execution and reruns it with the same `inputRef`. The state is in-memory and disappears when the sidecar restarts. The full call graph runs again, including upstream MCP calls.
|
|
182
|
+
|
|
180
183
|
## Saved-chain CLI flow
|
|
181
184
|
|
|
182
185
|
Saved chains are JSON manifests with sandboxed code plus explicit input/output JSON Schemas. Project-scoped chains live under `<project>/.pi/pi-codemcp/chains`; global chains live under `<agent-dir>/pi-codemcp/chains`.
|
package/package.json
CHANGED
package/sidecar/executor.py
CHANGED
|
@@ -50,9 +50,10 @@ INSPECT_BYTE_LIMIT = 8 * 1024
|
|
|
50
50
|
CHAIN_INPUT_EXTERNAL = "__codemcp_saved_chain_input"
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
type FailureStage = Literal["preflight", "runtime", "timeout", "cancelled", "result"]
|
|
53
|
+
type FailureStage = Literal["preflight", "arguments", "runtime", "timeout", "cancelled", "result"]
|
|
54
54
|
type FailureKind = Literal[
|
|
55
55
|
"preflight",
|
|
56
|
+
"argument_validation",
|
|
56
57
|
"result",
|
|
57
58
|
"result_reference",
|
|
58
59
|
"sandbox_runtime",
|
|
@@ -487,7 +488,18 @@ class MontyExecutor:
|
|
|
487
488
|
raise RuntimeError(
|
|
488
489
|
f"Call limit exceeded: maximum {context.settings.max_calls} total calls"
|
|
489
490
|
)
|
|
490
|
-
|
|
491
|
+
try:
|
|
492
|
+
validated = catalog.validate_arguments(name, arguments)
|
|
493
|
+
except (TypeError, ValidationError, ValueError) as error:
|
|
494
|
+
message = f"{spec.call}: invalid arguments: {error}"
|
|
495
|
+
context.failure = ExecutionFailureInfo(
|
|
496
|
+
kind="argument_validation",
|
|
497
|
+
server=spec.server,
|
|
498
|
+
tool=spec.backend_name,
|
|
499
|
+
retryable=False,
|
|
500
|
+
message=message,
|
|
501
|
+
)
|
|
502
|
+
raise ValueError(message) from error
|
|
491
503
|
if spec.kind == "saved_chain":
|
|
492
504
|
context.chain_calls += 1
|
|
493
505
|
return await context.call_tool(name, validated, context)
|
|
@@ -624,8 +636,10 @@ class MontyExecutor:
|
|
|
624
636
|
context.metrics.runtime_ms += _elapsed_ms(runtime_started)
|
|
625
637
|
message = error.display("type-msg").strip()
|
|
626
638
|
lowered = message.lower()
|
|
627
|
-
stage: Literal["runtime", "timeout"] = (
|
|
628
|
-
"
|
|
639
|
+
stage: Literal["arguments", "runtime", "timeout"] = (
|
|
640
|
+
"arguments"
|
|
641
|
+
if context.failure is not None and context.failure.kind == "argument_validation"
|
|
642
|
+
else "timeout"
|
|
629
643
|
if (context.failure is not None and context.failure.kind == "upstream_timeout")
|
|
630
644
|
or "duration" in lowered
|
|
631
645
|
or "timed out" in lowered
|
|
@@ -728,7 +742,7 @@ class MontyExecutor:
|
|
|
728
742
|
@staticmethod
|
|
729
743
|
def _failure(
|
|
730
744
|
context: ExecutionContext,
|
|
731
|
-
stage:
|
|
745
|
+
stage: FailureStage,
|
|
732
746
|
error: str,
|
|
733
747
|
) -> ExecutionResponse:
|
|
734
748
|
failure = context.failure or _execution_failure_info(stage, error)
|
|
@@ -749,6 +763,8 @@ def _execution_failure_info(
|
|
|
749
763
|
) -> ExecutionFailureInfo:
|
|
750
764
|
if stage == "preflight":
|
|
751
765
|
kind: FailureKind = "preflight"
|
|
766
|
+
elif stage == "arguments":
|
|
767
|
+
kind = "argument_validation"
|
|
752
768
|
elif stage == "result":
|
|
753
769
|
kind = "result"
|
|
754
770
|
elif stage == "timeout":
|
package/sidecar/gateway.py
CHANGED
|
@@ -206,6 +206,11 @@ class SavedChainHandlers(NamedTuple):
|
|
|
206
206
|
delete: Callable[[str, ChainScope, str], Awaitable[ChainListResponse]]
|
|
207
207
|
|
|
208
208
|
|
|
209
|
+
class _LastExecution(NamedTuple):
|
|
210
|
+
code: str
|
|
211
|
+
input_ref: str | None
|
|
212
|
+
|
|
213
|
+
|
|
209
214
|
class SavedChainRuntime:
|
|
210
215
|
def __init__(self, handlers: SavedChainHandlers) -> None:
|
|
211
216
|
self.handlers = handlers
|
|
@@ -307,6 +312,8 @@ class GatewayRuntime:
|
|
|
307
312
|
)
|
|
308
313
|
)
|
|
309
314
|
self._catalog_lock = asyncio.Lock()
|
|
315
|
+
self._execute_lock = asyncio.Lock()
|
|
316
|
+
self._last_execution: _LastExecution | None = None
|
|
310
317
|
|
|
311
318
|
@classmethod
|
|
312
319
|
def create(
|
|
@@ -595,6 +602,38 @@ class GatewayRuntime:
|
|
|
595
602
|
trace_id: str,
|
|
596
603
|
input_ref: str | None = None,
|
|
597
604
|
) -> ExecutionResponse:
|
|
605
|
+
async with self._execute_lock:
|
|
606
|
+
return await self._execute(code, trace_id, input_ref)
|
|
607
|
+
|
|
608
|
+
async def edit_execute(
|
|
609
|
+
self,
|
|
610
|
+
old_text: str,
|
|
611
|
+
new_text: str,
|
|
612
|
+
trace_id: str,
|
|
613
|
+
) -> ExecutionResponse:
|
|
614
|
+
async with self._execute_lock:
|
|
615
|
+
previous = self._last_execution
|
|
616
|
+
if previous is None:
|
|
617
|
+
raise ValueError("No previous CodeMCP execution is available in this sidecar")
|
|
618
|
+
if not old_text:
|
|
619
|
+
raise ValueError("old_text must not be empty")
|
|
620
|
+
if old_text == new_text:
|
|
621
|
+
raise ValueError("old_text and new_text must differ")
|
|
622
|
+
matches = previous.code.count(old_text)
|
|
623
|
+
if matches != 1:
|
|
624
|
+
raise ValueError(
|
|
625
|
+
f"old_text must match the previous code exactly once; found {matches} matches"
|
|
626
|
+
)
|
|
627
|
+
code = previous.code.replace(old_text, new_text, 1)
|
|
628
|
+
return await self._execute(code, trace_id, previous.input_ref)
|
|
629
|
+
|
|
630
|
+
async def _execute(
|
|
631
|
+
self,
|
|
632
|
+
code: str,
|
|
633
|
+
trace_id: str,
|
|
634
|
+
input_ref: str | None,
|
|
635
|
+
) -> ExecutionResponse:
|
|
636
|
+
self._last_execution = None
|
|
598
637
|
started = time.perf_counter()
|
|
599
638
|
input_bytes = len(code.encode()) + len((input_ref or "").encode())
|
|
600
639
|
input_value: JsonValue = None
|
|
@@ -619,6 +658,7 @@ class GatewayRuntime:
|
|
|
619
658
|
response,
|
|
620
659
|
trace_id,
|
|
621
660
|
)
|
|
661
|
+
self._last_execution = _LastExecution(code, input_ref)
|
|
622
662
|
return response
|
|
623
663
|
discovery_started = time.perf_counter()
|
|
624
664
|
try:
|
|
@@ -655,6 +695,7 @@ class GatewayRuntime:
|
|
|
655
695
|
)
|
|
656
696
|
raise
|
|
657
697
|
self._record_execution("execute", started, input_bytes, response, trace_id)
|
|
698
|
+
self._last_execution = _LastExecution(code, input_ref)
|
|
658
699
|
return response
|
|
659
700
|
|
|
660
701
|
async def _execute_chain(
|
|
@@ -1511,6 +1552,7 @@ def _execution_failure_subtype(response: ExecutionResponse) -> str:
|
|
|
1511
1552
|
failure = response.failure
|
|
1512
1553
|
if failure is not None and failure.kind in {
|
|
1513
1554
|
"result_reference",
|
|
1555
|
+
"argument_validation",
|
|
1514
1556
|
"sandbox_timeout",
|
|
1515
1557
|
"upstream",
|
|
1516
1558
|
"upstream_transport",
|
|
@@ -1678,6 +1720,16 @@ async def execute(
|
|
|
1678
1720
|
return await _require_runtime().execute(code, trace_id, input_ref)
|
|
1679
1721
|
|
|
1680
1722
|
|
|
1723
|
+
@mcp.tool
|
|
1724
|
+
async def edit_execute(
|
|
1725
|
+
trace_id: str,
|
|
1726
|
+
old_text: str,
|
|
1727
|
+
new_text: str,
|
|
1728
|
+
) -> ExecutionResponse:
|
|
1729
|
+
"""Patch and rerun the last sandbox execution."""
|
|
1730
|
+
return await _require_runtime().edit_execute(old_text, new_text, trace_id)
|
|
1731
|
+
|
|
1732
|
+
|
|
1681
1733
|
@mcp.tool
|
|
1682
1734
|
async def save_chain(
|
|
1683
1735
|
trace_id: str,
|
package/sidecar/sandbox_api.py
CHANGED
|
@@ -14,7 +14,7 @@ SANDBOX_FUNCTION_EXTERNALS = {
|
|
|
14
14
|
EXPECT_INTEGER_NAME: "__codemcp_expect_integer",
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
STUB_IMPORTS = "from typing import Literal, Never, NotRequired, TypeAlias, TypedDict"
|
|
17
|
+
STUB_IMPORTS = "from typing import Literal, Mapping, Never, NotRequired, TypeAlias, TypedDict"
|
|
18
18
|
JSON_TYPE_STUBS = (
|
|
19
19
|
"JsonScalar: TypeAlias = bool | int | float | str | None",
|
|
20
20
|
'JsonValue: TypeAlias = JsonScalar | list["JsonValue"] | dict[str, "JsonValue"]',
|
package/sidecar/stats.py
CHANGED
|
@@ -98,6 +98,7 @@ DISTINCT_NAME_QUERIES = {
|
|
|
98
98
|
FailureOutcome = Literal[
|
|
99
99
|
"success",
|
|
100
100
|
"preflight_rejection",
|
|
101
|
+
"argument_rejection",
|
|
101
102
|
"result_refinement",
|
|
102
103
|
"upstream_failure",
|
|
103
104
|
"transport_failure",
|
|
@@ -996,6 +997,8 @@ def _operation_outcome(
|
|
|
996
997
|
return "internal_error"
|
|
997
998
|
if failure.stage == "preflight":
|
|
998
999
|
return "preflight_rejection"
|
|
1000
|
+
if failure.stage == "arguments":
|
|
1001
|
+
return "argument_rejection"
|
|
999
1002
|
if failure.stage == "result":
|
|
1000
1003
|
return "result_refinement"
|
|
1001
1004
|
if failure.stage == "cancelled":
|
package/sidecar/tool_catalog.py
CHANGED
|
@@ -426,7 +426,8 @@ class ToolCatalog(BaseModel):
|
|
|
426
426
|
for spec in specs:
|
|
427
427
|
definitions.extend(spec.stub.split("\n\n") if spec.stub else [])
|
|
428
428
|
facade_methods.setdefault(spec.namespace, []).append(
|
|
429
|
-
f" async def {spec.method}(self, arguments:
|
|
429
|
+
f" async def {spec.method}(self, arguments: "
|
|
430
|
+
f"{spec.input_type_name} | Mapping[str, JsonValue]) "
|
|
430
431
|
f"-> {spec.output_type_name}: ..."
|
|
431
432
|
)
|
|
432
433
|
facades: list[str] = []
|
|
@@ -114,6 +114,9 @@ function renderCompactFailure(
|
|
|
114
114
|
if (stage === "preflight") {
|
|
115
115
|
return theme.fg("warning", `✗ Preflight · code not run · ${summary}`);
|
|
116
116
|
}
|
|
117
|
+
if (stage === "arguments") {
|
|
118
|
+
return theme.fg("warning", `✗ Argument validation · code run · ${summary}`);
|
|
119
|
+
}
|
|
117
120
|
if (stage === "timeout") {
|
|
118
121
|
return theme.fg("error", `✗ Timeout · stopped after ${summary}`);
|
|
119
122
|
}
|
|
@@ -131,6 +134,9 @@ function failureHeading(stage: string, calls: number, chainCalls: number, theme:
|
|
|
131
134
|
if (stage === "preflight") {
|
|
132
135
|
return `${theme.fg("warning", theme.bold("Preflight failed"))}\n${theme.fg("muted", "Code was not executed; no upstream side effects")}`;
|
|
133
136
|
}
|
|
137
|
+
if (stage === "arguments") {
|
|
138
|
+
return `${theme.fg("warning", theme.bold("Argument validation failed"))}\n${theme.fg("muted", "Code ran; no invalid MCP call was sent")}`;
|
|
139
|
+
}
|
|
134
140
|
if (stage === "timeout") {
|
|
135
141
|
return `${theme.fg("error", theme.bold("Execution timed out"))}\n${theme.fg("muted", `Stopped after ${summary}`)}`;
|
|
136
142
|
}
|
package/src/mcp-client.ts
CHANGED
|
@@ -23,6 +23,7 @@ export type SidecarToolName =
|
|
|
23
23
|
| "discover"
|
|
24
24
|
| "reload_settings"
|
|
25
25
|
| "execute"
|
|
26
|
+
| "edit_execute"
|
|
26
27
|
| "save_chain"
|
|
27
28
|
| "list_chains"
|
|
28
29
|
| "execute_chain"
|
|
@@ -34,6 +35,7 @@ export type SidecarToolName =
|
|
|
34
35
|
|
|
35
36
|
const LONG_RUNNING_TOOLS = new Set<SidecarToolName>([
|
|
36
37
|
"execute",
|
|
38
|
+
"edit_execute",
|
|
37
39
|
"save_chain",
|
|
38
40
|
"execute_chain",
|
|
39
41
|
"revalidate_chain",
|
package/src/tools.ts
CHANGED
|
@@ -152,6 +152,16 @@ const ExecuteParameters = Type.Object({
|
|
|
152
152
|
),
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
+
const EditExecuteParameters = Type.Object({
|
|
156
|
+
oldText: Type.String({
|
|
157
|
+
minLength: 1,
|
|
158
|
+
description: "Exact text that must occur once in the previous execution code",
|
|
159
|
+
}),
|
|
160
|
+
newText: Type.String({
|
|
161
|
+
description: "Replacement text; may be empty",
|
|
162
|
+
}),
|
|
163
|
+
});
|
|
164
|
+
|
|
155
165
|
export function registerCodeMcpTools(
|
|
156
166
|
pi: ExtensionAPI,
|
|
157
167
|
lifecycle: CodeMcpLifecycle,
|
|
@@ -321,32 +331,7 @@ export function registerCodeMcpTools(
|
|
|
321
331
|
},
|
|
322
332
|
signal,
|
|
323
333
|
);
|
|
324
|
-
|
|
325
|
-
const modelValue = ok
|
|
326
|
-
? result.result
|
|
327
|
-
: {
|
|
328
|
-
failure_stage: result.failure_stage,
|
|
329
|
-
error: result.error,
|
|
330
|
-
failure: result.failure,
|
|
331
|
-
shape: result.shape,
|
|
332
|
-
result_ref: result.result_ref,
|
|
333
|
-
expires_in_seconds: result.expires_in_seconds,
|
|
334
|
-
calls_made: result.calls_made,
|
|
335
|
-
chain_calls: result.chain_calls,
|
|
336
|
-
};
|
|
337
|
-
const output = formatCodeMcpOutput(modelValue, outputLimits(lifecycle));
|
|
338
|
-
return {
|
|
339
|
-
content: [{ type: "text", text: output.text }],
|
|
340
|
-
details: {
|
|
341
|
-
...output.details,
|
|
342
|
-
ok,
|
|
343
|
-
failureStage: typeof result.failure_stage === "string" ? result.failure_stage : undefined,
|
|
344
|
-
callsMade: Number(result.calls_made ?? 0),
|
|
345
|
-
chainCalls: Number(result.chain_calls ?? 0),
|
|
346
|
-
...(isRecord(result.timings) ? { timings: result.timings } : {}),
|
|
347
|
-
preview: previewExecutionValue(ok ? result.result : result.error),
|
|
348
|
-
},
|
|
349
|
-
};
|
|
334
|
+
return formatExecutionResponse(result, lifecycle);
|
|
350
335
|
},
|
|
351
336
|
renderCall(args, theme, context) {
|
|
352
337
|
const code = args.code.trim();
|
|
@@ -379,6 +364,46 @@ export function registerCodeMcpTools(
|
|
|
379
364
|
},
|
|
380
365
|
});
|
|
381
366
|
|
|
367
|
+
pi.registerTool({
|
|
368
|
+
name: "codemcp_edit",
|
|
369
|
+
label: "MCP Edit",
|
|
370
|
+
description:
|
|
371
|
+
"Patch and rerun the most recent codemcp_execute in the current sidecar without resending its full code. oldText must match exactly once. The original inputRef is reused and the entire call graph, including upstream MCP calls, runs again.",
|
|
372
|
+
promptSnippet: "Patch and rerun the previous CodeMCP execution",
|
|
373
|
+
parameters: EditExecuteParameters,
|
|
374
|
+
async execute(toolCallId, params, signal, onUpdate) {
|
|
375
|
+
onUpdate?.({
|
|
376
|
+
content: [{ type: "text", text: "Patching and type-checking MCP chain..." }],
|
|
377
|
+
details: undefined,
|
|
378
|
+
});
|
|
379
|
+
const result = await lifecycle.request(
|
|
380
|
+
"edit_execute",
|
|
381
|
+
{
|
|
382
|
+
old_text: params.oldText,
|
|
383
|
+
new_text: params.newText,
|
|
384
|
+
trace_id: toolCallId,
|
|
385
|
+
},
|
|
386
|
+
signal,
|
|
387
|
+
);
|
|
388
|
+
return formatExecutionResponse(result, lifecycle);
|
|
389
|
+
},
|
|
390
|
+
renderCall(args, theme) {
|
|
391
|
+
const lines = args.oldText ? args.oldText.split("\n").length : 0;
|
|
392
|
+
const title = theme.fg("toolTitle", theme.bold("MCP Edit"));
|
|
393
|
+
const patchLabel = theme.fg(
|
|
394
|
+
"accent",
|
|
395
|
+
theme.bold(`Edited ${lines} ${lines === 1 ? "line" : "lines"}`),
|
|
396
|
+
);
|
|
397
|
+
return new Text(`${title} ${theme.fg("muted", "·")} ${patchLabel}`, 0, 0);
|
|
398
|
+
},
|
|
399
|
+
renderResult(result, state, theme) {
|
|
400
|
+
return renderExecutionResult(result, state, theme, {
|
|
401
|
+
partialText: "Applying edit, then execution...",
|
|
402
|
+
expandDescription: "full output",
|
|
403
|
+
});
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
|
|
382
407
|
pi.registerTool({
|
|
383
408
|
name: "codemcp_save_chain",
|
|
384
409
|
label: "Save MCP Chain",
|
|
@@ -552,6 +577,35 @@ export function registerCodeMcpTools(
|
|
|
552
577
|
});
|
|
553
578
|
}
|
|
554
579
|
|
|
580
|
+
function formatExecutionResponse(result: Record<string, unknown>, lifecycle: CodeMcpLifecycle) {
|
|
581
|
+
const ok = result.ok === true;
|
|
582
|
+
const modelValue = ok
|
|
583
|
+
? result.result
|
|
584
|
+
: {
|
|
585
|
+
failure_stage: result.failure_stage,
|
|
586
|
+
error: result.error,
|
|
587
|
+
failure: result.failure,
|
|
588
|
+
shape: result.shape,
|
|
589
|
+
result_ref: result.result_ref,
|
|
590
|
+
expires_in_seconds: result.expires_in_seconds,
|
|
591
|
+
calls_made: result.calls_made,
|
|
592
|
+
chain_calls: result.chain_calls,
|
|
593
|
+
};
|
|
594
|
+
const output = formatCodeMcpOutput(modelValue, outputLimits(lifecycle));
|
|
595
|
+
return {
|
|
596
|
+
content: [{ type: "text" as const, text: output.text }],
|
|
597
|
+
details: {
|
|
598
|
+
...output.details,
|
|
599
|
+
ok,
|
|
600
|
+
failureStage: typeof result.failure_stage === "string" ? result.failure_stage : undefined,
|
|
601
|
+
callsMade: Number(result.calls_made ?? 0),
|
|
602
|
+
chainCalls: Number(result.chain_calls ?? 0),
|
|
603
|
+
...(isRecord(result.timings) ? { timings: result.timings } : {}),
|
|
604
|
+
preview: previewExecutionValue(ok ? result.result : result.error),
|
|
605
|
+
},
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
555
609
|
function compactChainView(view: SavedChainView) {
|
|
556
610
|
return {
|
|
557
611
|
name: view.chain.name,
|