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/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
 
@@ -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.12"
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.2.0",
18
- "prek==0.4.8",
19
- "pytest==8.4.2",
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.21",
22
- "ty==0.0.58",
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.21"
31
- target-version = "py312"
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
- "COM812", # Conflicts with the formatter.
41
- "CPY001", # Legal copyright text is not configured.
42
- "D100", # Internal modules do not need module docstrings.
43
- "D101", # Pydantic catalog models are self-describing.
44
- "D102", # Internal methods are covered by types and focused class documentation.
45
- "D103", # Internal functions are covered by types and focused module documentation.
46
- "D104", # Internal packages do not need package docstrings beyond sidecar/__init__.py.
47
- "D105", # Magic methods do not need docstrings.
48
- "D107", # Constructor contracts are expressed by typed fields.
49
- "D203", # Incompatible with D211.
50
- "D213", # Incompatible with D212.
51
- "DOC201", # Return types are explicit and checked.
52
- "DOC501", # Repeating raised exception types in docstrings reduces readability.
53
- "EM101", # Inline exception messages are clearer at the failure site.
54
- "EM102", # Inline contextual exception messages are clearer at the failure site.
55
- "FBT001", # JSON Schema itself may be boolean; these are not boolean control arguments.
56
- "TRY003", # Custom exception wrappers would obscure concise boundary failures.
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
- "PLR2004",
87
- "S101",
88
- "S105",
89
- "S106",
90
- "SLF001",
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" = ["T201"]
92
+ "tests/fixtures/**/*.py" = ["print"]
93
93
 
94
94
  [tool.mypy]
95
- python_version = "3.12"
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()
@@ -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[1] = 1
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
- return CodeMcpSettings.model_validate_json(raw)
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)