Add MCP status slash command

This commit is contained in:
murashit 2026-05-18 21:43:05 +09:00
parent 1b816bda74
commit 4ebbc3bda3
7 changed files with 140 additions and 1 deletions

View file

@ -49,7 +49,7 @@ Codex Panel supports the app-server-backed Codex workflows that fit a persistent
- Respond to command, file, and permission approval requests.
- Stream assistant messages, reasoning, commands, tool calls, hooks, file changes, and agent activity.
- Inspect file changes and roll back the latest turn without reverting local files.
- Inspect context usage, usage limits, connection diagnostics, and effective config (`/status`, `/doctor`).
- Inspect context usage, usage limits, connection diagnostics, MCP server inventory, and effective config (`/status`, `/doctor`, `/mcp`).
- Inspect and manage discovered Codex hooks from Codex Panel settings.
- Rewrite the current Markdown editor selection from an inline popover.

View file

@ -9,6 +9,7 @@ export const SLASH_COMMANDS = [
{ command: "/plan", detail: "Toggle Plan mode, optionally sending a message." },
{ command: "/status", detail: "Show current session, context, and usage limits." },
{ command: "/doctor", detail: "Show Codex CLI and app-server connection diagnostics." },
{ command: "/mcp", detail: "Show MCP servers recognized by Codex app-server." },
{ command: "/model", detail: "Show or set the model for subsequent turns." },
{ command: "/effort", detail: "Show or set reasoning effort for subsequent turns." },
{ command: "/help", detail: "Show available Codex slash commands." },

46
src/panel/mcp-status.ts Normal file
View file

@ -0,0 +1,46 @@
import type { McpServerStatus } from "../generated/app-server/v2/McpServerStatus";
import type { McpServerDiagnostic } from "../app-server/compatibility";
export function mcpStatusLines(servers: McpServerStatus[], diagnostics: McpServerDiagnostic[] = []): string[] {
if (servers.length === 0 && diagnostics.length === 0) {
return ["MCP servers", "Codex app-server reports no MCP servers."];
}
const statusByName = new Map(servers.map((server) => [server.name, server]));
const diagnosticByName = new Map(diagnostics.map((diagnostic) => [diagnostic.name, diagnostic]));
const names = new Set([...statusByName.keys(), ...diagnosticByName.keys()]);
const rows = [...names]
.sort((a, b) => a.localeCompare(b))
.map((name) => {
const server = statusByName.get(name);
const diagnostic = diagnosticByName.get(name);
return server ? mcpServerStatusLine(server, diagnostic) : mcpDiagnosticOnlyLine(name, diagnostic);
});
return ["MCP servers", ...rows];
}
function mcpServerStatusLine(server: McpServerStatus, diagnostic: McpServerDiagnostic | undefined): string {
const startup = diagnostic?.startupStatus && diagnostic.startupStatus !== "unknown" ? diagnostic.startupStatus : "available";
const tools = Object.keys(server.tools ?? {}).length;
const resources = server.resources.length;
const templates = server.resourceTemplates.length;
const parts = [startup, `auth ${server.authStatus}`, countLabel(tools, "tool"), countLabel(resources, "resource")];
if (templates > 0) parts.push(countLabel(templates, "resource template"));
if (diagnostic?.message) parts.push(diagnostic.message);
return `${server.name}: ${parts.join(", ")}`;
}
function mcpDiagnosticOnlyLine(name: string, diagnostic: McpServerDiagnostic | undefined): string {
const startup = diagnostic?.startupStatus ?? "unknown";
const auth = diagnostic?.authStatus ? `auth ${diagnostic.authStatus}` : "auth unknown";
const tools =
diagnostic?.toolCount === null || diagnostic?.toolCount === undefined ? "tools unknown" : countLabel(diagnostic.toolCount, "tool");
const parts = [startup, auth, tools];
if (diagnostic?.message) parts.push(diagnostic.message);
return `${name}: ${parts.join(", ")}`;
}
function countLabel(count: number, singular: string): string {
return `${count} ${singular}${count === 1 ? "" : "s"}`;
}

View file

@ -29,6 +29,7 @@ export interface SlashCommandExecutionContext {
setRequestedReasoningEffort: (effort: ReasoningEffort | null) => void;
statusSummaryLines: () => string[];
connectionDiagnosticLines: () => string[];
mcpStatusLines: () => Promise<string[]>;
modelStatusLines: () => string[];
effortStatusLines: () => string[];
}
@ -151,6 +152,15 @@ export async function executeSlashCommand(
return;
}
if (command === "mcp") {
if (args) {
context.addSystemMessage(`Unsupported slash command arguments: ${args}`);
return;
}
context.addSystemMessage((await context.mcpStatusLines()).join("\n"));
return;
}
if (command === "model") {
const requested = parseModelOverride(args);
if (requested !== undefined) {

View file

@ -41,6 +41,7 @@ import { sortedAvailableModels } from "../runtime/model";
import { compactContextLabel, modelOverrideMessage, reasoningEffortOverrideMessage } from "../runtime/settings";
import { executeSlashCommand as runSlashCommand, type SlashCommandExecutionResult } from "./slash-commands";
import type { ThreadReferenceInput } from "./slash-commands";
import { mcpStatusLines } from "./mcp-status";
import { PanelSessionController } from "./session-controller";
import { statusValue, usageLimitStatusLines } from "./status-lines";
import { ThreadHistoryLoader } from "./thread-history";
@ -529,6 +530,7 @@ export class CodexPanelView extends ItemView {
setRequestedReasoningEffort: (effort) => this.setRequestedReasoningEffort(effort),
statusSummaryLines: () => this.statusSummaryLines(),
connectionDiagnosticLines: () => this.connectionDiagnosticLines(),
mcpStatusLines: () => this.mcpStatusLines(),
modelStatusLines: () => this.modelStatusLines(),
effortStatusLines: () => this.effortStatusLines(),
});
@ -942,6 +944,18 @@ export class CodexPanelView extends ItemView {
return connectionDiagnosticLines(this.connectionDiagnosticRows());
}
private async mcpStatusLines(): Promise<string[]> {
if (!this.client) return ["MCP servers", "Codex app-server is not connected."];
try {
const response = await this.client.listMcpServerStatus();
return mcpStatusLines(response.data, this.state.appServerDiagnostics.mcpServers);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return ["MCP servers", `Could not load MCP servers: ${message}`];
}
}
private collaborationModeLabel(): string {
return formatCollaborationModeLabel(this.state.requestedCollaborationMode);
}

View file

@ -26,6 +26,7 @@ function context(overrides: Partial<SlashCommandExecutionContext> = {}): SlashCo
setRequestedReasoningEffort: vi.fn(),
statusSummaryLines: () => ["status"],
connectionDiagnosticLines: () => ["doctor"],
mcpStatusLines: vi.fn().mockResolvedValue(["mcp"]),
modelStatusLines: () => ["model"],
effortStatusLines: () => ["effort"],
...overrides,
@ -247,4 +248,26 @@ describe("slash commands", () => {
"/doctor - Show Codex CLI and app-server connection diagnostics.",
);
});
it("shows MCP server status", async () => {
const ctx = context();
await executeSlashCommand("mcp", "", ctx);
expect(ctx.mcpStatusLines).toHaveBeenCalledOnce();
expect(ctx.addSystemMessage).toHaveBeenCalledWith("mcp");
});
it("rejects /mcp arguments", async () => {
const ctx = context();
await executeSlashCommand("mcp", "enable github", ctx);
expect(ctx.mcpStatusLines).not.toHaveBeenCalled();
expect(ctx.addSystemMessage).toHaveBeenCalledWith("Unsupported slash command arguments: enable github");
});
it("documents MCP status", () => {
expect(slashCommandHelpLines().find((line) => line.startsWith("/mcp"))).toBe("/mcp - Show MCP servers recognized by Codex app-server.");
});
});

