diff --git a/src/__tests__/command-mentions.spec.ts b/src/__tests__/command-mentions.spec.ts
index 7ddaf3d092..4026d6ebb2 100644
--- a/src/__tests__/command-mentions.spec.ts
+++ b/src/__tests__/command-mentions.spec.ts
@@ -53,7 +53,7 @@ describe("Command Mentions", () => {
const input = "/setup Please help me set up the project"
const result = await callParseMentions(input)
- expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "setup")
+ expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "setup", undefined)
expect(result).toContain('')
expect(result).toContain(commandContent)
expect(result).toContain("")
@@ -94,8 +94,8 @@ describe("Command Mentions", () => {
const input = "/setup the project\nThen /deploy later"
const result = await callParseMentions(input)
- expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "setup")
- expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "deploy")
+ expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "setup", undefined)
+ expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "deploy", undefined)
expect(mockGetCommand).toHaveBeenCalledTimes(2) // Each unique command called once (optimized)
expect(result).toContain('')
expect(result).toContain("# Setup Environment")
@@ -110,7 +110,7 @@ describe("Command Mentions", () => {
const input = "/nonexistent command"
const result = await callParseMentions(input)
- expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "nonexistent")
+ expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "nonexistent", undefined)
// The command should remain unchanged in the text
expect(result).toBe("/nonexistent command")
// Should not contain any command tags
@@ -159,7 +159,7 @@ describe("Command Mentions", () => {
const input = "/setup-dev for the project"
const result = await callParseMentions(input)
- expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "setup-dev")
+ expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "setup-dev", undefined)
expect(result).toContain('')
expect(result).toContain("# Dev setup")
})
diff --git a/src/services/command/__tests__/mcp-prompts.spec.ts b/src/services/command/__tests__/mcp-prompts.spec.ts
new file mode 100644
index 0000000000..9e7d230c59
--- /dev/null
+++ b/src/services/command/__tests__/mcp-prompts.spec.ts
@@ -0,0 +1,392 @@
+import { describe, it, expect, vi, beforeEach } from "vitest"
+import { getMcpPromptsAsCommands, executeMcpPrompt, parsePromptArguments } from "../mcp-prompts"
+import { McpHub } from "../../mcp/McpHub"
+import type { McpServer, McpPrompt } from "../../../shared/mcp"
+
+// Mock McpHub
+vi.mock("../../mcp/McpHub")
+
+describe("MCP Prompts", () => {
+ let mockMcpHub: any
+
+ beforeEach(() => {
+ vi.clearAllMocks()
+ mockMcpHub = {
+ getAllServers: vi.fn(),
+ getPrompt: vi.fn(),
+ }
+ })
+
+ describe("getMcpPromptsAsCommands", () => {
+ it("should return empty array when mcpHub is undefined", async () => {
+ const result = await getMcpPromptsAsCommands(undefined)
+ expect(result).toEqual([])
+ })
+
+ it("should return empty array when no servers have prompts", async () => {
+ const servers: McpServer[] = [
+ {
+ name: "test-server",
+ config: "test-config",
+ status: "connected",
+ tools: [],
+ resources: [],
+ prompts: [],
+ },
+ ]
+ mockMcpHub.getAllServers.mockReturnValue(servers)
+
+ const result = await getMcpPromptsAsCommands(mockMcpHub)
+ expect(result).toEqual([])
+ })
+
+ it("should convert MCP prompts to commands", async () => {
+ const prompts: McpPrompt[] = [
+ {
+ name: "generate-code",
+ description: "Generate code based on requirements",
+ arguments: [
+ {
+ name: "language",
+ description: "Programming language",
+ required: true,
+ },
+ ],
+ },
+ {
+ name: "explain",
+ description: "Explain a concept",
+ arguments: [],
+ },
+ ]
+
+ const servers: McpServer[] = [
+ {
+ name: "coding-assistant",
+ config: "test-config",
+ status: "connected",
+ tools: [],
+ resources: [],
+ prompts,
+ source: "global",
+ },
+ ]
+ mockMcpHub.getAllServers.mockReturnValue(servers)
+
+ const result = await getMcpPromptsAsCommands(mockMcpHub)
+
+ expect(result).toHaveLength(2)
+ expect(result[0]).toEqual({
+ name: "mcp.coding-assistant.generate-code",
+ content: "",
+ source: "global",
+ filePath: "",
+ description: "Generate code based on requirements",
+ argumentHint: "",
+ })
+ expect(result[1]).toEqual({
+ name: "mcp.coding-assistant.explain",
+ content: "",
+ source: "global",
+ filePath: "",
+ description: "Explain a concept",
+ argumentHint: undefined,
+ })
+ })
+
+ it("should handle multiple servers with prompts", async () => {
+ const server1: McpServer = {
+ name: "server1",
+ config: "test-config",
+ status: "connected",
+ tools: [],
+ resources: [],
+ prompts: [
+ {
+ name: "prompt1",
+ description: "First prompt",
+ arguments: [],
+ },
+ ],
+ source: "project",
+ }
+
+ const server2: McpServer = {
+ name: "server2",
+ config: "test-config",
+ status: "connected",
+ tools: [],
+ resources: [],
+ prompts: [
+ {
+ name: "prompt2",
+ description: "Second prompt",
+ arguments: [
+ {
+ name: "param",
+ description: "A parameter",
+ required: false,
+ },
+ ],
+ },
+ ],
+ source: "global",
+ }
+
+ mockMcpHub.getAllServers.mockReturnValue([server1, server2])
+
+ const result = await getMcpPromptsAsCommands(mockMcpHub)
+
+ expect(result).toHaveLength(2)
+ expect(result[0].name).toBe("mcp.server1.prompt1")
+ expect(result[1].name).toBe("mcp.server2.prompt2")
+ expect(result[1].argumentHint).toBe("[param]")
+ })
+
+ it("should handle prompts with multiple arguments", async () => {
+ const prompts: McpPrompt[] = [
+ {
+ name: "complex-prompt",
+ description: "A complex prompt",
+ arguments: [
+ {
+ name: "arg1",
+ description: "First argument",
+ required: true,
+ },
+ {
+ name: "arg2",
+ description: "Second argument",
+ required: false,
+ },
+ {
+ name: "arg3",
+ description: "Third argument",
+ required: true,
+ },
+ ],
+ },
+ ]
+
+ const servers: McpServer[] = [
+ {
+ name: "test",
+ config: "test-config",
+ status: "connected",
+ tools: [],
+ resources: [],
+ prompts,
+ source: "global",
+ },
+ ]
+ mockMcpHub.getAllServers.mockReturnValue(servers)
+
+ const result = await getMcpPromptsAsCommands(mockMcpHub)
+
+ expect(result[0].argumentHint).toBe(" [arg2]")
+ })
+ })
+
+ describe("executeMcpPrompt", () => {
+ it("should execute prompt without arguments", async () => {
+ const mockResponse = {
+ messages: [
+ {
+ content: {
+ type: "text" as const,
+ text: "Generated content",
+ },
+ },
+ ],
+ }
+ mockMcpHub.getPrompt.mockResolvedValue(mockResponse)
+
+ const result = await executeMcpPrompt(mockMcpHub, "test-server", "test-prompt", {})
+
+ expect(mockMcpHub.getPrompt).toHaveBeenCalledWith("test-server", "test-prompt", {})
+ expect(result).toBe("Generated content")
+ })
+
+ it("should execute prompt with arguments", async () => {
+ const mockResponse = {
+ messages: [
+ {
+ content: {
+ type: "text" as const,
+ text: "Result with args",
+ },
+ },
+ ],
+ }
+ mockMcpHub.getPrompt.mockResolvedValue(mockResponse)
+
+ const result = await executeMcpPrompt(mockMcpHub, "server", "prompt", {
+ language: "python",
+ description: "A test function",
+ })
+
+ expect(mockMcpHub.getPrompt).toHaveBeenCalledWith("server", "prompt", {
+ language: "python",
+ description: "A test function",
+ })
+ expect(result).toBe("Result with args")
+ })
+
+ it("should handle multiple messages in response", async () => {
+ const mockResponse = {
+ messages: [
+ {
+ content: {
+ type: "text" as const,
+ text: "First message",
+ },
+ },
+ {
+ content: {
+ type: "text" as const,
+ text: "Second message",
+ },
+ },
+ ],
+ }
+ mockMcpHub.getPrompt.mockResolvedValue(mockResponse)
+
+ const result = await executeMcpPrompt(mockMcpHub, "server", "prompt", {})
+
+ expect(result).toBe("First message\n\nSecond message")
+ })
+
+ it("should handle resource content types", async () => {
+ const mockResponse = {
+ messages: [
+ {
+ content: {
+ type: "resource" as const,
+ resource: {
+ text: "Resource content",
+ },
+ },
+ },
+ ],
+ }
+ mockMcpHub.getPrompt.mockResolvedValue(mockResponse)
+
+ const result = await executeMcpPrompt(mockMcpHub, "server", "prompt", {})
+
+ expect(result).toBe("Resource content")
+ })
+
+ it("should handle errors gracefully", async () => {
+ mockMcpHub.getPrompt.mockRejectedValue(new Error("Connection failed"))
+
+ await expect(executeMcpPrompt(mockMcpHub, "server", "prompt", {})).rejects.toThrow(
+ "Failed to execute MCP prompt: Connection failed",
+ )
+ })
+
+ it("should handle empty response", async () => {
+ const mockResponse = {
+ messages: [],
+ }
+ mockMcpHub.getPrompt.mockResolvedValue(mockResponse)
+
+ const result = await executeMcpPrompt(mockMcpHub, "server", "prompt", {})
+
+ expect(result).toBe("No messages returned from MCP prompt")
+ })
+
+ it("should handle messages without text content", async () => {
+ const mockResponse = {
+ messages: [
+ {
+ content: {
+ type: "text" as const,
+ text: "",
+ },
+ },
+ {
+ content: {
+ type: "resource" as const,
+ resource: {},
+ },
+ },
+ ],
+ }
+ mockMcpHub.getPrompt.mockResolvedValue(mockResponse)
+
+ const result = await executeMcpPrompt(mockMcpHub, "server", "prompt", {})
+
+ expect(result).toBe("No content returned from MCP prompt")
+ })
+ })
+
+ describe("parsePromptArguments", () => {
+ it("should return empty object for prompts without arguments", () => {
+ const prompt: McpPrompt = {
+ name: "test",
+ description: "Test prompt",
+ arguments: [],
+ }
+
+ const result = parsePromptArguments(prompt, "some args")
+ expect(result).toEqual({})
+ })
+
+ it("should parse positional arguments", () => {
+ const prompt: McpPrompt = {
+ name: "test",
+ description: "Test prompt",
+ arguments: [
+ { name: "language", description: "Language", required: true },
+ { name: "framework", description: "Framework", required: false },
+ ],
+ }
+
+ const result = parsePromptArguments(prompt, "python django")
+ expect(result).toEqual({
+ language: "python",
+ framework: "django",
+ })
+ })
+
+ it("should handle missing optional arguments", () => {
+ const prompt: McpPrompt = {
+ name: "test",
+ description: "Test prompt",
+ arguments: [
+ { name: "required", description: "Required arg", required: true },
+ { name: "optional", description: "Optional arg", required: false },
+ ],
+ }
+
+ const result = parsePromptArguments(prompt, "value1")
+ expect(result).toEqual({
+ required: "value1",
+ })
+ })
+
+ it("should handle extra arguments", () => {
+ const prompt: McpPrompt = {
+ name: "test",
+ description: "Test prompt",
+ arguments: [{ name: "arg1", description: "Arg 1", required: true }],
+ }
+
+ const result = parsePromptArguments(prompt, "value1 value2 value3")
+ expect(result).toEqual({
+ arg1: "value1",
+ })
+ })
+
+ it("should handle empty argument string", () => {
+ const prompt: McpPrompt = {
+ name: "test",
+ description: "Test prompt",
+ arguments: [{ name: "arg1", description: "Arg 1", required: false }],
+ }
+
+ const result = parsePromptArguments(prompt, "")
+ expect(result).toEqual({})
+ })
+ })
+})
diff --git a/src/services/command/mcp-prompts.ts b/src/services/command/mcp-prompts.ts
index 1b03235528..45a93cef62 100644
--- a/src/services/command/mcp-prompts.ts
+++ b/src/services/command/mcp-prompts.ts
@@ -146,8 +146,13 @@ export function parsePromptArguments(prompt: McpPrompt, argsString: string): Rec
return {}
}
+ const trimmedArgs = argsString.trim()
+ if (trimmedArgs.length === 0) {
+ return {}
+ }
+
const args: Record = {}
- const parts = argsString.trim().split(/\s+/)
+ const parts = trimmedArgs.split(/\s+/)
// Simple positional argument parsing
// In a more sophisticated implementation, we could support named arguments
diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts
index aaddc520cb..951fa8581c 100644
--- a/src/shared/ExtensionMessage.ts
+++ b/src/shared/ExtensionMessage.ts
@@ -24,7 +24,7 @@ import { ModelRecord, RouterModels } from "./api"
// Command interface for frontend/backend communication
export interface Command {
name: string
- source: "global" | "project" | "built-in"
+ source: "global" | "project" | "built-in" | "mcp"
filePath?: string
description?: string
argumentHint?: string