fix: include resource templates in access_mcp_resource tool filtering and description

- Fix hasAnyMcpResources() to also check resourceTemplates, not just
  static resources. MCP servers that only expose resource templates
  (URI templates with parameters) were incorrectly excluded.
- Make access_mcp_resource tool description dynamic to include available
  resources and resource templates from connected MCP servers, giving
  the model the information it needs to construct valid resource URIs.
- Pass mcpHub through getNativeTools() and build-tools.ts so the
  dynamic description is generated at tool assembly time.
- Add comprehensive tests for both fixes.
This commit is contained in:
Roo Code 2026-05-04 17:37:21 +00:00
parent ad25634905
commit 12d7ea0f87
6 changed files with 399 additions and 34 deletions

View file

@ -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")
})
})

View file

@ -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),
)
}
/**

View file

@ -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<string, unknown>
expect(params.required).toContain("server_name")
expect(params.required).toContain("uri")
})
})

View file

@ -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()

View file

@ -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,

View file

@ -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.