fix(coding-agent,ai): resolve issues #33, #34, #35, #37

- show help instead of crashing on `omp setup` with no args
- show runtime-discovered MCP servers in `/mcp list`
- remove deprecated Anthropic model entries from models.json
- sort models by recency in model selector
This commit is contained in:
can1357
2026-02-12 18:12:31 +01:00
parent 7a3f63eb4a
commit 412ab9ea00
20 changed files with 151 additions and 160 deletions
+7
View File
@@ -1,6 +1,13 @@
# Changelog
## [Unreleased]
### Removed
- Removed deprecated Anthropic model entries: `claude-3-5-haiku-latest`, `claude-3-5-haiku-20241022`, `claude-3-7-sonnet-20250219`, `claude-3-7-sonnet-latest`, `claude-3-opus-20240229`, `claude-3-sonnet-20240229` ([#33](https://github.com/can1357/oh-my-pi/issues/33))
### Fixed
- Added deprecation filter in model generation script to prevent re-adding deprecated Anthropic models ([#33](https://github.com/can1357/oh-my-pi/issues/33))
## [11.14.1] - 2026-02-12
### Added
+2 -2
View File
@@ -856,7 +856,7 @@ const restored: Context = JSON.parse(localStorage.getItem("conversation")!);
restored.messages.push({ role: "user", content: "Tell me more about its type system" });
// Continue with any model
const newModel = getModel("anthropic", "claude-3-5-haiku-20241022");
const newModel = getModel("anthropic", "claude-haiku-4-5-20251001");
const continuation = await complete(newModel, restored);
```
@@ -870,7 +870,7 @@ The library supports browser environments. You must pass the API key explicitly
import { getModel, complete } from "@oh-my-pi/pi-ai";
// API key must be passed explicitly in browser
const model = getModel("anthropic", "claude-3-5-haiku-20241022");
const model = getModel("anthropic", "claude-haiku-4-5-20251001");
const response = await complete(
model,
+10
View File
@@ -442,6 +442,16 @@ async function loadModelsDevData(): Promise<Model[]> {
const m = model as ModelsDevModel;
if (m.tool_call !== true) continue;
// Skip deprecated Anthropic models (old naming convention)
if (
modelId.startsWith("claude-3-5-haiku") ||
modelId.startsWith("claude-3-7-sonnet") ||
modelId === "claude-3-opus-20240229" ||
modelId === "claude-3-sonnet-20240229"
) {
continue;
}
models.push({
id: modelId,
name: m.name || modelId,
-120
View File
@@ -1452,26 +1452,6 @@
"contextWindow": 200000,
"maxTokens": 128000
},
"claude-3-5-haiku-latest": {
"id": "claude-3-5-haiku-latest",
"name": "Claude Haiku 3.5 (latest)",
"api": "anthropic-messages",
"provider": "anthropic",
"baseUrl": "https://api.anthropic.com",
"reasoning": false,
"input": [
"text",
"image"
],
"cost": {
"input": 0.8,
"output": 4,
"cacheRead": 0.08,
"cacheWrite": 1
},
"contextWindow": 200000,
"maxTokens": 8192
},
"claude-opus-4-5": {
"id": "claude-opus-4-5",
"name": "Claude Opus 4.5 (latest)",
@@ -1492,26 +1472,6 @@
"contextWindow": 200000,
"maxTokens": 64000
},
"claude-3-opus-20240229": {
"id": "claude-3-opus-20240229",
"name": "Claude Opus 3",
"api": "anthropic-messages",
"provider": "anthropic",
"baseUrl": "https://api.anthropic.com",
"reasoning": false,
"input": [
"text",
"image"
],
"cost": {
"input": 15,
"output": 75,
"cacheRead": 1.5,
"cacheWrite": 18.75
},
"contextWindow": 200000,
"maxTokens": 4096
},
"claude-opus-4-5-20251101": {
"id": "claude-opus-4-5-20251101",
"name": "Claude Opus 4.5",
@@ -1612,26 +1572,6 @@
"contextWindow": 200000,
"maxTokens": 32000
},
"claude-3-5-haiku-20241022": {
"id": "claude-3-5-haiku-20241022",
"name": "Claude Haiku 3.5",
"api": "anthropic-messages",
"provider": "anthropic",
"baseUrl": "https://api.anthropic.com",
"reasoning": false,
"input": [
"text",
"image"
],
"cost": {
"input": 0.8,
"output": 4,
"cacheRead": 0.08,
"cacheWrite": 1
},
"contextWindow": 200000,
"maxTokens": 8192
},
"claude-3-haiku-20240307": {
"id": "claude-3-haiku-20240307",
"name": "Claude Haiku 3",
@@ -1652,46 +1592,6 @@
"contextWindow": 200000,
"maxTokens": 4096
},
"claude-3-7-sonnet-20250219": {
"id": "claude-3-7-sonnet-20250219",
"name": "Claude Sonnet 3.7",
"api": "anthropic-messages",
"provider": "anthropic",
"baseUrl": "https://api.anthropic.com",
"reasoning": true,
"input": [
"text",
"image"
],
"cost": {
"input": 3,
"output": 15,
"cacheRead": 0.3,
"cacheWrite": 3.75
},
"contextWindow": 200000,
"maxTokens": 64000
},
"claude-3-7-sonnet-latest": {
"id": "claude-3-7-sonnet-latest",
"name": "Claude Sonnet 3.7 (latest)",
"api": "anthropic-messages",
"provider": "anthropic",
"baseUrl": "https://api.anthropic.com",
"reasoning": true,
"input": [
"text",
"image"
],
"cost": {
"input": 3,
"output": 15,
"cacheRead": 0.3,
"cacheWrite": 3.75
},
"contextWindow": 200000,
"maxTokens": 64000
},
"claude-sonnet-4-0": {
"id": "claude-sonnet-4-0",
"name": "Claude Sonnet 4 (latest)",
@@ -1732,26 +1632,6 @@
"contextWindow": 200000,
"maxTokens": 32000
},
"claude-3-sonnet-20240229": {
"id": "claude-3-sonnet-20240229",
"name": "Claude Sonnet 3",
"api": "anthropic-messages",
"provider": "anthropic",
"baseUrl": "https://api.anthropic.com",
"reasoning": false,
"input": [
"text",
"image"
],
"cost": {
"input": 3,
"output": 15,
"cacheRead": 0.3,
"cacheWrite": 0.3
},
"contextWindow": 200000,
"maxTokens": 4096
},
"claude-haiku-4-5-20251001": {
"id": "claude-haiku-4-5-20251001",
"name": "Claude Haiku 4.5",
+5 -5
View File
@@ -331,17 +331,17 @@ function resolveCacheRetention(cacheRetention?: CacheRetention): CacheRetention
/**
* Check if the model supports prompt caching.
* Supported: Claude 3.5 Haiku, Claude 3.7 Sonnet, Claude 4.x models
* Supported: Claude 3.5 Haiku, Claude 3.7 Sonnet, Claude 4.x+ models, Haiku 4.5+
*/
function supportsPromptCaching(model: Model<"bedrock-converse-stream">): boolean {
if (model.cost.cacheRead || model.cost.cacheWrite) return true;
const id = model.id.toLowerCase();
// Claude 4.x models (opus-4, sonnet-4, haiku-4)
if (id.includes("claude") && (id.includes("-4-") || id.includes("-4."))) return true;
// Claude 3.7 Sonnet
if (id.includes("claude-3-7-sonnet")) return true;
// Claude 3.5 Haiku
if (id.includes("claude-3-5-haiku")) return true;
// Claude 3.5 Haiku, Claude 3.7 Sonnet (legacy naming)
if (id.includes("claude-3-7-sonnet") || id.includes("claude-3-5-haiku")) return true;
// Claude Haiku 4.5+ (new naming)
if (id.includes("claude-haiku")) return true;
return false;
}
+1 -1
View File
@@ -642,7 +642,7 @@ function buildParams(
stream: true,
};
const includeClaudeCodeSystem = !model.id.startsWith("claude-3-5-haiku");
const includeClaudeCodeSystem = !model.id.includes("haiku");
const systemBlocks = buildAnthropicSystemBlocks(context.systemPrompt, {
includeClaudeCodeInstruction: includeClaudeCodeSystem,
});
+2 -2
View File
@@ -98,8 +98,8 @@ function logResult(result: OverflowResult) {
describe("Context overflow error handling", () => {
describe.skipIf(!e2eApiKey("ANTHROPIC_API_KEY"))("Anthropic (API Key)", () => {
it("claude-3-5-haiku - should detect overflow via isContextOverflow", async () => {
const model = getModel("anthropic", "claude-3-5-haiku-20241022");
it("claude-haiku-4-5 - should detect overflow via isContextOverflow", async () => {
const model = getModel("anthropic", "claude-haiku-4-5-20251001");
const result = await testContextOverflow(model, Bun.env.ANTHROPIC_API_KEY!);
logResult(result);
+2 -2
View File
@@ -250,7 +250,7 @@ describe("AI Providers Empty Message Tests", () => {
});
describe.skipIf(!e2eApiKey("ANTHROPIC_API_KEY"))("Anthropic Provider Empty Messages", () => {
const llm = getModel("anthropic", "claude-3-5-haiku-20241022");
const llm = getModel("anthropic", "claude-haiku-4-5-20251001");
it(
"should handle empty content array",
@@ -466,7 +466,7 @@ describe("AI Providers Empty Message Tests", () => {
});
describe("Anthropic OAuth Provider Empty Messages", () => {
const llm = getModel("anthropic", "claude-3-5-haiku-20241022");
const llm = getModel("anthropic", "claude-haiku-4-5-20251001");
it.skipIf(!anthropicOAuthToken)(
"should handle empty content array",
+7 -7
View File
@@ -41,7 +41,7 @@ const providerContexts = {
},
],
provider: "anthropic",
model: "claude-3-5-haiku-latest",
model: "claude-haiku-4-5",
usage: {
input: 100,
output: 50,
@@ -368,13 +368,13 @@ async function testProviderHandoff<TApi extends Api>(
describe("Cross-Provider Handoff Tests", () => {
describe.skipIf(!e2eApiKey("ANTHROPIC_API_KEY"))("Anthropic Provider Handoff", () => {
const model = getModel("anthropic", "claude-3-5-haiku-20241022");
const model = getModel("anthropic", "claude-haiku-4-5-20251001");
it("should handle contexts from all providers", async () => {
console.log("\nTesting Anthropic with pre-built contexts:\n");
const contextTests = [
{ label: "Anthropic-style", context: providerContexts.anthropic, sourceModel: "claude-3-5-haiku-20241022" },
{ label: "Anthropic-style", context: providerContexts.anthropic, sourceModel: "claude-haiku-4-5-20251001" },
{ label: "Google-style", context: providerContexts.google, sourceModel: "gemini-2.5-flash" },
{ label: "OpenAI-Completions", context: providerContexts.openaiCompletions, sourceModel: "gpt-4o-mini" },
{ label: "OpenAI-Responses", context: providerContexts.openaiResponses, sourceModel: "gpt-5-mini" },
@@ -410,7 +410,7 @@ describe("Cross-Provider Handoff Tests", () => {
console.log("\nTesting Google with pre-built contexts:\n");
const contextTests = [
{ label: "Anthropic-style", context: providerContexts.anthropic, sourceModel: "claude-3-5-haiku-20241022" },
{ label: "Anthropic-style", context: providerContexts.anthropic, sourceModel: "claude-haiku-4-5-20251001" },
{ label: "Google-style", context: providerContexts.google, sourceModel: "gemini-2.5-flash" },
{ label: "OpenAI-Completions", context: providerContexts.openaiCompletions, sourceModel: "gpt-4o-mini" },
{ label: "OpenAI-Responses", context: providerContexts.openaiResponses, sourceModel: "gpt-5-mini" },
@@ -446,7 +446,7 @@ describe("Cross-Provider Handoff Tests", () => {
console.log("\nTesting OpenAI Completions with pre-built contexts:\n");
const contextTests = [
{ label: "Anthropic-style", context: providerContexts.anthropic, sourceModel: "claude-3-5-haiku-20241022" },
{ label: "Anthropic-style", context: providerContexts.anthropic, sourceModel: "claude-haiku-4-5-20251001" },
{ label: "Google-style", context: providerContexts.google, sourceModel: "gemini-2.5-flash" },
{ label: "OpenAI-Completions", context: providerContexts.openaiCompletions, sourceModel: "gpt-4o-mini" },
{ label: "OpenAI-Responses", context: providerContexts.openaiResponses, sourceModel: "gpt-5-mini" },
@@ -482,7 +482,7 @@ describe("Cross-Provider Handoff Tests", () => {
console.log("\nTesting OpenAI Responses with pre-built contexts:\n");
const contextTests = [
{ label: "Anthropic-style", context: providerContexts.anthropic, sourceModel: "claude-3-5-haiku-20241022" },
{ label: "Anthropic-style", context: providerContexts.anthropic, sourceModel: "claude-haiku-4-5-20251001" },
{ label: "Google-style", context: providerContexts.google, sourceModel: "gemini-2.5-flash" },
{ label: "OpenAI-Completions", context: providerContexts.openaiCompletions, sourceModel: "gpt-4o-mini" },
{ label: "OpenAI-Responses", context: providerContexts.openaiResponses, sourceModel: "gpt-5-mini" },
@@ -518,7 +518,7 @@ describe("Cross-Provider Handoff Tests", () => {
console.log("\nTesting Mistral with pre-built contexts:\n");
const contextTests = [
{ label: "Anthropic-style", context: providerContexts.anthropic, sourceModel: "claude-3-5-haiku-20241022" },
{ label: "Anthropic-style", context: providerContexts.anthropic, sourceModel: "claude-haiku-4-5-20251001" },
{ label: "Google-style", context: providerContexts.google, sourceModel: "gemini-2.5-flash" },
{ label: "OpenAI-Completions", context: providerContexts.openaiCompletions, sourceModel: "gpt-4o-mini" },
{ label: "OpenAI-Responses", context: providerContexts.openaiResponses, sourceModel: "gpt-5-mini" },
+5 -5
View File
@@ -14,7 +14,7 @@
* BASIC LIMITS (small images):
* | Provider | Model | Max Images | Max Size | Max Dim |
* |-------------|--------------------|------------|----------|----------|
* | Anthropic | claude-3-5-haiku | 100 | 5MB | 8000px |
* | Anthropic | claude-haiku-4-5 | 100 | 5MB | 8000px |
* | OpenAI | gpt-4o-mini | 500 | ≥25MB | ≥20000px |
* | Gemini | gemini-2.5-flash | ~2000* | ≥40MB | 8000px |
* | Mistral | pixtral-12b | 8 | ~15MB | 8000px |
@@ -275,11 +275,11 @@ describe("Image Limits E2E Tests", () => {
});
// -------------------------------------------------------------------------
// Anthropic (claude-3-5-haiku-20241022)
// Anthropic (claude-haiku-4-5-20251001)
// Limits: 100 images, 5MB per image, 8000px max dimension
// -------------------------------------------------------------------------
describe.skipIf(!e2eApiKey("ANTHROPIC_API_KEY"))("Anthropic (claude-3-5-haiku-20241022)", () => {
const model = getModel("anthropic", "claude-3-5-haiku-20241022");
describe.skipIf(!e2eApiKey("ANTHROPIC_API_KEY"))("Anthropic (claude-haiku-4-5-20251001)", () => {
const model = getModel("anthropic", "claude-haiku-4-5-20251001");
it("should accept a small number of images (5)", async () => {
const result = await testImageCount(model, 5, smallImage);
@@ -979,7 +979,7 @@ describe("Image Limits E2E Tests", () => {
it.skipIf(!e2eApiKey("ANTHROPIC_API_KEY"))(
"Anthropic: max ~3MB images before rejection",
async () => {
const model = getModel("anthropic", "claude-3-5-haiku-20241022");
const model = getModel("anthropic", "claude-haiku-4-5-20251001");
const image3mb = await getImageAtSize(3);
// 32MB total limit / ~4MB actual = ~8 images
const counts = [1, 2, 4, 6, 8, 10, 12];
+2 -2
View File
@@ -553,8 +553,8 @@ describe("Generate E2E Tests", () => {
);
});
describe.skipIf(!e2eApiKey("ANTHROPIC_API_KEY"))("Anthropic Provider (claude-3-5-haiku-20241022)", () => {
const model = getModel("anthropic", "claude-3-5-haiku-20241022");
describe.skipIf(!e2eApiKey("ANTHROPIC_API_KEY"))("Anthropic Provider (claude-haiku-4-5-20251001)", () => {
const model = getModel("anthropic", "claude-haiku-4-5-20251001");
it(
"should complete basic text generation",
+2 -2
View File
@@ -109,7 +109,7 @@ describe("Token Statistics on Abort", () => {
});
describe.skipIf(!e2eApiKey("ANTHROPIC_API_KEY"))("Anthropic Provider", () => {
const llm = getModel("anthropic", "claude-3-5-haiku-20241022");
const llm = getModel("anthropic", "claude-haiku-4-5-20251001");
it(
"should include token stats when aborted mid-stream",
@@ -181,7 +181,7 @@ describe("Token Statistics on Abort", () => {
});
describe("Anthropic OAuth Provider", () => {
const llm = getModel("anthropic", "claude-3-5-haiku-20241022");
const llm = getModel("anthropic", "claude-haiku-4-5-20251001");
it.skipIf(!anthropicOAuthToken)(
"should include token stats when aborted mid-stream",
@@ -135,7 +135,7 @@ describe("Tool Call Without Result Tests", () => {
});
describe.skipIf(!e2eApiKey("ANTHROPIC_API_KEY"))("Anthropic Provider", () => {
const model = getModel("anthropic", "claude-3-5-haiku-20241022");
const model = getModel("anthropic", "claude-haiku-4-5-20251001");
it(
"should filter out tool calls without corresponding tool results",
@@ -207,7 +207,7 @@ describe("Tool Call Without Result Tests", () => {
});
describe("Anthropic OAuth Provider", () => {
const model = getModel("anthropic", "claude-3-5-haiku-20241022");
const model = getModel("anthropic", "claude-haiku-4-5-20251001");
it.skipIf(!anthropicOAuthToken)(
"should filter out tool calls without corresponding tool results",
+2 -2
View File
@@ -101,9 +101,9 @@ describe("totalTokens field", () => {
describe.skipIf(!e2eApiKey("ANTHROPIC_API_KEY"))("Anthropic (API Key)", () => {
it(
"claude-3-5-haiku - should return totalTokens equal to sum of components",
"claude-haiku-4-5 - should return totalTokens equal to sum of components",
async () => {
const llm = getModel("anthropic", "claude-3-5-haiku-20241022");
const llm = getModel("anthropic", "claude-haiku-4-5-20251001");
console.log(`\nAnthropic / ${llm.id}:`);
const { first, second } = await testTotalTokensWithCache(llm, { apiKey: Bun.env.ANTHROPIC_API_KEY });
+2 -2
View File
@@ -362,7 +362,7 @@ describe("AI Providers Unicode Surrogate Pair Tests", () => {
});
describe.skipIf(!e2eApiKey("ANTHROPIC_API_KEY"))("Anthropic Provider Unicode Handling", () => {
const llm = getModel("anthropic", "claude-3-5-haiku-20241022");
const llm = getModel("anthropic", "claude-haiku-4-5-20251001");
it(
"should handle emoji in tool results",
@@ -390,7 +390,7 @@ describe("AI Providers Unicode Surrogate Pair Tests", () => {
});
describe("Anthropic OAuth Provider Unicode Handling", () => {
const llm = getModel("anthropic", "claude-3-5-haiku-20241022");
const llm = getModel("anthropic", "claude-haiku-4-5-20251001");
it.skipIf(!anthropicOAuthToken)(
"should handle emoji in tool results",
+9
View File
@@ -1,6 +1,15 @@
# Changelog
## [Unreleased]
### Added
- Added `getAllServerNames()` method to MCPManager for enumerating all known servers
### Fixed
- Fixed `omp setup` crashing with uncaught exception when no component argument provided; now shows help ([#35](https://github.com/can1357/oh-my-pi/issues/35))
- Fixed `/mcp list` showing "No MCP servers configured" when servers are loaded from discovery sources like `.claude.json`, `.cursor/mcp.json`, `.vscode/mcp.json` ([#34](https://github.com/can1357/oh-my-pi/issues/34))
- Fixed model selector sorting to show newest models first within each provider instead of alphabetical; `-latest` aliases now appear before dated versions ([#37](https://github.com/can1357/oh-my-pi/issues/37))
## [11.14.4] - 2026-02-12
+6 -3
View File
@@ -1,7 +1,7 @@
/**
* Install dependencies for optional features.
*/
import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
import { Args, Command, Flags, renderCommandHelp } from "@oh-my-pi/pi-utils/cli";
import { runSetupCommand, type SetupCommandArgs, type SetupComponent } from "../cli/setup-cli";
import { initTheme } from "../modes/theme/theme";
@@ -13,7 +13,7 @@ export default class Setup extends Command {
static args = {
component: Args.string({
description: "Component to install",
required: true,
required: false,
options: COMPONENTS,
}),
};
@@ -25,6 +25,10 @@ export default class Setup extends Command {
async run(): Promise<void> {
const { args, flags } = await this.parse(Setup);
if (!args.component) {
renderCommandHelp("omp", "setup", Setup);
return;
}
const cmd: SetupCommandArgs = {
component: args.component as SetupComponent,
flags: {
@@ -32,7 +36,6 @@ export default class Setup extends Command {
check: flags.check,
},
};
await initTheme();
await runSetupCommand(cmd);
}
+9
View File
@@ -343,6 +343,15 @@ export class MCPManager {
return Array.from(this.#connections.keys());
}
/**
* Get all known server names (connected, connecting, or discovered).
*/
getAllServerNames(): string[] {
return Array.from(
new Set([...this.#sources.keys(), ...this.#connections.keys(), ...this.#pendingConnections.keys()]),
);
}
/**
* Disconnect from a specific server.
*/
@@ -203,6 +203,9 @@ export class ModelSelectorComponent extends Container {
return i;
};
const dateRe = /-(\d{8})$/;
const latestRe = /-latest$/;
models.sort((a, b) => {
const aKey = `${a.provider}/${a.id}`;
const bKey = `${b.provider}/${b.id}`;
@@ -216,10 +219,33 @@ export class ModelSelectorComponent extends Container {
const bMru = mruIndex.get(bKey) ?? Number.MAX_SAFE_INTEGER;
if (aMru !== bMru) return aMru - bMru;
// Finally alphabetical by provider, then id
// By provider, then recency within provider
const providerCmp = a.provider.localeCompare(b.provider);
if (providerCmp !== 0) return providerCmp;
return a.id.localeCompare(b.id);
const aIsLatest = latestRe.test(a.id);
const bIsLatest = latestRe.test(b.id);
const aDate = a.id.match(dateRe)?.[1] ?? "";
const bDate = b.id.match(dateRe)?.[1] ?? "";
// Both have dates or latest tags — sort by recency
const aHasRecency = aIsLatest || aDate !== "";
const bHasRecency = bIsLatest || bDate !== "";
// Models with recency info come before those without
if (aHasRecency !== bHasRecency) return aHasRecency ? -1 : 1;
// If neither has recency info, fall back to alphabetical
if (!aHasRecency) return a.id.localeCompare(b.id);
// -latest always sorts first within recency group
if (aIsLatest !== bIsLatest) return aIsLatest ? -1 : 1;
// Both have dates — descending (newest first)
if (aDate && bDate) return bDate.localeCompare(aDate);
// One has date, other is latest — latest first
return aIsLatest ? -1 : bIsLatest ? 1 : a.id.localeCompare(b.id);
});
}
@@ -4,6 +4,7 @@
* Handles /mcp subcommands for managing MCP servers.
*/
import { Spacer, Text } from "@oh-my-pi/pi-tui";
import type { SourceMeta } from "../../capability/types";
import { analyzeAuthError, discoverOAuthEndpoints, MCPManager } from "../../mcp";
import { connectToServer, disconnectServer, listTools } from "../../mcp/client";
import {
@@ -765,7 +766,20 @@ export class MCPCommandController {
const userServers = Object.keys(userConfig.mcpServers ?? {});
const projectServers = Object.keys(projectConfig.mcpServers ?? {});
if (userServers.length === 0 && projectServers.length === 0) {
// Collect runtime-discovered servers not in config files
const configServerNames = new Set([...userServers, ...projectServers]);
const discoveredServers: { name: string; source: SourceMeta }[] = [];
if (this.ctx.mcpManager) {
for (const name of this.ctx.mcpManager.getAllServerNames()) {
if (configServerNames.has(name)) continue;
const source = this.ctx.mcpManager.getSource(name);
if (source) {
discoveredServers.push({ name, source });
}
}
}
if (userServers.length === 0 && projectServers.length === 0 && discoveredServers.length === 0) {
this.#showMessage(
[
"",
@@ -826,6 +840,39 @@ export class MCPCommandController {
lines.push("");
}
// Show discovered servers (from .claude.json, .cursor/mcp.json, .vscode/mcp.json, etc.)
if (discoveredServers.length > 0) {
// Group by source display name + path
const bySource = new Map<string, typeof discoveredServers>();
for (const entry of discoveredServers) {
const key = `${entry.source.providerName}|${entry.source.path}`;
let group = bySource.get(key);
if (!group) {
group = [];
bySource.set(key, group);
}
group.push(entry);
}
for (const [key, entries] of bySource) {
const sepIdx = key.indexOf("|");
const providerName = key.slice(0, sepIdx);
const sourcePath = key.slice(sepIdx + 1);
const shortPath = sourcePath.replace(process.env.HOME ?? "", "~");
lines.push(theme.fg("accent", providerName) + theme.fg("muted", ` (${shortPath}):`));
for (const { name } of entries) {
const state = this.ctx.mcpManager!.getConnectionStatus(name);
const status =
state === "connected"
? theme.fg("success", " ● connected")
: state === "connecting"
? theme.fg("muted", " ◌ connecting")
: theme.fg("muted", " ○ not connected");
lines.push(` ${theme.fg("accent", name)}${status}`);
}
lines.push("");
}
}
this.#showMessage(lines.join("\n"));
} catch (error) {
this.ctx.showError(`Failed to list servers: ${error instanceof Error ? error.message : String(error)}`);