Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 61 additions & 45 deletions src/core/assistant-message/NativeToolCallParser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { type ToolName, toolNames, type FileEntry } from "@roo-code/types"
import { type ToolUse, type ToolParamName, toolParamNames, type NativeToolArgs } from "../../shared/tools"
import {
type ToolUse,
type McpToolUse,
type ToolParamName,
toolParamNames,
type NativeToolArgs,
} from "../../shared/tools"
import { parseJSON } from "partial-json"
import type {
ApiStreamToolCallStartChunk,
Expand Down Expand Up @@ -41,11 +47,12 @@ export type ToolCallStreamEvent = ApiStreamToolCallStartChunk | ApiStreamToolCal
*/
export class NativeToolCallParser {
// Streaming state management for argument accumulation (keyed by tool call id)
// Note: name is string to accommodate dynamic MCP tools (mcp_serverName_toolName)
private static streamingToolCalls = new Map<
string,
{
id: string
name: ToolName
name: string
argumentsAccumulator: string
}
>()
Expand Down Expand Up @@ -188,8 +195,9 @@ export class NativeToolCallParser {
/**
* Start streaming a new tool call.
* Initializes tracking for incremental argument parsing.
* Accepts string to support both ToolName and dynamic MCP tools (mcp_serverName_toolName).
*/
public static startStreamingToolCall(id: string, name: ToolName): void {
public static startStreamingToolCall(id: string, name: string): void {
this.streamingToolCalls.set(id, {
id,
name,
Expand Down Expand Up @@ -229,6 +237,11 @@ export class NativeToolCallParser {
// Accumulate the JSON string
toolCall.argumentsAccumulator += chunk

// For dynamic MCP tools, we don't return partial updates - wait for final
if (toolCall.name.startsWith("mcp_")) {
return null
}

// Parse whatever we can from the incomplete JSON!
// partial-json-parser extracts partial values (strings, arrays, objects) immediately
try {
Expand All @@ -237,7 +250,7 @@ export class NativeToolCallParser {
// Create partial ToolUse with extracted values
return this.createPartialToolUse(
toolCall.id,
toolCall.name,
toolCall.name as ToolName,
partialArgs || {},
true, // partial
)
Expand All @@ -250,19 +263,20 @@ export class NativeToolCallParser {

/**
* Finalize a streaming tool call.
* Parses the complete JSON and returns the final ToolUse.
* Parses the complete JSON and returns the final ToolUse or McpToolUse.
*/
public static finalizeStreamingToolCall(id: string): ToolUse | null {
public static finalizeStreamingToolCall(id: string): ToolUse | McpToolUse | null {
const toolCall = this.streamingToolCalls.get(id)
if (!toolCall) {
console.warn(`[NativeToolCallParser] Attempting to finalize unknown tool call: ${id}`)
return null
}

// Parse the complete accumulated JSON
// Cast to any for the name since parseToolCall handles both ToolName and dynamic MCP tools
const finalToolUse = this.parseToolCall({
id: toolCall.id,
name: toolCall.name,
name: toolCall.name as ToolName,
arguments: toolCall.argumentsAccumulator,
})

Expand Down Expand Up @@ -490,10 +504,10 @@ export class NativeToolCallParser {
id: string
name: TName
arguments: string
}): ToolUse<TName> | null {
}): ToolUse<TName> | McpToolUse | null {
// Check if this is a dynamic MCP tool (mcp_serverName_toolName)
if (typeof toolCall.name === "string" && toolCall.name.startsWith("mcp_")) {
return this.parseDynamicMcpTool(toolCall) as ToolUse<TName> | null
return this.parseDynamicMcpTool(toolCall)
}

// Validate tool name
Expand Down Expand Up @@ -697,6 +711,15 @@ export class NativeToolCallParser {
}
break

case "access_mcp_resource":
if (args.server_name !== undefined && args.uri !== undefined) {
nativeArgs = {
server_name: args.server_name,
uri: args.uri,
} as NativeArgsFor<TName>
}
break

default:
break
}
Expand All @@ -719,51 +742,44 @@ export class NativeToolCallParser {

/**
* Parse dynamic MCP tools (named mcp_serverName_toolName).
* These are generated dynamically by getMcpServerTools() and need to be
* converted back to use_mcp_tool format.
* These are generated dynamically by getMcpServerTools() and are returned
* as McpToolUse objects that preserve the original tool name.
*
* In native mode, MCP tools are NOT converted to use_mcp_tool - they keep
* their original name so it appears correctly in API conversation history.
* The use_mcp_tool wrapper is only used in XML mode.
*/
private static parseDynamicMcpTool(toolCall: {
id: string
name: string
arguments: string
}): ToolUse<"use_mcp_tool"> | null {
public static parseDynamicMcpTool(toolCall: { id: string; name: string; arguments: string }): McpToolUse | null {
try {
const args = JSON.parse(toolCall.arguments)

// Extract server_name and tool_name from the arguments
// The dynamic tool schema includes these as const properties
const serverName = args.server_name
const toolName = args.tool_name
const toolInputProps = args.toolInputProps

if (!serverName || !toolName) {
console.error(`Missing server_name or tool_name in dynamic MCP tool`)
// Parse the arguments - these are the actual tool arguments passed directly
const args = JSON.parse(toolCall.arguments || "{}")

// Extract server_name and tool_name from the tool name itself
// Format: mcp_serverName_toolName
const nameParts = toolCall.name.split("_")
if (nameParts.length < 3 || nameParts[0] !== "mcp") {
console.error(`Invalid dynamic MCP tool name format: ${toolCall.name}`)
return null
}

// Build params for backward compatibility with XML protocol
const params: Partial<Record<string, string>> = {
server_name: serverName,
tool_name: toolName,
}

if (toolInputProps) {
params.arguments = JSON.stringify(toolInputProps)
}
// Server name is the second part, tool name is everything after
const serverName = nameParts[1]
const toolName = nameParts.slice(2).join("_")

// Build nativeArgs with properly typed structure
const nativeArgs: NativeToolArgs["use_mcp_tool"] = {
server_name: serverName,
tool_name: toolName,
arguments: toolInputProps,
if (!serverName || !toolName) {
console.error(`Could not extract server_name or tool_name from: ${toolCall.name}`)
return null
}

const result: ToolUse<"use_mcp_tool"> = {
type: "tool_use" as const,
name: "use_mcp_tool",
params,
const result: McpToolUse = {
type: "mcp_tool_use" as const,
id: toolCall.id,
// Keep the original tool name (e.g., "mcp_serverName_toolName") for API history
name: toolCall.name,
serverName,
toolName,
arguments: args,
partial: false,
nativeArgs,
}

return result
Expand Down
4 changes: 2 additions & 2 deletions src/core/assistant-message/parseAssistantMessage.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { type ToolName, toolNames } from "@roo-code/types"

import { TextContent, ToolUse, ToolParamName, toolParamNames } from "../../shared/tools"
import { TextContent, ToolUse, McpToolUse, ToolParamName, toolParamNames } from "../../shared/tools"

export type AssistantMessageContent = TextContent | ToolUse
export type AssistantMessageContent = TextContent | ToolUse | McpToolUse

export function parseAssistantMessage(assistantMessage: string): AssistantMessageContent[] {
let contentBlocks: AssistantMessageContent[] = []
Expand Down
Loading
Loading