Skip to content
5 changes: 5 additions & 0 deletions .changeset/green-melons-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---

Add Native MCP Support for JSON Tool Calling
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,16 @@ Because of these risks and considerations, this capability is experiment, and of

To enable and use native function calling, consider and perform the following:

1. Ensure you are using a provider that has been enabled in Kilo Code for this experiment. As of Oct 16, 2025, they include:
1. Ensure you are using a provider that has been enabled in Kilo Code for this experiment. As of Oct 21, 2025, they include:

- OpenRouter
- Kilo Code
- LM Studio
- OpenAI Compatible
- Z.ai
- Synthetic
- X.ai
- Chutes

By default, native function calling is _disabled_ for most models. Should you wish to try it, open the Advanced settings for a given provider profile that is included in the testing group.

Expand All @@ -55,11 +59,13 @@ Change the Tool Calling Style to `JSON`, and save the profile.
## Caveats

This feature is currently experimental and mostly intended for users interested in contributing to its development.
It is so far only supported when using OpenRouter or Kilo Code providers. There are possible issues including, but not limited to:

- Missing tools
There are possible issues including, but not limited to:

- ~~Missing tools~~: As of Oct 21, all tools are supported
- Tools calls not updating the UI until they are complete
- MCP servers not working
- ~~MCP servers not working~~: As of Oct 21, MCPs are supported
- Errors specific to certain inference providers
- Not all inference providers use servers that are fully compatible with the OpenAI specification. As a result, behavior will vary, even with the same model across providers.

While nearly any provider can be configured via the OpenAI Compatible profile, testers should be aware that this is enabled purely for ease of testing and should be prepared to experience unexpected responses from providers that are not prepared to handle native function calls.
43 changes: 38 additions & 5 deletions src/core/assistant-message/AssistantMessageParser.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type ToolName, toolNames } from "@roo-code/types"
import { TextContent, ToolUse, ToolParamName, toolParamNames } from "../../shared/tools"
import { AssistantMessageContent } from "./parseAssistantMessage"
import { NativeToolCall, parseDoubleEncodedParams } from "./kilocode/native-tool-call"
import { extractMcpToolInfo, NativeToolCall, parseDoubleEncodedParams } from "./kilocode/native-tool-call"
import Anthropic from "@anthropic-ai/sdk" // kilocode_change

/**
Expand Down Expand Up @@ -119,8 +119,11 @@ export class AssistantMessageParser {
if (toolCall.function?.name) {
const toolName = toolCall.function.name

// Validate that this is a recognized tool name
if (!toolNames.includes(toolName as ToolName)) {
// Check if it's a dynamic MCP tool or a recognized static tool name
const mcpToolInfo = extractMcpToolInfo(toolName)
const isValidTool = mcpToolInfo !== null || toolNames.includes(toolName as ToolName)

if (!isValidTool) {
console.warn("[AssistantMessageParser] Unknown tool name in native call:", toolName)
continue
}
Expand Down Expand Up @@ -176,17 +179,46 @@ export class AssistantMessageParser {
// Tool call is complete - convert it to ToolUse format
if (isComplete) {
const toolName = accumulatedCall.function!.name

// Finalize any current text content before adding tool use
if (this.currentTextContent) {
this.currentTextContent.partial = false
this.currentTextContent = undefined
}

// Normalize dynamic MCP tool names to "use_mcp_tool"
// Dynamic tools have format: use_mcp_tool_{serverName}_{toolName}
const mcpToolInfo = extractMcpToolInfo(toolName)
let normalizedToolName: ToolName
let normalizedParams = parsedArgs

if (mcpToolInfo) {
// Dynamic MCP tool - normalize to "use_mcp_tool"
// Tool name format: use_mcp_tool___{serverName}___{toolName}
normalizedToolName = "use_mcp_tool"

// Extract toolInputProps and convert to JSON string for the arguments parameter
// The model provides: { server_name, tool_name, toolInputProps: {...actual args...} }
// We need: { server_name, tool_name, arguments: "{...actual args as JSON string...}" }
const toolInputProps = (parsedArgs as any).toolInputProps || {}
const argumentsJson = JSON.stringify(toolInputProps)

// Add server_name, tool_name, and arguments to params
normalizedParams = {
server_name: parsedArgs.server_name || mcpToolInfo.serverName,
tool_name: parsedArgs.tool_name || mcpToolInfo.toolName,
arguments: argumentsJson,
}
} else {
// Standard tool
normalizedToolName = toolName as ToolName
}

// Create a ToolUse block from the native tool call
const toolUse: ToolUse = {
type: "tool_use",
name: toolName as ToolName,
params: parsedArgs,
name: normalizedToolName,
params: normalizedParams,
partial: false, // Now complete after accumulation
toolUseId: accumulatedCall.id,
}
Expand All @@ -207,6 +239,7 @@ export class AssistantMessageParser {
}
}
}

// kilocode_change end

/**
Expand Down
45 changes: 45 additions & 0 deletions src/core/assistant-message/kilocode/native-tool-call.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { ToolName } from "@roo-code/types"
import { ToolUse } from "../../../shared/tools"

/**
* Represents a native tool call from OpenAI-compatible APIs
*/
Expand Down Expand Up @@ -57,3 +60,45 @@ export function parseDoubleEncodedParams(obj: any): any {
// Primitive types (number, boolean, etc.) return as-is
return obj
}

