Skip to content

Commit 774b492

Browse files
authored
fix: preserve dynamic MCP tool names in native mode API history (#9559)
1 parent 86cdbff commit 774b492

File tree

13 files changed

+483
-135
lines changed

13 files changed

+483
-135
lines changed

src/core/assistant-message/NativeToolCallParser.ts

Lines changed: 61 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { type ToolName, toolNames, type FileEntry } from "@roo-code/types"
2-
import { type ToolUse, type ToolParamName, toolParamNames, type NativeToolArgs } from "../../shared/tools"
2+
import {
3+
type ToolUse,
4+
type McpToolUse,
5+
type ToolParamName,
6+
toolParamNames,
7+
type NativeToolArgs,
8+
} from "../../shared/tools"
39
import { parseJSON } from "partial-json"
410
import type {
511
ApiStreamToolCallStartChunk,
@@ -41,11 +47,12 @@ export type ToolCallStreamEvent = ApiStreamToolCallStartChunk | ApiStreamToolCal
4147
*/
4248
export class NativeToolCallParser {
4349
// Streaming state management for argument accumulation (keyed by tool call id)
50+
// Note: name is string to accommodate dynamic MCP tools (mcp_serverName_toolName)
4451
private static streamingToolCalls = new Map<
4552
string,
4653
{
4754
id: string
48-
name: ToolName
55+
name: string
4956
argumentsAccumulator: string
5057
}
5158
>()
@@ -188,8 +195,9 @@ export class NativeToolCallParser {
188195
/**
189196
* Start streaming a new tool call.
190197
* Initializes tracking for incremental argument parsing.
198+
* Accepts string to support both ToolName and dynamic MCP tools (mcp_serverName_toolName).
191199
*/
192-
public static startStreamingToolCall(id: string, name: ToolName): void {
200+
public static startStreamingToolCall(id: string, name: string): void {
193201
this.streamingToolCalls.set(id, {
194202
id,
195203
name,
@@ -229,6 +237,11 @@ export class NativeToolCallParser {
229237
// Accumulate the JSON string
230238
toolCall.argumentsAccumulator += chunk
231239

240+
// For dynamic MCP tools, we don't return partial updates - wait for final
241+
if (toolCall.name.startsWith("mcp_")) {
242+
return null
243+
}
244+
232245
// Parse whatever we can from the incomplete JSON!
233246
// partial-json-parser extracts partial values (strings, arrays, objects) immediately
234247
try {
@@ -237,7 +250,7 @@ export class NativeToolCallParser {
237250
// Create partial ToolUse with extracted values
238251
return this.createPartialToolUse(
239252
toolCall.id,
240-
toolCall.name,
253+
toolCall.name as ToolName,
241254
partialArgs || {},
242255
true, // partial
243256
)
@@ -250,19 +263,20 @@ export class NativeToolCallParser {
250263

251264
/**
252265
* Finalize a streaming tool call.
253-
* Parses the complete JSON and returns the final ToolUse.
266+
* Parses the complete JSON and returns the final ToolUse or McpToolUse.
254267
*/
255-
public static finalizeStreamingToolCall(id: string): ToolUse | null {
268+
public static finalizeStreamingToolCall(id: string): ToolUse | McpToolUse | null {
256269
const toolCall = this.streamingToolCalls.get(id)
257270
if (!toolCall) {
258271
console.warn(`[NativeToolCallParser] Attempting to finalize unknown tool call: ${id}`)
259272
return null
260273
}
261274

262275
// Parse the complete accumulated JSON
276+
// Cast to any for the name since parseToolCall handles both ToolName and dynamic MCP tools
263277
const finalToolUse = this.parseToolCall({
264278
id: toolCall.id,
265-
name: toolCall.name,
279+
name: toolCall.name as ToolName,
266280
arguments: toolCall.argumentsAccumulator,
267281
})
268282

@@ -490,10 +504,10 @@ export class NativeToolCallParser {
490504
id: string
491505
name: TName
492506
arguments: string
493-
}): ToolUse<TName> | null {
507+
}): ToolUse<TName> | McpToolUse | null {
494508
// Check if this is a dynamic MCP tool (mcp_serverName_toolName)
495509
if (typeof toolCall.name === "string" && toolCall.name.startsWith("mcp_")) {
496-
return this.parseDynamicMcpTool(toolCall) as ToolUse<TName> | null
510+
return this.parseDynamicMcpTool(toolCall)
497511
}
498512

499513
// Validate tool name
@@ -697,6 +711,15 @@ export class NativeToolCallParser {
697711
}
698712
break
699713

714+
case "access_mcp_resource":
715+
if (args.server_name !== undefined && args.uri !== undefined) {
716+
nativeArgs = {
717+
server_name: args.server_name,
718+
uri: args.uri,
719+
} as NativeArgsFor<TName>
720+
}
721+
break
722+
700723
default:
701724
break
702725
}
@@ -719,51 +742,44 @@ export class NativeToolCallParser {
719742

720743
/**
721744
* Parse dynamic MCP tools (named mcp_serverName_toolName).
722-
* These are generated dynamically by getMcpServerTools() and need to be
723-
* converted back to use_mcp_tool format.
745+
* These are generated dynamically by getMcpServerTools() and are returned
746+
* as McpToolUse objects that preserve the original tool name.
747+
*
748+
* In native mode, MCP tools are NOT converted to use_mcp_tool - they keep
749+
* their original name so it appears correctly in API conversation history.
750+
* The use_mcp_tool wrapper is only used in XML mode.
724751
*/
725-
private static parseDynamicMcpTool(toolCall: {
726-
id: string
727-
name: string
728-
arguments: string
729-
}): ToolUse<"use_mcp_tool"> | null {
752+
public static parseDynamicMcpTool(toolCall: { id: string; name: string; arguments: string }): McpToolUse | null {
730753
try {
731-
const args = JSON.parse(toolCall.arguments)
732-
733-
// Extract server_name and tool_name from the arguments
734-
// The dynamic tool schema includes these as const properties
735-
const serverName = args.server_name
736-
const toolName = args.tool_name
737-
const toolInputProps = args.toolInputProps
738-
739-
if (!serverName || !toolName) {
740-
console.error(`Missing server_name or tool_name in dynamic MCP tool`)
754+
// Parse the arguments - these are the actual tool arguments passed directly
755+
const args = JSON.parse(toolCall.arguments || "{}")
756+
757+
// Extract server_name and tool_name from the tool name itself
758+
// Format: mcp_serverName_toolName
759+
const nameParts = toolCall.name.split("_")
760+
if (nameParts.length < 3 || nameParts[0] !== "mcp") {
761+
console.error(`Invalid dynamic MCP tool name format: ${toolCall.name}`)
741762
return null
742763
}
743764

744-
// Build params for backward compatibility with XML protocol
745-
const params: Partial<Record<string, string>> = {
746-
server_name: serverName,
747-
tool_name: toolName,
748-
}
749-
750-
if (toolInputProps) {
751-
params.arguments = JSON.stringify(toolInputProps)
752-
}
765+
// Server name is the second part, tool name is everything after
766+
const serverName = nameParts[1]
767+
const toolName = nameParts.slice(2).join("_")
753768

754-
// Build nativeArgs with properly typed structure
755-
const nativeArgs: NativeToolArgs["use_mcp_tool"] = {
756-
server_name: serverName,
757-
tool_name: toolName,
758-
arguments: toolInputProps,
769+
if (!serverName || !toolName) {
770+
console.error(`Could not extract server_name or tool_name from: ${toolCall.name}`)
771+
return null
759772
}
760773

761-
const result: ToolUse<"use_mcp_tool"> = {
762-
type: "tool_use" as const,
763-
name: "use_mcp_tool",
764-
params,
774+
const result: McpToolUse = {
775+
type: "mcp_tool_use" as const,
776+
id: toolCall.id,
777+
// Keep the original tool name (e.g., "mcp_serverName_toolName") for API history
778+
name: toolCall.name,
779+
serverName,
780+
toolName,
781+
arguments: args,
765782
partial: false,
766-
nativeArgs,
767783
}
768784

769785
return result

src/core/assistant-message/parseAssistantMessage.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { type ToolName, toolNames } from "@roo-code/types"
22

3-
import { TextContent, ToolUse, ToolParamName, toolParamNames } from "../../shared/tools"
3+
import { TextContent, ToolUse, McpToolUse, ToolParamName, toolParamNames } from "../../shared/tools"
44

5-
export type AssistantMessageContent = TextContent | ToolUse
5+
export type AssistantMessageContent = TextContent | ToolUse | McpToolUse
66

77
export function parseAssistantMessage(assistantMessage: string): AssistantMessageContent[] {
88
let contentBlocks: AssistantMessageContent[] = []

0 commit comments

Comments
 (0)