pi-codemcp 1.1.2 → 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})
@@ -45,7 +45,7 @@ export function createCodeMcpExtension(options: SidecarClientOptions = {}) {
45
45
  return;
46
46
  }
47
47
 
48
- await showServerManagerModal(ctx, {
48
+ const managerResult = await showServerManagerModal(ctx, {
49
49
  servers,
50
50
  chains: chainStatesFromViews(savedChains),
51
51
  settings,
@@ -74,6 +74,7 @@ export function createCodeMcpExtension(options: SidecarClientOptions = {}) {
74
74
  onDeleteChain: async (chain) =>
75
75
  chainStatesFromViews(await chains.delete(chain.name, chain.scope)),
76
76
  });
77
+ if (managerResult === "report-problem") await promptForProblemReport(pi, ctx);
77
78
  } catch (error) {
78
79
  ctx.ui.notify(summarizeError(error), "error");
79
80
  }
@@ -163,6 +164,23 @@ export async function saveManagerChanges(
163
164
 
164
165
  export default createCodeMcpExtension();
165
166
 
167
+ export async function promptForProblemReport(
168
+ pi: Pick<ExtensionAPI, "sendUserMessage">,
169
+ ctx: Pick<ExtensionCommandContext, "ui">,
170
+ ): Promise<void> {
171
+ const description = await ctx.ui.editor("What went wrong?", "");
172
+ if (description?.trim()) pi.sendUserMessage(formatProblemReportPrompt(description.trim()));
173
+ }
174
+
175
+ export function formatProblemReportPrompt(description: string): string {
176
+ return `Something went wrong with pi-codemcp.
177
+
178
+ User's description:
179
+ ${description}
180
+
181
+ Investigate the problem in current pi setup. Inspect the available pi-codemcp configuration, environment, and installed package as needed. Determine the likely cause, then prepare a GitHub issue for https://github.com/yolonir/pi-codemcp. Do not include any personal or sensitive information in the issue. Do not autosumbit issue without clear approval.`;
182
+ }
183
+
166
184
  function requireServerStatus(
167
185
  status: Record<string, unknown>,
168
186
  serverName: string,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-codemcp",
3
- "version": "1.1.2",
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",
@@ -298,6 +298,7 @@ class MontyExecutor:
298
298
  type_stubs = catalog.type_stubs_for(referenced, include=spec.name)
299
299
  async with self._execution_lock:
300
300
  await self._type_check(wrapped, catalog, type_stubs)
301
+ await self._compile_runtime(code, catalog, has_input=True)
301
302
 
302
303
  def _new_context(
303
304
  self,
@@ -351,6 +352,9 @@ class MontyExecutor:
351
352
  except pydantic_monty.MontySyntaxError as error:
352
353
  context.metrics.typecheck_ms += _elapsed_ms(typecheck_started)
353
354
  return self._failure(context, "preflight", error.display("type-msg").strip())
355
+ except pydantic_monty.MontyRuntimeError as error:
356
+ context.metrics.typecheck_ms += _elapsed_ms(typecheck_started)
357
+ return self._failure(context, "preflight", error.display("type-msg").strip())
354
358
  except RuntimeError as error:
355
359
  context.metrics.typecheck_ms += _elapsed_ms(typecheck_started)
356
360
  return self._failure(
@@ -360,18 +364,22 @@ class MontyExecutor:
360
364
  )
361
365
  context.metrics.typecheck_ms += _elapsed_ms(typecheck_started)
362
366
 
363
- runtime_wrapped = _wrap_code(code, typed=False, has_input=has_input)
364
- runtime_code = _rewrite_sdk_calls(runtime_wrapped, context.catalog)
365
367
  try:
366
- monty = await pydantic_monty.Monty.acreate(
367
- runtime_code,
368
- script_name="codemcp_execute.py",
368
+ monty = await self._compile_runtime(
369
+ code,
370
+ context.catalog,
371
+ has_input=has_input,
369
372
  )
370
- except (pydantic_monty.MontySyntaxError, RuntimeError) as error:
373
+ except (
374
+ pydantic_monty.MontySyntaxError,
375
+ pydantic_monty.MontyRuntimeError,
376
+ RuntimeError,
377
+ NotImplementedError,
378
+ ) as error:
371
379
  return self._failure(
372
380
  context,
373
381
  "preflight",
374
- f"SDK facade compilation failed: {error}",
382
+ f"Sandbox compilation failed: {error}",
375
383
  )
376
384
 
377
385
  async def dispatch_wrapper(name: str, arguments: JsonObject) -> JsonValue:
@@ -530,6 +538,20 @@ class MontyExecutor:
530
538
  response.metrics = context.metrics.model_copy()
531
539
  return response
532
540
 
541
+ @staticmethod
542
+ async def _compile_runtime(
543
+ code: str,
544
+ catalog: ToolCatalog,
545
+ *,
546
+ has_input: bool,
547
+ ) -> pydantic_monty.Monty:
548
+ runtime_wrapped = _wrap_code(code, typed=False, has_input=has_input)
549
+ runtime_code = _rewrite_sdk_calls(runtime_wrapped, catalog)
550
+ return await pydantic_monty.Monty.acreate(
551
+ runtime_code,
552
+ script_name="codemcp_execute.py",
553
+ )
554
+
533
555
  async def _type_check(
534
556
  self,
535
557
  wrapped_code: str,
@@ -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
  )
@@ -727,11 +733,22 @@ class GatewayRuntime:
727
733
  spec = candidate_catalog.tools[candidate.public_name]
728
734
  try:
729
735
  await self.executor.validate_saved_chain(code, candidate_catalog, spec)
730
- except (pydantic_monty.MontyTypingError, pydantic_monty.MontySyntaxError) as error:
736
+ except (
737
+ pydantic_monty.MontyTypingError,
738
+ pydantic_monty.MontySyntaxError,
739
+ pydantic_monty.MontyRuntimeError,
740
+ NotImplementedError,
741
+ RuntimeError,
742
+ ) as error:
731
743
  if isinstance(error, pydantic_monty.MontyTypingError):
732
744
  message = error.display("concise", color=False).strip()
733
- else:
745
+ elif isinstance(
746
+ error,
747
+ (pydantic_monty.MontySyntaxError, pydantic_monty.MontyRuntimeError),
748
+ ):
734
749
  message = error.display("type-msg").strip()
750
+ else:
751
+ message = f"Sandbox compilation failed: {error}"
735
752
  raise ValueError(_saved_chain_preflight_error(message, output_schema)) from error
736
753
  dependencies = self._chain_dependencies(code, candidate_catalog)
737
754
  saved = ChainStore.build(
@@ -790,11 +807,22 @@ class GatewayRuntime:
790
807
  spec = candidate_catalog.tools[current.public_name]
791
808
  try:
792
809
  await self.executor.validate_saved_chain(current.code, candidate_catalog, spec)
793
- except (pydantic_monty.MontyTypingError, pydantic_monty.MontySyntaxError) as error:
810
+ except (
811
+ pydantic_monty.MontyTypingError,
812
+ pydantic_monty.MontySyntaxError,
813
+ pydantic_monty.MontyRuntimeError,
814
+ NotImplementedError,
815
+ RuntimeError,
816
+ ) as error:
794
817
  if isinstance(error, pydantic_monty.MontyTypingError):
795
818
  message = error.display("concise", color=False).strip()
796
- else:
819
+ elif isinstance(
820
+ error,
821
+ (pydantic_monty.MontySyntaxError, pydantic_monty.MontyRuntimeError),
822
+ ):
797
823
  message = error.display("type-msg").strip()
824
+ else:
825
+ message = f"Sandbox compilation failed: {error}"
798
826
  raise ValueError(
799
827
  _saved_chain_preflight_error(message, current.output_schema)
800
828
  ) from error
@@ -959,6 +987,36 @@ class GatewayRuntime:
959
987
  async def _ensure_catalog_complete(self) -> None:
960
988
  await self._ensure_servers_discovered(self.handles.keys())
961
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
+
962
1020
  async def _ensure_servers_discovered(self, server_names: Iterable[str]) -> None:
963
1021
  requested = set(server_names)
964
1022
  missing = [
@@ -1148,6 +1206,10 @@ def _elapsed_ms(started: float) -> float:
1148
1206
  return (time.perf_counter() - started) * 1_000
1149
1207
 
1150
1208
 
1209
+ def _discovery_error_message(error: Exception) -> str:
1210
+ return str(error).strip() or type(error).__name__
1211
+
1212
+
1151
1213
  def _parse_code(code: str) -> ast.AST | None:
1152
1214
  normalized = textwrap.dedent(code).strip("\n")
1153
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/modal.ts CHANGED
@@ -124,6 +124,7 @@ interface ManagerSaveResult {
124
124
  }
125
125
 
126
126
  type UnsavedAction = "save" | "discard" | "cancel";
127
+ export type ServerManagerResult = "report-problem" | undefined;
127
128
 
128
129
  interface ServerManagerOptions {
129
130
  servers: ServerModalState[];
@@ -159,6 +160,11 @@ const OVERLAY_OPTIONS = {
159
160
  maxHeight: "85%",
160
161
  } as const;
161
162
 
163
+ const PROBLEM_REPORT_LABEL = "Extension is broken!";
164
+ const PROBLEM_REPORT_DESCRIPTION =
165
+ "Well, that sucks. With this button you can ask the agent to describe the problem and prepare a GitHub issue for review. The goal is to make pi-codemcp usable for everyone, don't be lazy - submit an issue. Don't worry, you will see all prompts, this is a transparent process.";
166
+ const PROBLEM_REPORT_SHORTCUT = "Report issue: R";
167
+
162
168
  const SETTING_DEFINITIONS: SettingDefinition[] = [
163
169
  {
164
170
  key: "backgroundWarmup",
@@ -214,18 +220,18 @@ const SETTING_DEFINITIONS: SettingDefinition[] = [
214
220
  export async function showServerManagerModal(
215
221
  ctx: ExtensionCommandContext,
216
222
  options: ServerManagerOptions,
217
- ): Promise<void> {
223
+ ): Promise<ServerManagerResult> {
218
224
  if (ctx.mode !== "tui") {
219
225
  throw new Error("CodeMCP server manager requires interactive mode");
220
226
  }
221
227
 
222
- await ctx.ui.custom<void>(
228
+ return ctx.ui.custom<ServerManagerResult>(
223
229
  (tui, theme, keybindings, done) =>
224
230
  new ServerManagerModal(
225
231
  options,
226
232
  theme,
227
233
  keybindings,
228
- () => done(undefined),
234
+ (result) => done(result),
229
235
  () => tui.requestRender(),
230
236
  ),
231
237
  { overlay: true, overlayOptions: OVERLAY_OPTIONS },
@@ -352,7 +358,7 @@ class ServerManagerModal implements Component, Focusable {
352
358
  private readonly options: ServerManagerOptions,
353
359
  private readonly theme: Theme,
354
360
  private readonly keybindings: Keybindings,
355
- private readonly close: () => void,
361
+ private readonly close: (result?: ServerManagerResult) => void,
356
362
  private readonly requestRender: () => void,
357
363
  ) {
358
364
  this.savedSettings = cloneSettings(options.settings);
@@ -400,6 +406,11 @@ class ServerManagerModal implements Component, Focusable {
400
406
  return;
401
407
  }
402
408
  if (this.settingsBusy || this.closePromptBusy) return;
409
+ if (data === "R") {
410
+ this.focusProblemReport();
411
+ this.requestRender();
412
+ return;
413
+ }
403
414
  if (this.keybindings.matches(data, "tui.select.cancel") || matchesKey(data, Key.escape)) {
404
415
  if (this.activeTab !== "settings" && this.search.getValue()) {
405
416
  this.search.setValue("");
@@ -504,7 +515,7 @@ class ServerManagerModal implements Component, Focusable {
504
515
  this.selectedSettingIndex = cycleIndex(
505
516
  this.selectedSettingIndex,
506
517
  -1,
507
- SETTING_DEFINITIONS.length,
518
+ SETTING_DEFINITIONS.length + 1,
508
519
  );
509
520
  return;
510
521
  }
@@ -512,10 +523,14 @@ class ServerManagerModal implements Component, Focusable {
512
523
  this.selectedSettingIndex = cycleIndex(
513
524
  this.selectedSettingIndex,
514
525
  1,
515
- SETTING_DEFINITIONS.length,
526
+ SETTING_DEFINITIONS.length + 1,
516
527
  );
517
528
  return;
518
529
  }
530
+ if (this.selectedSettingIndex === SETTING_DEFINITIONS.length) {
531
+ if (matchesKey(data, Key.enter) || data === " ") this.openProblemReport();
532
+ return;
533
+ }
519
534
  if (matchesKey(data, Key.left)) this.cycleSelectedSetting(-1);
520
535
  else if (matchesKey(data, Key.right) || matchesKey(data, Key.enter) || data === " ") {
521
536
  this.cycleSelectedSetting(1);
@@ -820,27 +835,50 @@ class ServerManagerModal implements Component, Focusable {
820
835
  ),
821
836
  );
822
837
  }
838
+ const problemReportSelected = this.selectedSettingIndex === SETTING_DEFINITIONS.length;
839
+ const problemReportPrefix = problemReportSelected ? this.theme.fg("accent", "→") : " ";
840
+ left.push(
841
+ "",
842
+ truncateToWidth(
843
+ `${problemReportPrefix} ${
844
+ problemReportSelected
845
+ ? this.theme.fg("accent", PROBLEM_REPORT_LABEL)
846
+ : PROBLEM_REPORT_LABEL
847
+ }`,
848
+ leftWidth,
849
+ ),
850
+ );
823
851
  const definition = SETTING_DEFINITIONS[this.selectedSettingIndex];
824
- const right = definition
852
+ const right = problemReportSelected
825
853
  ? [
826
- this.theme.fg("accent", this.theme.bold(definition.label)),
827
- this.theme.fg("muted", settingLabel(definition, this.draftSettings[definition.key])),
854
+ this.theme.fg("accent", this.theme.bold(PROBLEM_REPORT_LABEL)),
828
855
  "",
829
- ...wrapPlainText(definition.description, rightWidth).map((line) =>
856
+ ...wrapPlainText(PROBLEM_REPORT_DESCRIPTION, rightWidth).map((line) =>
830
857
  this.theme.fg("muted", line),
831
858
  ),
832
859
  "",
833
- this.theme.fg("dim", "←/→ change · enter next · ctrl+s save"),
834
- ...(this.settingsBusy
835
- ? ["", this.theme.fg("warning", "Saving staged changes…")]
836
- : this.hasUnsavedChanges()
837
- ? ["", this.theme.fg("warning", "Unsaved changes")]
838
- : []),
839
- ...(this.settingsError
840
- ? ["", this.theme.fg("warning", `Error: ${this.settingsError}`)]
841
- : []),
860
+ this.theme.fg("dim", "enter describe the problem"),
842
861
  ]
843
- : [];
862
+ : definition
863
+ ? [
864
+ this.theme.fg("accent", this.theme.bold(definition.label)),
865
+ this.theme.fg("muted", settingLabel(definition, this.draftSettings[definition.key])),
866
+ "",
867
+ ...wrapPlainText(definition.description, rightWidth).map((line) =>
868
+ this.theme.fg("muted", line),
869
+ ),
870
+ "",
871
+ this.theme.fg("dim", "←/→ change · enter next · ctrl+s save"),
872
+ ...(this.settingsBusy
873
+ ? ["", this.theme.fg("warning", "Saving staged changes…")]
874
+ : this.hasUnsavedChanges()
875
+ ? ["", this.theme.fg("warning", "Unsaved changes")]
876
+ : []),
877
+ ...(this.settingsError
878
+ ? ["", this.theme.fg("warning", `Error: ${this.settingsError}`)]
879
+ : []),
880
+ ]
881
+ : [];
844
882
  const lines: string[] = [];
845
883
  for (let index = 0; index < splitHeight; index += 1) {
846
884
  lines.push(
@@ -852,16 +890,17 @@ class ServerManagerModal implements Component, Focusable {
852
890
 
853
891
  private footer(): string {
854
892
  const pending = this.hasUnsavedChanges() ? " · * unsaved · ctrl+s save" : "";
893
+ const report = ` · ${PROBLEM_REPORT_SHORTCUT}`;
855
894
  if (this.activeTab === "settings") {
856
- return `tab servers · ↑/↓ navigate · ←/→/enter change · ctrl+s save · esc close${pending}`;
895
+ return `tab servers · ↑/↓ navigate · ←/→ change · enter select · ctrl+s save · esc close${pending}${report}`;
857
896
  }
858
897
  if (this.activeTab === "chains") {
859
- return `tab stats · ↑/↓ navigate · space toggle · r revalidate · del delete · esc close${pending}`;
898
+ return `tab stats · ↑/↓ navigate · space toggle · r revalidate · del delete · esc close${pending}${report}`;
860
899
  }
861
900
  if (this.activeTab === "stats") {
862
- return `tab settings · bounded local rollups · esc close${pending}`;
901
+ return `tab settings · bounded local rollups · esc close${pending}${report}`;
863
902
  }
864
- return `tab chains · ←/→ pane · ↑/↓ navigate · space toggle · d discover · esc close${pending}`;
903
+ return `tab chains · ←/→ pane · ↑/↓ navigate · space toggle · d discover · esc close${pending}${report}`;
865
904
  }
866
905
 
867
906
  private moveSelection(direction: -1 | 1): void {
@@ -1053,17 +1092,29 @@ class ServerManagerModal implements Component, Focusable {
1053
1092
  }
1054
1093
  }
1055
1094
 
1056
- private resolveUnsavedClose(): void {
1095
+ private focusProblemReport(): void {
1096
+ this.activeTab = "settings";
1097
+ this.selectedSettingIndex = SETTING_DEFINITIONS.length;
1098
+ this.search.setValue("");
1099
+ this.search.focused = false;
1100
+ }
1101
+
1102
+ private openProblemReport(): void {
1103
+ if (this.hasUnsavedChanges()) this.resolveUnsavedClose("report-problem");
1104
+ else this.close("report-problem");
1105
+ }
1106
+
1107
+ private resolveUnsavedClose(result?: ServerManagerResult): void {
1057
1108
  if (this.closePromptBusy || this.settingsBusy) return;
1058
1109
  this.closePromptBusy = true;
1059
1110
  void this.options
1060
1111
  .onResolveUnsaved()
1061
1112
  .then(async (action) => {
1062
1113
  if (action === "discard") {
1063
- this.close();
1114
+ this.close(result);
1064
1115
  return;
1065
1116
  }
1066
- if (action === "save" && (await this.persistDraft())) this.close();
1117
+ if (action === "save" && (await this.persistDraft())) this.close(result);
1067
1118
  })
1068
1119
  .catch((error: unknown) => {
1069
1120
  this.settingsError = summarizeError(error);
package/src/prompts.ts CHANGED
@@ -11,11 +11,12 @@ export const EXECUTE_PROMPT_GUIDELINES = [
11
11
  "Use programmatic execution for a bounded workflow when code can deterministically filter, join, aggregate, deduplicate, validate, or reduce intermediate results.",
12
12
  "Keep a model turn between calls when an intermediate result changes the semantic decision or user approval is required.",
13
13
  "Return the smallest result that answers the request; oversized results fail explicitly with bounded structural inspection data.",
14
- "SDK facades returned by search are prebound globals and must not be imported; use a normal import statement such as `import asyncio` before `asyncio.gather`, because `__import__` is unavailable.",
14
+ "SDK facades are prebound globals and must not be imported. Import supported stdlib normally (for example, `import asyncio`); class declarations and `__import__` are unsupported.",
15
15
  ] as const;
16
16
 
17
17
  export const SAVE_CHAIN_PROMPT_GUIDELINES = [
18
18
  "Save only after the user explicitly asks or accepts, and only after the same code has executed successfully.",
19
+ "Use generated result item types for nested schema collections; do not declare TypedDict classes.",
19
20
  "Use project scope when available unless the user explicitly requests global scope; make schemas describe the exact parameterized contract.",
20
21
  ] as const;
21
22
 
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
  }