pi-codemcp 1.2.0 → 1.2.1

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 CHANGED
@@ -148,7 +148,7 @@ The Python sidecar enforces catalog cache TTL, execution timeout, per-tool timeo
148
148
 
149
149
  ## Search and execute flow
150
150
 
151
- The agent searches for a capability, inspects the selected exact stub when needed, and executes a compact plan:
151
+ The agent searches for a capability, inspects the selected exact stub when needed, and executes a compact plan. Unscoped searches discover stale or missing server catalogs independently: available servers still return results, while `discovery_failures` explicitly reports unavailable servers. Server-scoped searches remain fail-fast.
152
152
 
153
153
  ```python
154
154
  issues = await linear.list_issues({"assignee": "me", "limit": 50})
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-codemcp",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Typed, sandboxed Code Mode access to configured MCP servers for Pi",
5
5
  "type": "module",
6
6
  "packageManager": "bun@1.3.10",
@@ -36,6 +36,7 @@ from .models import (
36
36
  SearchDetail,
37
37
  SearchMode,
38
38
  SearchResponse,
39
+ ServerDiscoveryFailure,
39
40
  ServerToolSummary,
40
41
  StatusResponse,
41
42
  UpstreamStatus,
@@ -377,7 +378,11 @@ class GatewayRuntime:
377
378
  discovery_servers = ()
378
379
  else:
379
380
  discovery_servers = (server,)
380
- await self._ensure_servers_discovered(discovery_servers)
381
+ if server is None:
382
+ discovery_failures = await self._discover_servers_for_unscoped_search(discovery_servers)
383
+ else:
384
+ await self._ensure_servers_discovered(discovery_servers)
385
+ discovery_failures = []
381
386
  self.stats_store.record_phase("discovery", _elapsed_ms(discovery_started))
382
387
  bounded_limit = min(max(limit, 1), 20)
383
388
  bounded_cursor = max(cursor, 0)
@@ -439,6 +444,7 @@ class GatewayRuntime:
439
444
  has_more=next_cursor is not None,
440
445
  project_scope_available=self.chain_store.project_store is not None,
441
446
  execution_limits=self._execution_limits_view(),
447
+ discovery_failures=discovery_failures,
442
448
  prelude=self.catalog.stub_prelude if include_prelude else None,
443
449
  results=results,
444
450
  )
@@ -981,6 +987,36 @@ class GatewayRuntime:
981
987
  async def _ensure_catalog_complete(self) -> None:
982
988
  await self._ensure_servers_discovered(self.handles.keys())
983
989
 
990
+ async def _discover_servers_for_unscoped_search(
991
+ self,
992
+ server_names: Iterable[str],
993
+ ) -> list[ServerDiscoveryFailure]:
994
+ requested = set(server_names)
995
+ missing = [
996
+ (name, handle)
997
+ for name, handle in self.handles.items()
998
+ if name in requested and handle.tools is None
999
+ ]
1000
+ if not missing:
1001
+ return []
1002
+ results = await asyncio.gather(
1003
+ *(handle.discover() for _, handle in missing),
1004
+ return_exceptions=True,
1005
+ )
1006
+ failures: list[ServerDiscoveryFailure] = []
1007
+ for (name, _handle), result in zip(missing, results, strict=True):
1008
+ if isinstance(result, Exception):
1009
+ failures.append(
1010
+ ServerDiscoveryFailure(
1011
+ server=name,
1012
+ error=_discovery_error_message(result),
1013
+ )
1014
+ )
1015
+ elif isinstance(result, BaseException):
1016
+ raise result
1017
+ await self._rebuild_catalog()
1018
+ return failures
1019
+
984
1020
  async def _ensure_servers_discovered(self, server_names: Iterable[str]) -> None:
985
1021
  requested = set(server_names)
986
1022
  missing = [
@@ -1170,6 +1206,10 @@ def _elapsed_ms(started: float) -> float:
1170
1206
  return (time.perf_counter() - started) * 1_000
1171
1207
 
1172
1208
 
1209
+ def _discovery_error_message(error: Exception) -> str:
1210
+ return str(error).strip() or type(error).__name__
1211
+
1212
+
1173
1213
  def _parse_code(code: str) -> ast.AST | None:
1174
1214
  normalized = textwrap.dedent(code).strip("\n")
1175
1215
  wrapped = f"async def __codemcp_main():\n{textwrap.indent(normalized, ' ')}\n"
package/sidecar/models.py CHANGED
@@ -63,6 +63,13 @@ class ServerToolSummary(BaseModel):
63
63
  tool_count: int
64
64
 
65
65
 
66
+ class ServerDiscoveryFailure(BaseModel):
67
+ model_config = ConfigDict(extra="forbid", strict=True)
68
+
69
+ server: str
70
+ error: str
71
+
72
+
66
73
  class ExecutionLimitsView(BaseModel):
67
74
  model_config = ConfigDict(extra="forbid", strict=True)
68
75
 
@@ -85,6 +92,7 @@ class SearchResponse(BaseModel):
85
92
  has_more: bool = False
86
93
  project_scope_available: bool
87
94
  execution_limits: ExecutionLimitsView
95
+ discovery_failures: list[ServerDiscoveryFailure] = Field(default_factory=list)
88
96
  prelude: str | None = None
89
97
  results: list[ToolSchemaView]
90
98
 
package/src/tools.ts CHANGED
@@ -30,6 +30,7 @@ interface SearchRenderDetails extends CodeMcpOutputDetails {
30
30
  nextCursor?: number;
31
31
  detail: string;
32
32
  preview: string[];
33
+ discoveryFailures: string[];
33
34
  }
34
35
 
35
36
  const SearchParameters = Type.Object({
@@ -154,7 +155,7 @@ export function registerCodeMcpTools(
154
155
  name: "codemcp_search",
155
156
  label: "MCP Search",
156
157
  description:
157
- "Search configured MCP capabilities or page through compact inventory. Default signature search includes exact stubs for up to three top matches plus compact alternatives; codemcp_inspect loads other selected stubs. Returns ranking evidence, pagination, scope, and execution limits; invalid server names fail with suggestions.",
158
+ "Search configured MCP capabilities or page through compact inventory. Default signature search includes exact stubs for up to three top matches plus compact alternatives; codemcp_inspect loads other selected stubs. Unscoped searches return available results plus explicit discovery failures; scoped searches remain fail-fast. Returns ranking evidence, pagination, scope, and execution limits; invalid server names fail with suggestions.",
158
159
  promptSnippet: "Discover compact MCP capabilities or inventory",
159
160
  promptGuidelines: [...SEARCH_PROMPT_GUIDELINES],
160
161
  parameters: SearchParameters,
@@ -186,6 +187,11 @@ export function registerCodeMcpTools(
186
187
  return [`${String(item.name)} ${String(item.tool_count ?? 0)}`];
187
188
  })
188
189
  : [];
190
+ const discoveryFailures = Array.isArray(result.discovery_failures)
191
+ ? result.discovery_failures.flatMap((item) =>
192
+ isRecord(item) && typeof item.server === "string" ? [item.server] : [],
193
+ )
194
+ : [];
189
195
  return {
190
196
  content: [{ type: "text", text: output.text }],
191
197
  details: {
@@ -197,6 +203,7 @@ export function registerCodeMcpTools(
197
203
  nextCursor: typeof result.next_cursor === "number" ? result.next_cursor : undefined,
198
204
  detail: typeof result.detail === "string" ? result.detail : "signatures",
199
205
  preview,
206
+ discoveryFailures,
200
207
  },
201
208
  };
202
209
  },
@@ -212,13 +219,18 @@ export function registerCodeMcpTools(
212
219
  if (isPartial) return new Text(theme.fg("warning", "Searching catalog..."), 0, 0);
213
220
  if (expanded) return renderExpandedJson(result.content);
214
221
  const details = result.details as SearchRenderDetails | undefined;
222
+ const discoveryFailures = details?.discoveryFailures ?? [];
223
+ const unavailable = discoveryFailures.length;
215
224
  let text = theme.fg(
216
- "success",
217
- `${details?.matchCount ?? 0} matches · ${details?.detail ?? "signatures"} · ${details?.totalToolCount ?? 0} tools · ${details?.serverCount ?? 0} servers`,
225
+ unavailable > 0 ? "warning" : "success",
226
+ `${details?.matchCount ?? 0} matches · ${details?.detail ?? "signatures"} · ${details?.totalToolCount ?? 0} tools · ${details?.serverCount ?? 0} servers${unavailable > 0 ? ` · ${unavailable} unavailable` : ""}`,
218
227
  );
219
228
  for (const name of details?.preview ?? []) {
220
229
  text += `\n${theme.fg("dim", ` ${name}`)}`;
221
230
  }
231
+ for (const server of discoveryFailures) {
232
+ text += `\n${theme.fg("warning", ` unavailable: ${server}`)}`;
233
+ }
222
234
  if (details?.hasMore) {
223
235
  text += `\n${theme.fg("muted", ` more at cursor ${details.nextCursor ?? "?"}`)}`;
224
236
  }