diff --git a/src/core/prompts/sections/__tests__/mcp-servers.spec.ts b/src/core/prompts/sections/__tests__/mcp-servers.spec.ts new file mode 100644 index 0000000000..de9b7b8dac --- /dev/null +++ b/src/core/prompts/sections/__tests__/mcp-servers.spec.ts @@ -0,0 +1,156 @@ +import type { McpServer, McpTool } from "@roo-code/types" + +import type { McpHub } from "../../../../services/mcp/McpHub" + +import { getMcpServersSection } from "../mcp-servers" + +describe("getMcpServersSection", () => { + const createMockTool = (name: string, description = "Test tool", enabledForPrompt?: boolean): McpTool => ({ + name, + description, + inputSchema: { + type: "object", + properties: {}, + }, + ...(enabledForPrompt !== undefined ? { enabledForPrompt } : {}), + }) + + const createMockServer = ( + name: string, + tools: McpTool[], + options: { instructions?: string; source?: "global" | "project" } = {}, + ): McpServer => ({ + name, + config: JSON.stringify({ type: "stdio", command: "test" }), + status: "connected", + source: options.source ?? "global", + tools, + instructions: options.instructions, + }) + + const createMockMcpHub = (servers: McpServer[]): Partial => ({ + getServers: vi.fn().mockReturnValue(servers), + }) + + it("should return empty string when mcpHub is undefined", () => { + const result = getMcpServersSection(undefined) + expect(result).toBe("") + }) + + it("should return empty string when no servers are available", () => { + const mockHub = createMockMcpHub([]) + const result = getMcpServersSection(mockHub as McpHub) + expect(result).toBe("") + }) + + it("should return empty string when servers have no enabled tools", () => { + const server = createMockServer("testServer", []) + const mockHub = createMockMcpHub([server]) + const result = getMcpServersSection(mockHub as McpHub) + expect(result).toBe("") + }) + + it("should return empty string when all tools are disabled", () => { + const server = createMockServer("testServer", [ + createMockTool("tool1", "Tool 1", false), + createMockTool("tool2", "Tool 2", false), + ]) + const mockHub = createMockMcpHub([server]) + const result = getMcpServersSection(mockHub as McpHub) + expect(result).toBe("") + }) + + it("should include MCP SERVERS header", () => { + const server = createMockServer("testServer", [createMockTool("testTool")]) + const mockHub = createMockMcpHub([server]) + const result = getMcpServersSection(mockHub as McpHub) + expect(result).toContain("MCP SERVERS") + }) + + it("should explain the naming convention", () => { + const server = createMockServer("testServer", [createMockTool("testTool")]) + const mockHub = createMockMcpHub([server]) + const result = getMcpServersSection(mockHub as McpHub) + expect(result).toContain("mcp--serverName--toolName") + }) + + it("should list server name as heading", () => { + const server = createMockServer("context7", [createMockTool("resolve-library-id")]) + const mockHub = createMockMcpHub([server]) + const result = getMcpServersSection(mockHub as McpHub) + expect(result).toContain("## context7") + }) + + it("should list tool names using the mcp-- naming convention", () => { + const server = createMockServer("context7", [ + createMockTool("resolve-library-id"), + createMockTool("get-library-docs"), + ]) + const mockHub = createMockMcpHub([server]) + const result = getMcpServersSection(mockHub as McpHub) + expect(result).toContain("mcp--context7--resolve-library-id") + expect(result).toContain("mcp--context7--get-library-docs") + }) + + it("should include server instructions when provided", () => { + const server = createMockServer("conport", [createMockTool("init-db")], { + instructions: "Always initialize the database before performing queries.", + }) + const mockHub = createMockMcpHub([server]) + const result = getMcpServersSection(mockHub as McpHub) + expect(result).toContain("Server Instructions:") + expect(result).toContain("Always initialize the database before performing queries.") + }) + + it("should not include server instructions section when not provided", () => { + const server = createMockServer("testServer", [createMockTool("testTool")]) + const mockHub = createMockMcpHub([server]) + const result = getMcpServersSection(mockHub as McpHub) + expect(result).not.toContain("Server Instructions:") + }) + + it("should list multiple servers", () => { + const server1 = createMockServer("context7", [createMockTool("resolve-library-id")]) + const server2 = createMockServer("git", [createMockTool("git-status")]) + const mockHub = createMockMcpHub([server1, server2]) + const result = getMcpServersSection(mockHub as McpHub) + expect(result).toContain("## context7") + expect(result).toContain("## git") + expect(result).toContain("mcp--context7--resolve-library-id") + expect(result).toContain("mcp--git--git-status") + }) + + it("should filter out disabled tools", () => { + const server = createMockServer("testServer", [ + createMockTool("enabledTool", "Enabled tool"), + createMockTool("disabledTool", "Disabled tool", false), + ]) + const mockHub = createMockMcpHub([server]) + const result = getMcpServersSection(mockHub as McpHub) + expect(result).toContain("mcp--testServer--enabledTool") + expect(result).not.toContain("mcp--testServer--disabledTool") + }) + + it("should skip servers with only disabled tools", () => { + const serverWithDisabledTools = createMockServer("disabledServer", [createMockTool("tool1", "Tool 1", false)]) + const serverWithEnabledTools = createMockServer("enabledServer", [createMockTool("tool1", "Tool 1")]) + const mockHub = createMockMcpHub([serverWithDisabledTools, serverWithEnabledTools]) + const result = getMcpServersSection(mockHub as McpHub) + expect(result).not.toContain("## disabledServer") + expect(result).toContain("## enabledServer") + }) + + it("should skip servers with undefined tools", () => { + const serverWithUndefinedTools: McpServer = { + name: "noTools", + config: JSON.stringify({ type: "stdio", command: "test" }), + status: "connected", + tools: undefined, + } + const serverWithTools = createMockServer("withTools", [createMockTool("tool1")]) + const mockHub = createMockMcpHub([serverWithUndefinedTools, serverWithTools]) + const result = getMcpServersSection(mockHub as McpHub) + expect(result).not.toContain("## noTools") + expect(result).toContain("## withTools") + }) +}) diff --git a/src/core/prompts/sections/index.ts b/src/core/prompts/sections/index.ts index 318cd47bc9..d5400cd1ae 100644 --- a/src/core/prompts/sections/index.ts +++ b/src/core/prompts/sections/index.ts @@ -8,3 +8,4 @@ export { getCapabilitiesSection } from "./capabilities" export { getModesSection } from "./modes" export { markdownFormattingSection } from "./markdown-formatting" export { getSkillsSection } from "./skills" +export { getMcpServersSection } from "./mcp-servers" diff --git a/src/core/prompts/sections/mcp-servers.ts b/src/core/prompts/sections/mcp-servers.ts new file mode 100644 index 0000000000..0306b35547 --- /dev/null +++ b/src/core/prompts/sections/mcp-servers.ts @@ -0,0 +1,64 @@ +import type { McpTool } from "@roo-code/types" + +import { McpHub } from "../../../services/mcp/McpHub" +import { buildMcpToolName } from "../../../utils/mcp-name" + +/** + * Generates a lightweight MCP servers section for the system prompt. + * + * This provides the model with context about connected MCP servers, including: + * - The naming convention for MCP tool calls (mcp--serverName--toolName) + * - A list of connected servers and their available tools + * - Server-specific instructions (from the MCP protocol's `instructions` field) + * + * This does NOT duplicate tool schemas or descriptions, since those are already + * provided via native tool definitions. The purpose is to give the model enough + * context to understand how to use these tools, which is especially important + * for models like OpenAI's GPT series that benefit from explicit system prompt + * guidance about available MCP tools. + */ +export function getMcpServersSection(mcpHub?: McpHub): string { + if (!mcpHub) { + return "" + } + + const servers = mcpHub.getServers() + + if (servers.length === 0) { + return "" + } + + // Build per-server entries with tool names and optional instructions + const serverEntries: string[] = [] + + for (const server of servers) { + const enabledTools = (server.tools || []).filter((tool: McpTool) => tool.enabledForPrompt !== false) + + if (enabledTools.length === 0) { + continue + } + + const toolNames = enabledTools.map((tool: McpTool) => ` - ${buildMcpToolName(server.name, tool.name)}`) + + let entry = `## ${server.name}\n` + entry += `Tools:\n${toolNames.join("\n")}` + + if (server.instructions) { + entry += `\n\nServer Instructions:\n${server.instructions}` + } + + serverEntries.push(entry) + } + + if (serverEntries.length === 0) { + return "" + } + + return `==== + +MCP SERVERS + +The following MCP (Model Context Protocol) servers are connected and provide additional tools you can use. MCP tools are called as native tool calls using the naming convention \`mcp--serverName--toolName\`. When a task could benefit from an MCP tool, prefer using it. + +${serverEntries.join("\n\n")}` +} diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index 1731952a4e..40a552c6ec 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -26,6 +26,7 @@ import { addCustomInstructions, markdownFormattingSection, getSkillsSection, + getMcpServersSection, } from "./sections" // Helper function to get prompt component, filtering out empty objects @@ -86,6 +87,8 @@ async function generatePrompt( // Tools catalog is not included in the system prompt. const toolsCatalog = "" + const mcpServersSection = shouldIncludeMcp ? getMcpServersSection(mcpHub) : "" + const basePrompt = `${roleDefinition} ${markdownFormattingSection()} @@ -95,7 +98,7 @@ ${getSharedToolUseSection()}${toolsCatalog} ${getToolUseGuidelinesSection()} ${getCapabilitiesSection(cwd, shouldIncludeMcp ? mcpHub : undefined)} - +${mcpServersSection ? `\n${mcpServersSection}` : ""} ${modesSection} ${skillsSection ? `\n${skillsSection}` : ""} ${getRulesSection(cwd, settings)}