mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
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:
parent
ad25634905
commit
12d7ea0f87
6 changed files with 399 additions and 34 deletions
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue