diff --git a/.changeset/green-melons-retire.md b/.changeset/green-melons-retire.md
new file mode 100644
index 00000000000..65f12f4ddd2
--- /dev/null
+++ b/.changeset/green-melons-retire.md
@@ -0,0 +1,5 @@
+---
+"kilo-code": minor
+---
+
+Add Native MCP Support for JSON Tool Calling
diff --git a/apps/kilocode-docs/docs/features/experimental/native-function-calling.md b/apps/kilocode-docs/docs/features/experimental/native-function-calling.md
index 4d9b56d06d7..1fa194c242e 100644
--- a/apps/kilocode-docs/docs/features/experimental/native-function-calling.md
+++ b/apps/kilocode-docs/docs/features/experimental/native-function-calling.md
@@ -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.
@@ -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.
diff --git a/src/core/assistant-message/AssistantMessageParser.ts b/src/core/assistant-message/AssistantMessageParser.ts
index 94aed9ca4aa..22391f9ea4d 100644
--- a/src/core/assistant-message/AssistantMessageParser.ts
+++ b/src/core/assistant-message/AssistantMessageParser.ts
@@ -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
/**
@@ -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
}
@@ -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,
}
@@ -207,6 +239,7 @@ export class AssistantMessageParser {
}
}
}
+
// kilocode_change end
/**
diff --git a/src/core/assistant-message/kilocode/native-tool-call.ts b/src/core/assistant-message/kilocode/native-tool-call.ts
index 7fc844ebb93..55c426d36d2 100644
--- a/src/core/assistant-message/kilocode/native-tool-call.ts
+++ b/src/core/assistant-message/kilocode/native-tool-call.ts
@@ -1,3 +1,6 @@
+import { ToolName } from "@roo-code/types"
+import { ToolUse } from "../../../shared/tools"
+
/**
* Represents a native tool call from OpenAI-compatible APIs
*/
@@ -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 }
+}
diff --git a/src/core/prompts/sections/mcp-servers.ts b/src/core/prompts/sections/mcp-servers.ts
index fc257b9d524..c524a6bf9f4 100644
--- a/src/core/prompts/sections/mcp-servers.ts
+++ b/src/core/prompts/sections/mcp-servers.ts
@@ -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
@@ -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:
create_mcp_server
`
- }
-
- return descSection
+ )
}
diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts
index f300ffa5ac0..d123ee152b9 100644
--- a/src/core/prompts/system.ts
+++ b/src/core/prompts/system.ts
@@ -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(""),
])
diff --git a/src/core/prompts/tools/native-tools/__tests__/getAllowedJSONToolsForMode.spec.ts b/src/core/prompts/tools/native-tools/__tests__/getAllowedJSONToolsForMode.spec.ts
index 40c72a0086a..b8e5dafc21b 100644
--- a/src/core/prompts/tools/native-tools/__tests__/getAllowedJSONToolsForMode.spec.ts
+++ b/src/core/prompts/tools/native-tools/__tests__/getAllowedJSONToolsForMode.spec.ts
@@ -1,70 +1,376 @@
-import { describe, it, expect } from "vitest"
+import { beforeEach, describe, expect, it, vi } from "vitest"
import { getAllowedJSONToolsForMode } from "../getAllowedJSONToolsForMode"
import { Mode } from "../../../../../shared/modes"
-import { ClineProviderState } from "../../../../webview/ClineProvider"
+import { ClineProvider, ClineProviderState } from "../../../../webview/ClineProvider"
import { apply_diff_multi_file, apply_diff_single_file } from "../apply_diff"
+import { CodeIndexManager } from "../../../../../services/code-index/manager"
+import { McpServerManager } from "../../../../../services/mcp/McpServerManager"
+import { ContextProxy } from "../../../../config/ContextProxy"
+import * as vscode from "vscode"
+
+vi.mock("vscode")
+vi.mock("../../../../../services/code-index/manager")
+vi.mock("../../../../../services/mcp/McpServerManager")
+vi.mock("../../../../config/ContextProxy")
describe("getAllowedJSONToolsForMode", () => {
- const mockCodeIndexManager = {
- isFeatureEnabled: true,
- isFeatureConfigured: true,
- isInitialized: true,
- } as any
-
- const baseProviderState: Partial = {
- apiConfiguration: {
- diffEnabled: true,
- },
- experiments: {},
+ let mockProvider: Partial
+ let mockContext: any
+ const modelWithImages = {
+ id: "mock-model",
+ info: { contextWindow: 2048, supportsPromptCache: false, supportsImages: true },
+ }
+ const modelWithoutImages = {
+ id: "mock-model",
+ info: { contextWindow: 2048, supportsPromptCache: false, supportsImages: false },
}
- it("should use single file diff when multiFileApplyDiff experiment is disabled", () => {
- const providerState: Partial = {
- ...baseProviderState,
- apiConfiguration: {
- diffEnabled: true,
- },
- experiments: {
- multiFileApplyDiff: false,
+ beforeEach(() => {
+ vi.clearAllMocks()
+
+ mockContext = {
+ globalState: {
+ get: vi.fn(),
+ update: vi.fn(),
},
+ subscriptions: [],
+ }
+
+ mockProvider = {
+ context: mockContext,
+ getState: vi.fn(),
}
- const tools = getAllowedJSONToolsForMode(
- "code" as Mode,
- mockCodeIndexManager,
- providerState as ClineProviderState,
- true,
- undefined,
- )
-
- const applyDiffTool = tools.find((tool) => "function" in tool && tool.function.name === "apply_diff")
- expect(applyDiffTool).toBeDefined()
- expect(applyDiffTool).toEqual(apply_diff_single_file)
- expect(applyDiffTool).not.toEqual(apply_diff_multi_file)
+ // Mock ContextProxy static getter
+ vi.spyOn(ContextProxy, "instance", "get").mockReturnValue({
+ rawContext: mockContext,
+ } as any)
+
+ // Mock vscode workspace
+ vi.mocked(vscode.workspace).workspaceFolders = undefined
+
+ // Mock CodeIndexManager
+ vi.mocked(CodeIndexManager.getInstance).mockReturnValue(undefined)
+
+ // Mock McpServerManager
+ vi.mocked(McpServerManager.getInstance).mockResolvedValue(undefined as any)
})
- it("should use multi file diff when multiFileApplyDiff experiment is enabled", () => {
- const providerState: Partial = {
- ...baseProviderState,
- apiConfiguration: {
- diffEnabled: true,
- },
- experiments: {
- multiFileApplyDiff: true,
- },
- }
+ describe("apply_diff tool selection", () => {
+ it("should use single file diff when multiFileApplyDiff experiment is disabled", async () => {
+ const providerState: Partial = {
+ experiments: {
+ multiFileApplyDiff: false,
+ },
+ }
+
+ vi.mocked(mockProvider.getState!).mockResolvedValue(providerState as ClineProviderState)
+
+ const tools = await getAllowedJSONToolsForMode(
+ "code" as Mode,
+ mockProvider as ClineProvider,
+ true,
+ undefined,
+ )
+
+ const applyDiffTool = tools.find((tool) => "function" in tool && tool.function.name === "apply_diff")
+ expect(applyDiffTool).toBeDefined()
+ expect(applyDiffTool).toEqual(apply_diff_single_file)
+ expect(applyDiffTool).not.toEqual(apply_diff_multi_file)
+ })
+
+ it("should use multi file diff when multiFileApplyDiff experiment is enabled", async () => {
+ const providerState: Partial = {
+ experiments: {
+ multiFileApplyDiff: true,
+ },
+ }
+
+ vi.mocked(mockProvider.getState!).mockResolvedValue(providerState as ClineProviderState)
+
+ const tools = await getAllowedJSONToolsForMode(
+ "code" as Mode,
+ mockProvider as ClineProvider,
+ true,
+ undefined,
+ )
+
+ const applyDiffTool = tools.find((tool) => "function" in tool && tool.function.name === "apply_diff")
+ expect(applyDiffTool).toBeDefined()
+ expect(applyDiffTool).toEqual(apply_diff_multi_file)
+ expect(applyDiffTool).not.toEqual(apply_diff_single_file)
+ })
+
+ it("should not include apply_diff when diffEnabled is false", async () => {
+ const providerState: Partial = {
+ experiments: {
+ multiFileApplyDiff: true,
+ },
+ }
+
+ vi.mocked(mockProvider.getState!).mockResolvedValue(providerState as ClineProviderState)
+
+ const tools = await getAllowedJSONToolsForMode(
+ "code" as Mode,
+ mockProvider as ClineProvider,
+ false,
+ undefined,
+ )
+
+ const applyDiffTool = tools.find((tool) => "function" in tool && tool.function.name === "apply_diff")
+ expect(applyDiffTool).toBeUndefined()
+ })
+ })
+
+ describe("no duplicate tools", () => {
+ it("should not return duplicate tools", async () => {
+ const providerState: Partial = {
+ experiments: {},
+ }
+
+ vi.mocked(mockProvider.getState!).mockResolvedValue(providerState as ClineProviderState)
+
+ const tools = await getAllowedJSONToolsForMode(
+ "code" as Mode,
+ mockProvider as ClineProvider,
+ true,
+ modelWithImages,
+ )
+
+ // Check for duplicate tool names
+ const toolNames = tools.map((tool) => ("function" in tool ? tool.function.name : ""))
+ const uniqueToolNames = new Set(toolNames)
+
+ expect(toolNames.length).toBe(uniqueToolNames.size)
+ })
+
+ it("should return consistent tool count across multiple calls", async () => {
+ const providerState: Partial = {
+ experiments: {},
+ }
+
+ vi.mocked(mockProvider.getState!).mockResolvedValue(providerState as ClineProviderState)
+
+ const tools1 = await getAllowedJSONToolsForMode(
+ "code" as Mode,
+ mockProvider as ClineProvider,
+ true,
+ modelWithImages,
+ )
+ const tools2 = await getAllowedJSONToolsForMode(
+ "code" as Mode,
+ mockProvider as ClineProvider,
+ true,
+ modelWithImages,
+ )
+
+ expect(tools1.length).toBe(tools2.length)
+ })
+ })
+
+ describe("tool filtering", () => {
+ it("should exclude codebase_search when CodeIndexManager is not available", async () => {
+ const providerState: Partial = {
+ experiments: {},
+ }
+
+ vi.mocked(mockProvider.getState!).mockResolvedValue(providerState as ClineProviderState)
+ vi.mocked(CodeIndexManager.getInstance).mockReturnValue(undefined)
+
+ const tools = await getAllowedJSONToolsForMode(
+ "code" as Mode,
+ mockProvider as ClineProvider,
+ true,
+ modelWithImages,
+ )
+
+ const codebaseSearchTool = tools.find(
+ (tool) => "function" in tool && tool.function.name === "codebase_search",
+ )
+ expect(codebaseSearchTool).toBeUndefined()
+ })
+
+ it("should exclude browser_action when browserToolEnabled is false", async () => {
+ const providerState: Partial = {
+ browserToolEnabled: false,
+ experiments: {},
+ }
+
+ vi.mocked(mockProvider.getState!).mockResolvedValue(providerState as ClineProviderState)
+
+ const tools = await getAllowedJSONToolsForMode(
+ "code" as Mode,
+ mockProvider as ClineProvider,
+ true,
+ modelWithImages,
+ )
+
+ const browserActionTool = tools.find(
+ (tool) => "function" in tool && tool.function.name === "browser_action",
+ )
+ expect(browserActionTool).toBeUndefined()
+ })
+
+ it("should exclude browser_action when supportsImages is false", async () => {
+ const providerState: Partial = {
+ browserToolEnabled: true,
+ experiments: {},
+ }
+
+ vi.mocked(mockProvider.getState!).mockResolvedValue(providerState as ClineProviderState)
+
+ const tools = await getAllowedJSONToolsForMode(
+ "code" as Mode,
+ mockProvider as ClineProvider,
+ true,
+ modelWithoutImages,
+ )
+
+ const browserActionTool = tools.find(
+ (tool) => "function" in tool && tool.function.name === "browser_action",
+ )
+ expect(browserActionTool).toBeUndefined()
+ })
+
+ it("should exclude update_todo_list when todoListEnabled is false", async () => {
+ const providerState: Partial = {
+ apiConfiguration: {
+ todoListEnabled: false,
+ },
+ experiments: {},
+ }
+
+ vi.mocked(mockProvider.getState!).mockResolvedValue(providerState as ClineProviderState)
+
+ const tools = await getAllowedJSONToolsForMode(
+ "code" as Mode,
+ mockProvider as ClineProvider,
+ true,
+ modelWithImages,
+ )
+
+ const todoListTool = tools.find((tool) => "function" in tool && tool.function.name === "update_todo_list")
+ expect(todoListTool).toBeUndefined()
+ })
+
+ it("should exclude generate_image when imageGeneration experiment is disabled", async () => {
+ const providerState: Partial = {
+ experiments: {
+ imageGeneration: false,
+ },
+ }
+
+ vi.mocked(mockProvider.getState!).mockResolvedValue(providerState as ClineProviderState)
+
+ const tools = await getAllowedJSONToolsForMode(
+ "code" as Mode,
+ mockProvider as ClineProvider,
+ true,
+ modelWithImages,
+ )
+
+ const generateImageTool = tools.find(
+ (tool) => "function" in tool && tool.function.name === "generate_image",
+ )
+ expect(generateImageTool).toBeUndefined()
+ })
+
+ it("should exclude run_slash_command when runSlashCommand experiment is disabled", async () => {
+ const providerState: Partial = {
+ experiments: {
+ runSlashCommand: false,
+ },
+ }
+
+ vi.mocked(mockProvider.getState!).mockResolvedValue(providerState as ClineProviderState)
+
+ const tools = await getAllowedJSONToolsForMode(
+ "code" as Mode,
+ mockProvider as ClineProvider,
+ true,
+ modelWithImages,
+ )
+
+ const runSlashCommandTool = tools.find(
+ (tool) => "function" in tool && tool.function.name === "run_slash_command",
+ )
+ expect(runSlashCommandTool).toBeUndefined()
+ })
+ })
+
+ describe("always available tools", () => {
+ it("should always include ask_followup_question", async () => {
+ const providerState: Partial = {
+ experiments: {},
+ }
+
+ vi.mocked(mockProvider.getState!).mockResolvedValue(providerState as ClineProviderState)
+
+ const tools = await getAllowedJSONToolsForMode(
+ "code" as Mode,
+ mockProvider as ClineProvider,
+ true,
+ modelWithImages,
+ )
+
+ const askTool = tools.find((tool) => "function" in tool && tool.function.name === "ask_followup_question")
+ expect(askTool).toBeDefined()
+ })
+
+ it("should always include attempt_completion", async () => {
+ const providerState: Partial = {
+ experiments: {},
+ }
+
+ vi.mocked(mockProvider.getState!).mockResolvedValue(providerState as ClineProviderState)
+
+ const tools = await getAllowedJSONToolsForMode(
+ "code" as Mode,
+ mockProvider as ClineProvider,
+ true,
+ modelWithoutImages,
+ )
+
+ const completionTool = tools.find(
+ (tool) => "function" in tool && tool.function.name === "attempt_completion",
+ )
+ expect(completionTool).toBeDefined()
+ })
+
+ it("should always include switch_mode", async () => {
+ const providerState: Partial = {
+ experiments: {},
+ }
+
+ vi.mocked(mockProvider.getState!).mockResolvedValue(providerState as ClineProviderState)
+
+ const tools = await getAllowedJSONToolsForMode(
+ "code" as Mode,
+ mockProvider as ClineProvider,
+ true,
+ modelWithImages,
+ )
+
+ const switchModeTool = tools.find((tool) => "function" in tool && tool.function.name === "switch_mode")
+ expect(switchModeTool).toBeDefined()
+ })
+
+ it("should always include new_task", async () => {
+ const providerState: Partial = {
+ experiments: {},
+ }
+
+ vi.mocked(mockProvider.getState!).mockResolvedValue(providerState as ClineProviderState)
+
+ const tools = await getAllowedJSONToolsForMode(
+ "code" as Mode,
+ mockProvider as ClineProvider,
+ true,
+ modelWithImages,
+ )
- const tools = getAllowedJSONToolsForMode(
- "code" as Mode,
- mockCodeIndexManager,
- providerState as ClineProviderState,
- true,
- undefined,
- )
-
- const applyDiffTool = tools.find((tool) => "function" in tool && tool.function.name === "apply_diff")
- expect(applyDiffTool).toBeDefined()
- expect(applyDiffTool).toEqual(apply_diff_multi_file)
- expect(applyDiffTool).not.toEqual(apply_diff_single_file)
+ const newTaskTool = tools.find((tool) => "function" in tool && tool.function.name === "new_task")
+ expect(newTaskTool).toBeDefined()
+ })
})
})
diff --git a/src/core/prompts/tools/native-tools/ask_followup_question.ts b/src/core/prompts/tools/native-tools/ask_followup_question.ts
index 81846d762cb..8ea03c00a05 100644
--- a/src/core/prompts/tools/native-tools/ask_followup_question.ts
+++ b/src/core/prompts/tools/native-tools/ask_followup_question.ts
@@ -15,9 +15,9 @@ export default {
description: "Clear, specific question that captures the missing information you need",
},
follow_up: {
- type: ["array", "null"],
+ type: "array",
description:
- "Optional list of 2-4 suggested responses; each suggestion must be a complete, actionable answer and may include a mode switch",
+ "Required list of 2-4 suggested responses; each suggestion must be a complete, actionable answer and may include a mode switch",
items: {
type: "object",
properties: {
diff --git a/src/core/prompts/tools/native-tools/getAllowedJSONToolsForMode.ts b/src/core/prompts/tools/native-tools/getAllowedJSONToolsForMode.ts
index d0bd9823d70..d86272fa9f8 100644
--- a/src/core/prompts/tools/native-tools/getAllowedJSONToolsForMode.ts
+++ b/src/core/prompts/tools/native-tools/getAllowedJSONToolsForMode.ts
@@ -7,16 +7,56 @@ import { ALWAYS_AVAILABLE_TOOLS, TOOL_GROUPS } from "../../../../shared/tools"
import { isFastApplyAvailable } from "../../../tools/editFileTool"
import { nativeTools } from "."
import { apply_diff_multi_file, apply_diff_single_file } from "./apply_diff"
+import pWaitFor from "p-wait-for"
+import { McpHub } from "../../../../services/mcp/McpHub"
+import { McpServerManager } from "../../../../services/mcp/McpServerManager"
+import { getMcpServerTools } from "./mcp_server"
+import { ClineProvider } from "../../../webview/ClineProvider"
+import { ContextProxy } from "../../../config/ContextProxy"
+import * as vscode from "vscode"
import { read_file_multi, read_file_single } from "./read_file"
-export function getAllowedJSONToolsForMode(
+export async function getAllowedJSONToolsForMode(
mode: Mode,
- codeIndexManager: CodeIndexManager | undefined,
- clineProviderState: ClineProviderState | undefined,
- diffEnabled: boolean,
+ provider: ClineProvider | undefined,
+ diffEnabled: boolean = false,
model: { id: string; info: ModelInfo } | undefined,
-): OpenAI.Chat.ChatCompletionTool[] {
- const config = getModeConfig(mode, clineProviderState?.customModes)
+): Promise {
+ const providerState: ClineProviderState | undefined = await provider?.getState()
+ const config = getModeConfig(mode, providerState?.customModes)
+ const context = ContextProxy.instance.rawContext
+
+ // Initialize code index managers for all workspace folders.
+ let codeIndexManager: CodeIndexManager | undefined = undefined
+
+ if (vscode.workspace.workspaceFolders) {
+ for (const folder of vscode.workspace.workspaceFolders) {
+ const manager = CodeIndexManager.getInstance(context, folder.uri.fsPath)
+ if (manager) {
+ codeIndexManager = manager
+ }
+ }
+ }
+
+ const { mcpEnabled } = providerState ?? {}
+ let mcpHub: McpHub | undefined
+ if (mcpEnabled) {
+ if (!provider) {
+ throw new Error("Provider reference lost during view transition")
+ }
+
+ // Wait for MCP hub initialization through McpServerManager
+ mcpHub = await McpServerManager.getInstance(provider.context, provider)
+
+ if (!mcpHub) {
+ throw new Error("Failed to get MCP hub from server manager")
+ }
+
+ // Wait for MCP servers to be connected before generating system prompt
+ await pWaitFor(() => !mcpHub!.isConnecting, { timeout: 10_000 }).catch(() => {
+ console.error("MCP servers failed to connect in time")
+ })
+ }
const tools = new Set()
@@ -30,10 +70,10 @@ export function getAllowedJSONToolsForMode(
isToolAllowedForMode(
tool as ToolName,
mode,
- clineProviderState?.customModes ?? [],
+ providerState?.customModes ?? [],
undefined,
undefined,
- clineProviderState?.experiments ?? {},
+ providerState?.experiments ?? {},
)
) {
tools.add(tool)
@@ -53,8 +93,8 @@ export function getAllowedJSONToolsForMode(
tools.delete("codebase_search")
}
- if (isFastApplyAvailable(clineProviderState)) {
- // When Morph is enabled, disable traditional editing tools
+ if (isFastApplyAvailable(providerState)) {
+ // When Fast Apply is enabled, disable traditional editing tools
const traditionalEditingTools = ["apply_diff", "write_to_file", "insert_content", "search_and_replace"]
traditionalEditingTools.forEach((tool) => tools.delete(tool))
} else {
@@ -62,27 +102,32 @@ export function getAllowedJSONToolsForMode(
}
// Conditionally exclude update_todo_list if disabled in settings
- if (clineProviderState?.apiConfiguration?.todoListEnabled === false) {
+ if (providerState?.apiConfiguration?.todoListEnabled === false) {
tools.delete("update_todo_list")
}
// Conditionally exclude generate_image if experiment is not enabled
- if (!clineProviderState?.experiments?.imageGeneration) {
+ if (!providerState?.experiments?.imageGeneration) {
tools.delete("generate_image")
}
// Conditionally exclude run_slash_command if experiment is not enabled
- if (!clineProviderState?.experiments?.runSlashCommand) {
+ if (!providerState?.experiments?.runSlashCommand) {
tools.delete("run_slash_command")
}
- if (!clineProviderState?.browserToolEnabled || !model?.info.supportsImages) {
+ if (!providerState?.browserToolEnabled || !model?.info.supportsImages) {
tools.delete("browser_action")
}
// Create a map of tool names to native tool definitions for quick lookup
// Exclude apply_diff tools as they are handled specially below
- const allowedTools: OpenAI.Chat.ChatCompletionTool[] = []
+ // Create a map of tool names to native tool definitions for quick lookup
+ const nativeToolsMap = new Map()
+ nativeTools.forEach((tool) => {
+ nativeToolsMap.set(tool.function.name, tool)
+ })
+ let allowedTools: OpenAI.Chat.ChatCompletionTool[] = []
let isReadFileToolAllowedForMode = false
let isApplyDiffToolAllowedForMode = false
@@ -112,12 +157,21 @@ export function getAllowedJSONToolsForMode(
// Handle the "apply_diff" logic separately because the same tool has different
// implementations depending on whether multi-file diffs are enabled, but the same name is used.
if (isApplyDiffToolAllowedForMode && diffEnabled) {
- if (clineProviderState?.experiments.multiFileApplyDiff) {
+ if (providerState?.experiments.multiFileApplyDiff) {
allowedTools.push(apply_diff_multi_file)
} else {
allowedTools.push(apply_diff_single_file)
}
}
+ // Check if MCP functionality should be included
+ const hasMcpGroup = config.groups.some((groupEntry) => getGroupName(groupEntry) === "mcp")
+ if (hasMcpGroup && mcpHub) {
+ const mcpTools = getMcpServerTools(mcpHub)
+ if (mcpTools) {
+ allowedTools.push(...mcpTools)
+ }
+ }
+
return allowedTools
}
diff --git a/src/core/prompts/tools/native-tools/list_files.ts b/src/core/prompts/tools/native-tools/list_files.ts
index 5a2f9a83d9e..6f48ec3e886 100644
--- a/src/core/prompts/tools/native-tools/list_files.ts
+++ b/src/core/prompts/tools/native-tools/list_files.ts
@@ -15,8 +15,8 @@ export default {
description: "Directory path to inspect, relative to the workspace",
},
recursive: {
- type: ["boolean", "null"],
- description: "Set true to list contents recursively; omit or false to show only the top level",
+ type: ["boolean"],
+ description: "Set true to list contents recursively; false to show only the top level",
},
},
required: ["path", "recursive"],
diff --git a/src/core/prompts/tools/native-tools/mcp_server.ts b/src/core/prompts/tools/native-tools/mcp_server.ts
new file mode 100644
index 00000000000..db63745f612
--- /dev/null
+++ b/src/core/prompts/tools/native-tools/mcp_server.ts
@@ -0,0 +1,105 @@
+import type OpenAI from "openai"
+import { McpHub } from "../../../../services/mcp/McpHub"
+
+/**
+ * Dynamically generates native tool definitions for all enabled tools across connected MCP servers.
+ *
+ * @param mcpHub The McpHub instance containing connected servers.
+ * @returns An array of OpenAI.Chat.ChatCompletionTool definitions.
+ */
+export function getMcpServerTools(mcpHub?: McpHub): OpenAI.Chat.ChatCompletionTool[] {
+ if (!mcpHub) {
+ return []
+ }
+
+ const servers = mcpHub.getServers()
+ const tools: OpenAI.Chat.ChatCompletionTool[] = []
+
+ for (const server of servers) {
+ if (!server.tools) {
+ continue
+ }
+ for (const tool of server.tools) {
+ // Filter tools where tool.enabledForPrompt is not explicitly false
+ if (tool.enabledForPrompt === false) {
+ continue
+ }
+
+ // Ensure parameters is a valid FunctionParameters object, even if inputSchema is undefined
+ const parameters = {
+ type: "object",
+ properties: {
+ server_name: {
+ type: "string",
+ const: server.name,
+ },
+ tool_name: {
+ type: "string",
+ const: tool.name,
+ },
+ },
+ required: ["server_name", "tool_name", "toolInputProps"],
+ additionalProperties: false,
+ } as OpenAI.FunctionParameters
+
+ const originalSchema = tool.inputSchema as Record | undefined
+ const toolInputPropsRaw = originalSchema?.properties ?? {}
+ const toolInputRequired = (originalSchema?.required ?? []) as string[]
+
+ // Handle reserved property names like 'type'
+ const sanitizedToolInputProps: Record = {}
+ const sanitizedRequired: string[] = []
+
+ for (const [propName, propValue] of Object.entries(toolInputPropsRaw)) {
+ // rename 'type' to 'renamed_type' because 'type' is a reserved word in JSON Schema
+ // for many parsers.
+ if (propName === "type") {
+ sanitizedToolInputProps[`renamed_${propName}`] = propValue
+ // Update required array if 'type' was required
+ if (toolInputRequired.includes(propName)) {
+ sanitizedRequired.push(`renamed_${propName}`)
+ }
+ } else {
+ sanitizedToolInputProps[propName] = propValue
+ if (toolInputRequired.includes(propName)) {
+ sanitizedRequired.push(propName)
+ }
+ }
+ }
+
+ // Create a proper JSON Schema object for toolInputProps
+ const toolInputPropsSchema: Record = {
+ type: "object",
+ properties: sanitizedToolInputProps,
+ additionalProperties: false,
+ }
+
+ // Only add required if there are required fields
+ if (sanitizedRequired.length > 0) {
+ toolInputPropsSchema.required = sanitizedRequired
+ }
+
+ parameters.properties = {
+ toolInputProps: toolInputPropsSchema,
+ ...(parameters.properties as Record), //putting this second ensures it overrides anything in the tool def.
+ }
+
+ //Add the server_name and tool_name properties
+
+ // The description matches what the MCP server provides as guidance.
+ // Use triple underscores as separator to allow underscores in tool names
+ const toolDefinition: OpenAI.Chat.ChatCompletionTool = {
+ type: "function",
+ function: {
+ name: `use_mcp_tool___${server.name}___${tool.name}`,
+ description: tool.description,
+ parameters: parameters,
+ },
+ }
+
+ tools.push(toolDefinition)
+ }
+ }
+
+ return tools
+}
diff --git a/src/core/prompts/tools/native-tools/read_file.ts b/src/core/prompts/tools/native-tools/read_file.ts
index 70fd77a5cdc..d91b3e76254 100644
--- a/src/core/prompts/tools/native-tools/read_file.ts
+++ b/src/core/prompts/tools/native-tools/read_file.ts
@@ -28,7 +28,6 @@ export const read_file_multi = {
type: "string",
pattern: "^\\d+-\\d+$",
},
- minItems: 1,
},
},
required: ["path", "line_ranges"],
diff --git a/src/core/prompts/tools/native-tools/switch_mode.ts b/src/core/prompts/tools/native-tools/switch_mode.ts
index 0a9ab1815d5..979ba124005 100644
--- a/src/core/prompts/tools/native-tools/switch_mode.ts
+++ b/src/core/prompts/tools/native-tools/switch_mode.ts
@@ -15,8 +15,8 @@ export default {
description: "Slug of the mode to switch to (e.g., code, ask, architect)",
},
reason: {
- type: ["string", "null"],
- description: "Optional explanation for why the mode switch is needed",
+ type: "string",
+ description: "Explanation for why the mode switch is needed",
},
},
required: ["mode_slug", "reason"],
diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts
index 591279543ba..56bcff99415 100644
--- a/src/core/task/Task.ts
+++ b/src/core/task/Task.ts
@@ -2957,17 +2957,12 @@ export class Task extends EventEmitter implements TaskLike {
if (getActiveToolUseStyle(apiConfiguration) === "json" && mode) {
try {
const provider = this.providerRef.deref()
- const providerState = await provider?.getState()
-
- const allowedTools = getAllowedJSONToolsForMode(
+ metadata.allowedTools = await getAllowedJSONToolsForMode(
mode,
- undefined, // codeIndexManager is private, not accessible here
- providerState,
+ provider,
this.diffEnabled,
this.api?.getModel(),
)
-
- metadata.allowedTools = allowedTools
} catch (error) {
console.error("[Task] Error getting allowed tools for mode:", error)
// Continue without allowedTools - will fall back to default behavior
diff --git a/src/core/tools/useMcpToolTool.ts b/src/core/tools/useMcpToolTool.ts
index 9a4a56edd88..464033133d3 100644
--- a/src/core/tools/useMcpToolTool.ts
+++ b/src/core/tools/useMcpToolTool.ts
@@ -83,6 +83,30 @@ async function validateParams(
}
}
+/**
+ * Reverses property renaming applied in schema generation.
+ * Properties named `renamed_*` are converted back to their original names.
+ */
+function reversePropertyRenaming(args: Record | undefined): Record | undefined {
+ if (!args) {
+ return args
+ }
+
+ const reversed: Record = {}
+
+ for (const [key, value] of Object.entries(args)) {
+ if (key.startsWith("renamed_")) {
+ // Extract original property name (e.g., "renamed_type" -> "type")
+ const originalKey = key.substring("renamed_".length)
+ reversed[originalKey] = value
+ } else {
+ reversed[key] = value
+ }
+ }
+
+ return reversed
+}
+
async function validateToolExists(
cline: Task,
serverName: string,
@@ -239,7 +263,10 @@ async function executeToolAndProcessResult(
toolName,
})
- const toolResult = await cline.providerRef.deref()?.getMcpHub()?.callTool(serverName, toolName, parsedArguments)
+ // Reverse any property renaming before calling the tool
+ const actualArguments = reversePropertyRenaming(parsedArguments)
+
+ const toolResult = await cline.providerRef.deref()?.getMcpHub()?.callTool(serverName, toolName, actualArguments)
let toolResultPretty = "(No response)"
diff --git a/webview-ui/src/components/chat/McpExecution.tsx b/webview-ui/src/components/chat/McpExecution.tsx
index 90be64913fa..478044316d2 100644
--- a/webview-ui/src/components/chat/McpExecution.tsx
+++ b/webview-ui/src/components/chat/McpExecution.tsx
@@ -54,6 +54,12 @@ export const McpExecution = ({
// kilocode_change: Main collapse state for the entire MCP execution content
const [isResponseExpanded, setIsResponseExpanded] = useState(initiallyExpanded)
+ // Remove "renamed_" prefix from property names in JSON
+ const removeRenamedPrefix = useCallback((text: string): string => {
+ if (!text) return text
+ return text.replace(/"renamed_([^"]+)":/g, '"$1":')
+ }, [])
+
// Try to parse JSON and return both the result and formatted text
const tryParseJson = useCallback((text: string): { isJson: boolean; formatted: string } => {
if (!text) return { isJson: false, formatted: "" }
@@ -275,7 +281,7 @@ export const McpExecution = ({
"mt-1 pt-1":
!isArguments && (useMcpServer?.type === "use_mcp_tool" || (toolName && serverName)),
})}>
-
+
)}