const NATIVE_MCP_TOOL_PREFIX = "use_mcp_tool___"
const NATIVE_MCP_TOOL_SEPARATOR = "___"

/**
* Check if a tool name is a dynamic MCP tool (starts with "use_mcp_tool_")
*/
function isDynamicMcpTool(toolName: string): boolean {
return toolName.startsWith(NATIVE_MCP_TOOL_PREFIX)
}

/**
* Extract server name and tool name from dynamic MCP tool names.
* Format: use_mcp_tool___{serverName}___{toolName}
* Uses triple underscores as separator to allow underscores in tool names.
* Returns null if the format is invalid.
*/
export function extractMcpToolInfo(toolName: string): { serverName: string; toolName: string } | null {
if (!isDynamicMcpTool(toolName)) {
return null
}

// Remove the prefix
const remainder = toolName.slice(NATIVE_MCP_TOOL_PREFIX.length)

// Find first triple underscore to split server name and tool name

const firstSeparatorIndex = remainder.indexOf(NATIVE_MCP_TOOL_SEPARATOR)

if (firstSeparatorIndex === -1) {
return null // Invalid format
}

const serverName = remainder.slice(0, firstSeparatorIndex)
const extractedToolName = remainder.slice(firstSeparatorIndex + NATIVE_MCP_TOOL_SEPARATOR.length)

if (!serverName || !extractedToolName) {
return null // Invalid format
}

return { serverName, toolName: extractedToolName }
}
16 changes: 8 additions & 8 deletions src/core/prompts/sections/mcp-servers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export async function getMcpServersSection(
if (!mcpHub) {
return ""
}
// kilocode_change start
if (toolUseStyle === "json") {
return ""
}
// kilocode_change end

const connectedServers =
mcpHub.getServers().length > 0
Expand Down Expand Up @@ -68,19 +73,14 @@ ${connectedServers}`
return baseSection
}

let descSection =
return (
baseSection +
`
## Creating an MCP Server

The user may ask you something along the lines of "add a tool" that does some function, in other words to create an MCP server that provides tools and resources that may connect to external APIs for example. If they do, you should obtain detailed instructions on this topic using the fetch_instructions tool, `
// kilocode_change: toolUseStyle
if (toolUseStyle !== "json") {
descSection += `like this:
The user may ask you something along the lines of "add a tool" that does some function, in other words to create an MCP server that provides tools and resources that may connect to external APIs for example. If they do, you should obtain detailed instructions on this topic using the fetch_instructions tool, like this:
<fetch_instructions>
<task>create_mcp_server</task>
</fetch_instructions>`
}

return descSection
)
}
7 changes: 6 additions & 1 deletion src/core/prompts/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,12 @@ async function generatePrompt(
const [modesSection, mcpServersSection] = await Promise.all([
getModesSection(context, toolUseStyle /*kilocode_change*/),
shouldIncludeMcp
? getMcpServersSection(mcpHub, effectiveDiffStrategy, enableMcpServerCreation)
? getMcpServersSection(
mcpHub,
effectiveDiffStrategy,
enableMcpServerCreation,
toolUseStyle, // kilocode_change
)
: Promise.resolve(""),
])

Expand Down
Loading