pi-codemcp 1.0.0 → 1.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/README.md +144 -13
- package/extensions/index.ts +4 -1
- package/package.json +7 -7
- package/sidecar/catalog_cache.py +2 -1
- package/sidecar/cli.py +402 -0
- package/sidecar/executor.py +349 -36
- package/sidecar/gateway.py +499 -60
- package/sidecar/models.py +41 -4
- package/sidecar/pyproject.toml +33 -40
- package/sidecar/runtime_paths.py +71 -0
- package/sidecar/settings.py +7 -3
- package/sidecar/stats.py +424 -0
- package/sidecar/tool_catalog.py +311 -46
- package/sidecar/uv.lock +99 -244
- package/src/chains.ts +1 -2
- package/src/execution-rendering.ts +7 -5
- package/src/mcp-client.ts +5 -1
- package/src/modal.ts +258 -14
- package/src/output.ts +27 -24
- package/src/prompts.ts +24 -0
- package/src/settings.ts +18 -17
- package/src/tools.ts +270 -36
package/sidecar/models.py
CHANGED
|
@@ -6,6 +6,8 @@ from pydantic import BaseModel, ConfigDict, Field, model_serializer
|
|
|
6
6
|
|
|
7
7
|
type ServerTransport = Literal["stdio", "http", "sse"]
|
|
8
8
|
type ServerAuth = Literal["oauth", "bearer"]
|
|
9
|
+
type SearchDetail = Literal["names", "signatures", "full"]
|
|
10
|
+
type SearchMode = Literal["search", "inventory"]
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
class NormalizedServerInfo(BaseModel):
|
|
@@ -27,8 +29,10 @@ class ToolSchemaView(BaseModel):
|
|
|
27
29
|
source: Literal["mcp_tool", "saved_chain"]
|
|
28
30
|
server: str | None = None
|
|
29
31
|
description: str | None = None
|
|
30
|
-
signature: str
|
|
31
|
-
stub: str
|
|
32
|
+
signature: str | None = None
|
|
33
|
+
stub: str | None = None
|
|
34
|
+
score: float | None = None
|
|
35
|
+
matched_fields: list[str] = Field(default_factory=list)
|
|
32
36
|
|
|
33
37
|
@model_serializer(mode="plain")
|
|
34
38
|
def serialize_compact(self) -> dict[str, object]:
|
|
@@ -36,13 +40,19 @@ class ToolSchemaView(BaseModel):
|
|
|
36
40
|
"name": self.name,
|
|
37
41
|
"call": self.call,
|
|
38
42
|
"source": self.source,
|
|
39
|
-
"signature": self.signature,
|
|
40
|
-
"stub": self.stub,
|
|
41
43
|
}
|
|
42
44
|
if self.server is not None:
|
|
43
45
|
values["server"] = self.server
|
|
44
46
|
if self.description is not None:
|
|
45
47
|
values["description"] = self.description
|
|
48
|
+
if self.signature is not None:
|
|
49
|
+
values["signature"] = self.signature
|
|
50
|
+
if self.stub is not None:
|
|
51
|
+
values["stub"] = self.stub
|
|
52
|
+
if self.score is not None:
|
|
53
|
+
values["score"] = round(self.score, 2)
|
|
54
|
+
if self.matched_fields:
|
|
55
|
+
values["matched_fields"] = self.matched_fields
|
|
46
56
|
return values
|
|
47
57
|
|
|
48
58
|
|
|
@@ -53,11 +63,38 @@ class ServerToolSummary(BaseModel):
|
|
|
53
63
|
tool_count: int
|
|
54
64
|
|
|
55
65
|
|
|
66
|
+
class ExecutionLimitsView(BaseModel):
|
|
67
|
+
model_config = ConfigDict(extra="forbid", strict=True)
|
|
68
|
+
|
|
69
|
+
timeout_seconds: float
|
|
70
|
+
tool_timeout_seconds: float
|
|
71
|
+
max_calls: int
|
|
72
|
+
result_limit_bytes: int
|
|
73
|
+
|
|
74
|
+
|
|
56
75
|
class SearchResponse(BaseModel):
|
|
57
76
|
model_config = ConfigDict(extra="forbid", strict=True)
|
|
58
77
|
|
|
78
|
+
mode: SearchMode
|
|
79
|
+
detail: SearchDetail
|
|
59
80
|
total_tool_count: int
|
|
81
|
+
filtered_tool_count: int
|
|
60
82
|
servers: list[ServerToolSummary]
|
|
83
|
+
cursor: int
|
|
84
|
+
next_cursor: int | None = None
|
|
85
|
+
has_more: bool = False
|
|
86
|
+
project_scope_available: bool
|
|
87
|
+
execution_limits: ExecutionLimitsView
|
|
88
|
+
prelude: str | None = None
|
|
89
|
+
results: list[ToolSchemaView]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class InspectResponse(BaseModel):
|
|
93
|
+
model_config = ConfigDict(extra="forbid", strict=True)
|
|
94
|
+
|
|
95
|
+
prelude: str
|
|
96
|
+
project_scope_available: bool
|
|
97
|
+
execution_limits: ExecutionLimitsView
|
|
61
98
|
results: list[ToolSchemaView]
|
|
62
99
|
|
|
63
100
|
|
package/sidecar/pyproject.toml
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name = "pi-codemcp-sidecar"
|
|
3
3
|
version = "0.1.0"
|
|
4
4
|
description = "Python sidecar for pi-codemcp"
|
|
5
|
-
requires-python = ">=3.
|
|
5
|
+
requires-python = ">=3.13"
|
|
6
6
|
dependencies = [
|
|
7
7
|
"fastmcp==3.4.4",
|
|
8
8
|
"httpx==0.28.1",
|
|
@@ -14,12 +14,12 @@ dependencies = [
|
|
|
14
14
|
|
|
15
15
|
[dependency-groups]
|
|
16
16
|
dev = [
|
|
17
|
-
"mypy==2.
|
|
18
|
-
"prek==0.4.
|
|
19
|
-
"pytest==
|
|
17
|
+
"mypy==2.3.0",
|
|
18
|
+
"prek==0.4.10",
|
|
19
|
+
"pytest==9.1.1",
|
|
20
20
|
"pytest-asyncio==1.4.0",
|
|
21
|
-
"ruff==0.15.
|
|
22
|
-
"ty==0.0.
|
|
21
|
+
"ruff==0.15.22",
|
|
22
|
+
"ty==0.0.61",
|
|
23
23
|
]
|
|
24
24
|
|
|
25
25
|
[tool.pytest.ini_options]
|
|
@@ -27,8 +27,8 @@ asyncio_mode = "auto"
|
|
|
27
27
|
pythonpath = [".."]
|
|
28
28
|
|
|
29
29
|
[tool.ruff]
|
|
30
|
-
required-version = "==0.15.
|
|
31
|
-
target-version = "
|
|
30
|
+
required-version = "==0.15.22"
|
|
31
|
+
target-version = "py313"
|
|
32
32
|
line-length = 100
|
|
33
33
|
src = ["sidecar"]
|
|
34
34
|
preview = true
|
|
@@ -37,23 +37,23 @@ force-exclude = true
|
|
|
37
37
|
[tool.ruff.lint]
|
|
38
38
|
select = ["ALL"]
|
|
39
39
|
ignore = [
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
40
|
+
"missing-trailing-comma", # Conflicts with the formatter.
|
|
41
|
+
"missing-copyright-notice", # Legal copyright text is not configured.
|
|
42
|
+
"undocumented-public-module", # Internal modules do not need module docstrings.
|
|
43
|
+
"undocumented-public-class", # Pydantic catalog models are self-describing.
|
|
44
|
+
"undocumented-public-method", # Internal methods are covered by types and focused class documentation.
|
|
45
|
+
"undocumented-public-function", # Internal functions are covered by types and focused module documentation.
|
|
46
|
+
"undocumented-public-package", # Internal packages do not need package docstrings beyond sidecar/__init__.py.
|
|
47
|
+
"undocumented-magic-method", # Magic methods do not need docstrings.
|
|
48
|
+
"undocumented-public-init", # Constructor contracts are expressed by typed fields.
|
|
49
|
+
"incorrect-blank-line-before-class", # Incompatible with D211.
|
|
50
|
+
"multi-line-summary-second-line", # Incompatible with D212.
|
|
51
|
+
"docstring-missing-returns", # Return types are explicit and checked.
|
|
52
|
+
"docstring-missing-exception", # Repeating raised exception types in docstrings reduces readability.
|
|
53
|
+
"raw-string-in-exception", # Inline exception messages are clearer at the failure site.
|
|
54
|
+
"f-string-in-exception", # Inline contextual exception messages are clearer at the failure site.
|
|
55
|
+
"boolean-type-hint-positional-argument", # JSON Schema itself may be boolean; these are not boolean control arguments.
|
|
56
|
+
"raise-vanilla-args", # Custom exception wrappers would obscure concise boundary failures.
|
|
57
57
|
]
|
|
58
58
|
|
|
59
59
|
[tool.ruff.lint.isort]
|
|
@@ -83,23 +83,19 @@ docstring-code-line-length = 100
|
|
|
83
83
|
"tests/**/*.py" = [
|
|
84
84
|
"ANN",
|
|
85
85
|
"D",
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
"
|
|
86
|
+
"magic-value-comparison",
|
|
87
|
+
"assert",
|
|
88
|
+
"hardcoded-password-string",
|
|
89
|
+
"hardcoded-password-func-arg",
|
|
90
|
+
"private-member-access",
|
|
91
91
|
]
|
|
92
|
-
"tests/fixtures/**/*.py" = ["
|
|
92
|
+
"tests/fixtures/**/*.py" = ["print"]
|
|
93
93
|
|
|
94
94
|
[tool.mypy]
|
|
95
|
-
python_version = "3.
|
|
95
|
+
python_version = "3.13"
|
|
96
96
|
plugins = ["pydantic.mypy"]
|
|
97
97
|
strict = true
|
|
98
|
-
files = [
|
|
99
|
-
"sidecar",
|
|
100
|
-
"tests/python/test_executor.py",
|
|
101
|
-
"tests/python/test_settings.py",
|
|
102
|
-
]
|
|
98
|
+
files = ["sidecar", "tests/python"]
|
|
103
99
|
pretty = true
|
|
104
100
|
show_error_codes = true
|
|
105
101
|
show_error_code_links = true
|
|
@@ -128,9 +124,6 @@ init_typed = true
|
|
|
128
124
|
warn_required_dynamic_aliases = true
|
|
129
125
|
warn_untyped_fields = true
|
|
130
126
|
|
|
131
|
-
[tool.ty.environment]
|
|
132
|
-
python-version = "3.12"
|
|
133
|
-
|
|
134
127
|
[tool.ty.rules]
|
|
135
128
|
all = "error"
|
|
136
129
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
DEFAULT_AGENT_DIR = Path.home() / ".pi" / "agent"
|
|
8
|
+
CODEMCP_AGENT_DIR_ENV = "PI_CODEMCP_AGENT_DIR"
|
|
9
|
+
CODEMCP_PROJECT_CHAINS_DIR_ENV = "PI_CODEMCP_PROJECT_CHAINS_DIR"
|
|
10
|
+
PI_AGENT_DIR_ENV = "PI_CODING_AGENT_DIR"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class RuntimePaths:
|
|
15
|
+
config_path: Path
|
|
16
|
+
oauth_dir: Path
|
|
17
|
+
catalog_dir: Path
|
|
18
|
+
settings_path: Path
|
|
19
|
+
global_chains_dir: Path
|
|
20
|
+
project_chains_dir: Path | None
|
|
21
|
+
|
|
22
|
+
def as_tuple(self) -> tuple[Path, Path, Path, Path, Path, Path | None]:
|
|
23
|
+
return (
|
|
24
|
+
self.config_path,
|
|
25
|
+
self.oauth_dir,
|
|
26
|
+
self.catalog_dir,
|
|
27
|
+
self.settings_path,
|
|
28
|
+
self.global_chains_dir,
|
|
29
|
+
self.project_chains_dir,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def resolve_runtime_paths(
|
|
34
|
+
*,
|
|
35
|
+
agent_dir: Path | str | None = None,
|
|
36
|
+
config_path: Path | str | None = None,
|
|
37
|
+
settings_path: Path | str | None = None,
|
|
38
|
+
oauth_dir: Path | str | None = None,
|
|
39
|
+
catalog_dir: Path | str | None = None,
|
|
40
|
+
global_chains_dir: Path | str | None = None,
|
|
41
|
+
project_chains_dir: Path | str | None = None,
|
|
42
|
+
) -> RuntimePaths:
|
|
43
|
+
resolved_agent_dir = _agent_dir(agent_dir)
|
|
44
|
+
state_dir = resolved_agent_dir / "pi-codemcp"
|
|
45
|
+
return RuntimePaths(
|
|
46
|
+
config_path=_resolve_path(config_path) or resolved_agent_dir / "mcp.json",
|
|
47
|
+
oauth_dir=_resolve_path(oauth_dir) or state_dir / "oauth",
|
|
48
|
+
catalog_dir=_resolve_path(catalog_dir) or state_dir / "catalog",
|
|
49
|
+
settings_path=_resolve_path(settings_path) or state_dir / "settings.json",
|
|
50
|
+
global_chains_dir=_resolve_path(global_chains_dir) or state_dir / "chains",
|
|
51
|
+
project_chains_dir=_project_chains_dir(project_chains_dir),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _agent_dir(value: Path | str | None) -> Path:
|
|
56
|
+
if value is not None:
|
|
57
|
+
return _resolve_path(value) or DEFAULT_AGENT_DIR
|
|
58
|
+
raw_agent_dir = os.environ.get(CODEMCP_AGENT_DIR_ENV) or os.environ.get(PI_AGENT_DIR_ENV)
|
|
59
|
+
return _resolve_path(raw_agent_dir) or DEFAULT_AGENT_DIR
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _project_chains_dir(value: Path | str | None) -> Path | None:
|
|
63
|
+
if value is not None:
|
|
64
|
+
return _resolve_path(value)
|
|
65
|
+
return _resolve_path(os.environ.get(CODEMCP_PROJECT_CHAINS_DIR_ENV))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _resolve_path(value: Path | str | None) -> Path | None:
|
|
69
|
+
if value is None:
|
|
70
|
+
return None
|
|
71
|
+
return Path(value).expanduser()
|
package/sidecar/settings.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
from typing import TYPE_CHECKING, Literal
|
|
4
5
|
|
|
5
6
|
from pydantic import BaseModel, ConfigDict, Field
|
|
@@ -24,7 +25,7 @@ class CodeMcpSettings(BaseModel):
|
|
|
24
25
|
strict=True,
|
|
25
26
|
)
|
|
26
27
|
|
|
27
|
-
version: Literal[
|
|
28
|
+
version: Literal[2] = 2
|
|
28
29
|
background_warmup: bool = True
|
|
29
30
|
cache_ttl_hours: int = Field(default=24, ge=0, le=720)
|
|
30
31
|
execution_timeout_seconds: int = Field(default=30, ge=1, le=300)
|
|
@@ -32,7 +33,6 @@ class CodeMcpSettings(BaseModel):
|
|
|
32
33
|
max_calls: int = Field(default=50, ge=1, le=200)
|
|
33
34
|
result_limit_kib: int = Field(default=16, ge=1, le=1024)
|
|
34
35
|
output_limit_kib: int = Field(default=50, ge=1, le=1024)
|
|
35
|
-
output_line_limit: int = Field(default=2000, ge=1, le=10000)
|
|
36
36
|
disabled_tools: dict[str, list[str]] = Field(default_factory=dict)
|
|
37
37
|
|
|
38
38
|
def tool_enabled(self, server: str, tool: str) -> bool:
|
|
@@ -56,4 +56,8 @@ def load_settings(path: Path) -> CodeMcpSettings:
|
|
|
56
56
|
raw = path.read_text(encoding="utf-8")
|
|
57
57
|
except FileNotFoundError:
|
|
58
58
|
return CodeMcpSettings()
|
|
59
|
-
|
|
59
|
+
value = json.loads(raw)
|
|
60
|
+
if isinstance(value, dict) and value.get("version", 1) == 1:
|
|
61
|
+
value = {key: item for key, item in value.items() if key != "outputLineLimit"}
|
|
62
|
+
value["version"] = 2
|
|
63
|
+
return CodeMcpSettings.model_validate(value)
|