View file

@ -0,0 +1,45 @@
import { describe, expect, it } from "vitest";
import type { McpServerStatus } from "../../src/generated/app-server/v2/McpServerStatus";
import { mcpStatusLines } from "../../src/panel/mcp-status";
function mcpServer(overrides: Partial<McpServerStatus> = {}): McpServerStatus {
return {
name: "github",
tools: {
search_issues: { name: "search_issues", description: null, inputSchema: {} },
fetch_pr: { name: "fetch_pr", description: null, inputSchema: {} },
},
resources: [],
resourceTemplates: [],
authStatus: "oAuth",
...overrides,
} as McpServerStatus;
}
describe("mcpStatusLines", () => {
it("reports no configured servers clearly", () => {
expect(mcpStatusLines([])).toEqual(["MCP servers", "Codex app-server reports no MCP servers."]);
});
it("formats recognized MCP servers", () => {
expect(mcpStatusLines([mcpServer()])).toEqual(["MCP servers", "github: available, auth oAuth, 2 tools, 0 resources"]);
});
it("includes diagnostic-only startup failures", () => {
expect(
mcpStatusLines(
[],
[
{
name: "figma",
startupStatus: "failed",
authStatus: null,
toolCount: null,
message: "command not found",
},
],
),
).toEqual(["MCP servers", "figma: failed, auth unknown, tools unknown, command not found"]);
});
});