mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Add MCP status slash command
This commit is contained in:
parent
1b816bda74
commit
4ebbc3bda3
7 changed files with 140 additions and 1 deletions
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
46
src/panel/mcp-status.ts
Normal 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"}`;
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
45
tests/panel/mcp-status.test.ts
Normal file
45
tests/panel/mcp-status.test.ts
Normal 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"]);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue