diff --git a/src/core/prompts/tools/__tests__/filter-tools-for-mode.spec.ts b/src/core/prompts/tools/__tests__/filter-tools-for-mode.spec.ts index 0b776a2bad9..e0d7a132414 100644 --- a/src/core/prompts/tools/__tests__/filter-tools-for-mode.spec.ts +++ b/src/core/prompts/tools/__tests__/filter-tools-for-mode.spec.ts @@ -1,6 +1,8 @@ // npx vitest run core/prompts/tools/__tests__/filter-tools-for-mode.spec.ts import type OpenAI from "openai" +import type { McpHub } from "../../../../services/mcp/McpHub" +import type { McpServer } from "@roo-code/types" import { filterNativeToolsForMode } from "../filter-tools-for-mode" @@ -15,6 +17,12 @@ function makeTool(name: string): OpenAI.Chat.ChatCompletionTool { } as OpenAI.Chat.ChatCompletionTool } +function createMockMcpHub(servers: McpServer[]): McpHub { + return { + getServers: () => servers, + } as unknown as McpHub +} + describe("filterNativeToolsForMode - disabledTools", () => { const nativeTools: OpenAI.Chat.ChatCompletionTool[] = [ makeTool("execute_command"), @@ -89,3 +97,109 @@ describe("filterNativeToolsForMode - disabledTools", () => { expect(resultNames).not.toContain("edit") }) }) + +describe("filterNativeToolsForMode - access_mcp_resource with resource templates", () => { + const toolsWithMcpResource: OpenAI.Chat.ChatCompletionTool[] = [ + makeTool("read_file"), + makeTool("access_mcp_resource"), + ] + + it("includes access_mcp_resource when server has only resource templates", () => { + const mcpHub = createMockMcpHub([ + { + name: "template-server", + config: "{}", + status: "connected", + resources: [], + resourceTemplates: [ + { + uriTemplate: "test://resource/{id}", + name: "Test Resource", + }, + ], + }, + ]) + + const result = filterNativeToolsForMode( + toolsWithMcpResource, + "code", + undefined, + undefined, + undefined, + undefined, + mcpHub, + ) + + const resultNames = result.map((t) => (t as any).function.name) + expect(resultNames).toContain("access_mcp_resource") + }) + + it("includes access_mcp_resource when server has only static resources", () => { + const mcpHub = createMockMcpHub([ + { + name: "static-server", + config: "{}", + status: "connected", + resources: [ + { + uri: "test://static", + name: "Static Resource", + }, + ], + resourceTemplates: [], + }, + ]) + + const result = filterNativeToolsForMode( + toolsWithMcpResource, + "code", + undefined, + undefined, + undefined, + undefined, + mcpHub, + ) + + const resultNames = result.map((t) => (t as any).function.name) + expect(resultNames).toContain("access_mcp_resource") + }) + + it("excludes access_mcp_resource when no mcpHub is provided", () => { + const result = filterNativeToolsForMode( + toolsWithMcpResource, + "code", + undefined, + undefined, + undefined, + undefined, + ) + + const resultNames = result.map((t) => (t as any).function.name) + expect(resultNames).not.toContain("access_mcp_resource") + }) + + it("excludes access_mcp_resource when server has no resources or templates", () => { + const mcpHub = createMockMcpHub([ + { + name: "empty-server", + config: "{}", + status: "connected", + resources: [], + resourceTemplates: [], + }, + ]) + + const result = filterNativeToolsForMode( + toolsWithMcpResource, + "code", + undefined, + undefined, + undefined, + undefined, + mcpHub, + ) + + const resultNames = result.map((t) => (t as any).function.name) + expect(resultNames).not.toContain("access_mcp_resource") + }) +}) diff --git a/src/core/prompts/tools/filter-tools-for-mode.ts b/src/core/prompts/tools/filter-tools-for-mode.ts index fdd41e7e330..c06c822a30a 100644 --- a/src/core/prompts/tools/filter-tools-for-mode.ts +++ b/src/core/prompts/tools/filter-tools-for-mode.ts @@ -330,11 +330,15 @@ export function filterNativeToolsForMode( } /** - * Helper function to check if any MCP server has resources available + * Helper function to check if any MCP server has resources or resource templates available */ function hasAnyMcpResources(mcpHub: McpHub): boolean { const servers = mcpHub.getServers() - return servers.some((server) => server.resources && server.resources.length > 0) + return servers.some( + (server) => + (server.resources && server.resources.length > 0) || + (server.resourceTemplates && server.resourceTemplates.length > 0), + ) } /** diff --git a/src/core/prompts/tools/native-tools/__tests__/access_mcp_resource.spec.ts b/src/core/prompts/tools/native-tools/__tests__/access_mcp_resource.spec.ts new file mode 100644 index 00000000000..8f40106bfd4 --- /dev/null +++ b/src/core/prompts/tools/native-tools/__tests__/access_mcp_resource.spec.ts @@ -0,0 +1,181 @@ +// npx vitest run core/prompts/tools/native-tools/__tests__/access_mcp_resource.spec.ts + +import type OpenAI from "openai" + +import type { McpServer } from "@roo-code/types" + +import type { McpHub } from "../../../../../services/mcp/McpHub" +import { createAccessMcpResourceTool } from "../access_mcp_resource" + +// Helper type to access function tools +type FunctionTool = OpenAI.Chat.ChatCompletionTool & { type: "function" } + +// Helper to get the function property from a tool +const getFunction = (tool: OpenAI.Chat.ChatCompletionTool) => (tool as FunctionTool).function + +function createMockMcpHub(servers: McpServer[]): McpHub { + return { + getServers: () => servers, + } as unknown as McpHub +} + +describe("createAccessMcpResourceTool", () => { + it("returns base description when no mcpHub is provided", () => { + const tool = createAccessMcpResourceTool() + const desc = getFunction(tool).description! + expect(desc).toContain("Request to access a resource provided by a connected MCP server") + expect(desc).not.toContain("Available MCP Resources") + }) + + it("returns base description when mcpHub has no servers", () => { + const mcpHub = createMockMcpHub([]) + const tool = createAccessMcpResourceTool(mcpHub) + const desc = getFunction(tool).description! + expect(desc).not.toContain("Available MCP Resources") + }) + + it("returns base description when servers have no resources or templates", () => { + const mcpHub = createMockMcpHub([ + { + name: "test-server", + config: "{}", + status: "connected", + tools: [], + resources: [], + resourceTemplates: [], + }, + ]) + const tool = createAccessMcpResourceTool(mcpHub) + const desc = getFunction(tool).description! + expect(desc).not.toContain("Available MCP Resources") + }) + + it("includes static resources in description", () => { + const mcpHub = createMockMcpHub([ + { + name: "weather-server", + config: "{}", + status: "connected", + resources: [ + { + uri: "weather://current", + name: "Current Weather", + description: "Get current weather data", + }, + ], + resourceTemplates: [], + }, + ]) + const tool = createAccessMcpResourceTool(mcpHub) + const desc = getFunction(tool).description! + expect(desc).toContain("Available MCP Resources") + expect(desc).toContain('Server "weather-server" resources:') + expect(desc).toContain("weather://current") + expect(desc).toContain("Current Weather") + expect(desc).toContain("Get current weather data") + }) + + it("includes resource templates in description", () => { + const mcpHub = createMockMcpHub([ + { + name: "orbitcity", + config: "{}", + status: "connected", + resources: [], + resourceTemplates: [ + { + uriTemplate: "orbitcity://event/details/{identifier}", + name: "Event Details", + description: "Get event details by identifier", + }, + { + uriTemplate: "orbitcity://system/history/{machine_type}/{serial}", + name: "System History", + description: "Get system history", + }, + ], + }, + ]) + const tool = createAccessMcpResourceTool(mcpHub) + const desc = getFunction(tool).description! + expect(desc).toContain("Available MCP Resources") + expect(desc).toContain('Server "orbitcity" resources:') + expect(desc).toContain("orbitcity://event/details/{identifier}") + expect(desc).toContain("Event Details") + expect(desc).toContain("orbitcity://system/history/{machine_type}/{serial}") + expect(desc).toContain("System History") + }) + + it("includes both resources and resource templates from the same server", () => { + const mcpHub = createMockMcpHub([ + { + name: "data-server", + config: "{}", + status: "connected", + resources: [ + { + uri: "data://status", + name: "Server Status", + }, + ], + resourceTemplates: [ + { + uriTemplate: "data://records/{id}", + name: "Record by ID", + }, + ], + }, + ]) + const tool = createAccessMcpResourceTool(mcpHub) + const desc = getFunction(tool).description! + expect(desc).toContain("data://status") + expect(desc).toContain("data://records/{id}") + }) + + it("includes resources from multiple servers", () => { + const mcpHub = createMockMcpHub([ + { + name: "server-a", + config: "{}", + status: "connected", + resources: [{ uri: "a://resource", name: "Resource A" }], + }, + { + name: "server-b", + config: "{}", + status: "connected", + resourceTemplates: [{ uriTemplate: "b://template/{id}", name: "Template B" }], + }, + ]) + const tool = createAccessMcpResourceTool(mcpHub) + const desc = getFunction(tool).description! + expect(desc).toContain('Server "server-a" resources:') + expect(desc).toContain("a://resource") + expect(desc).toContain('Server "server-b" resources:') + expect(desc).toContain("b://template/{id}") + }) + + it("handles resources without descriptions", () => { + const mcpHub = createMockMcpHub([ + { + name: "minimal-server", + config: "{}", + status: "connected", + resources: [{ uri: "min://data", name: "Data" }], + }, + ]) + const tool = createAccessMcpResourceTool(mcpHub) + const desc = getFunction(tool).description! + expect(desc).toContain("min://data (Data)") + }) + + it("always has correct tool name and parameters", () => { + const tool = createAccessMcpResourceTool() + const fn = getFunction(tool) + expect(fn.name).toBe("access_mcp_resource") + expect(fn.parameters).toBeDefined() + const params = fn.parameters as Record + expect(params.required).toContain("server_name") + expect(params.required).toContain("uri") + }) +}) diff --git a/src/core/prompts/tools/native-tools/access_mcp_resource.ts b/src/core/prompts/tools/native-tools/access_mcp_resource.ts index 2876fcf5604..fc87ba7edd0 100644 --- a/src/core/prompts/tools/native-tools/access_mcp_resource.ts +++ b/src/core/prompts/tools/native-tools/access_mcp_resource.ts @@ -1,41 +1,102 @@ import type OpenAI from "openai" +import type { McpHub } from "../../../../services/mcp/McpHub" -const ACCESS_MCP_RESOURCE_DESCRIPTION = `Request to access a resource provided by a connected MCP server. Resources represent data sources that can be used as context, such as files, API responses, or system information. +const BASE_DESCRIPTION = `Request to access a resource provided by a connected MCP server. Resources represent data sources that can be used as context, such as files, API responses, or system information. Parameters: - server_name: (required) The name of the MCP server providing the resource -- uri: (required) The URI identifying the specific resource to access - -Example: Accessing a weather resource -{ "server_name": "weather-server", "uri": "weather://san-francisco/current" } - -Example: Accessing a file resource from an MCP server -{ "server_name": "filesystem-server", "uri": "file:///path/to/data.json" }` +- uri: (required) The URI identifying the specific resource to access` const SERVER_NAME_PARAMETER_DESCRIPTION = `The name of the MCP server providing the resource` const URI_PARAMETER_DESCRIPTION = `The URI identifying the specific resource to access` -export default { - type: "function", - function: { - name: "access_mcp_resource", - description: ACCESS_MCP_RESOURCE_DESCRIPTION, - strict: true, - parameters: { - type: "object", - properties: { - server_name: { - type: "string", - description: SERVER_NAME_PARAMETER_DESCRIPTION, - }, - uri: { - type: "string", - description: URI_PARAMETER_DESCRIPTION, +/** + * Builds a dynamic description for the access_mcp_resource tool that includes + * available resources and resource templates from connected MCP servers. + * This gives the model the information it needs to construct valid resource URIs. + */ +function buildDescription(mcpHub?: McpHub): string { + if (!mcpHub) { + return BASE_DESCRIPTION + } + + const servers = mcpHub.getServers() + const serverSections: string[] = [] + + for (const server of servers) { + const resourceLines: string[] = [] + + if (server.resources && server.resources.length > 0) { + for (const resource of server.resources) { + let line = ` - ${resource.uri}` + if (resource.name) { + line += ` (${resource.name})` + } + if (resource.description) { + line += `: ${resource.description}` + } + resourceLines.push(line) + } + } + + if (server.resourceTemplates && server.resourceTemplates.length > 0) { + for (const template of server.resourceTemplates) { + let line = ` - ${template.uriTemplate}` + if (template.name) { + line += ` (${template.name})` + } + if (template.description) { + line += `: ${template.description}` + } + resourceLines.push(line) + } + } + + if (resourceLines.length > 0) { + serverSections.push(`\nServer "${server.name}" resources:\n${resourceLines.join("\n")}`) + } + } + + if (serverSections.length === 0) { + return BASE_DESCRIPTION + } + + return `${BASE_DESCRIPTION} + +Available MCP Resources: +${serverSections.join("\n")}` +} + +/** + * Creates the access_mcp_resource tool definition with a dynamic description + * that includes available resources from connected MCP servers. + */ +export function createAccessMcpResourceTool(mcpHub?: McpHub): OpenAI.Chat.ChatCompletionTool { + return { + type: "function", + function: { + name: "access_mcp_resource", + description: buildDescription(mcpHub), + strict: true, + parameters: { + type: "object", + properties: { + server_name: { + type: "string", + description: SERVER_NAME_PARAMETER_DESCRIPTION, + }, + uri: { + type: "string", + description: URI_PARAMETER_DESCRIPTION, + }, }, + required: ["server_name", "uri"], + additionalProperties: false, }, - required: ["server_name", "uri"], - additionalProperties: false, }, - }, -} satisfies OpenAI.Chat.ChatCompletionTool + } +} + +// Default export for backward compatibility (static definition without resource info) +export default createAccessMcpResourceTool() diff --git a/src/core/prompts/tools/native-tools/index.ts b/src/core/prompts/tools/native-tools/index.ts index 758914d2d65..59cb5b1de18 100644 --- a/src/core/prompts/tools/native-tools/index.ts +++ b/src/core/prompts/tools/native-tools/index.ts @@ -1,5 +1,5 @@ import type OpenAI from "openai" -import accessMcpResource from "./access_mcp_resource" +import accessMcpResource, { createAccessMcpResourceTool } from "./access_mcp_resource" import { apply_diff } from "./apply_diff" import applyPatch from "./apply_patch" import askFollowupQuestion from "./ask_followup_question" @@ -25,12 +25,16 @@ export { getMcpServerTools } from "./mcp_server" export { convertOpenAIToolToAnthropic, convertOpenAIToolsToAnthropic } from "./converters" export type { ReadFileToolOptions } from "./read_file" +import type { McpHub } from "../../../../services/mcp/McpHub" + /** * Options for customizing the native tools array. */ export interface NativeToolsOptions { /** Whether the model supports image processing (default: false) */ supportsImages?: boolean + /** MCP hub for generating dynamic resource descriptions */ + mcpHub?: McpHub } /** @@ -40,14 +44,14 @@ export interface NativeToolsOptions { * @returns Array of native tool definitions */ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.ChatCompletionTool[] { - const { supportsImages = false } = options + const { supportsImages = false, mcpHub } = options const readFileOptions: ReadFileToolOptions = { supportsImages, } return [ - accessMcpResource, + mcpHub ? createAccessMcpResourceTool(mcpHub) : accessMcpResource, apply_diff, applyPatch, askFollowupQuestion, diff --git a/src/core/task/build-tools.ts b/src/core/task/build-tools.ts index c32d8f6f9b2..58336e2d4b0 100644 --- a/src/core/task/build-tools.ts +++ b/src/core/task/build-tools.ts @@ -108,9 +108,10 @@ export async function buildNativeToolsArrayWithRestrictions(options: BuildToolsO // Check if the model supports images for read_file tool description. const supportsImages = modelInfo?.supportsImages ?? false - // Build native tools with dynamic read_file tool based on settings. + // Build native tools with dynamic read_file tool and MCP resource descriptions. const nativeTools = getNativeTools({ supportsImages, + mcpHub: mcpHub ?? undefined, }) // Filter native tools based on mode restrictions.