Add use_mcp_tool and access_mcp_resource tools

This commit is contained in:
Saoud Rizwan
2024-12-07 19:46:59 -08:00
parent 17d481d4d1
commit 1492604ee6
14 changed files with 821 additions and 66 deletions

View File

@@ -28,6 +28,7 @@ import {
ClineApiReqCancelReason,
ClineApiReqInfo,
ClineAsk,
ClineAskUseMcpServer,
ClineMessage,
ClineSay,
ClineSayBrowserAction,
@@ -757,7 +758,7 @@ export class Cline {
})
const mcpServers = this.providerRef.deref()?.mcpHub?.connections.map((conn) => conn.server)
console.log("mcpServers for system prompt:", JSON.stringify(mcpServers, null, 2))
let systemPrompt = await SYSTEM_PROMPT(cwd, this.api.getModel().info.supportsComputerUse ?? false)
let systemPrompt = await SYSTEM_PROMPT(cwd, this.api.getModel().info.supportsComputerUse ?? false, mcpServers)
if (this.customInstructions && this.customInstructions.trim()) {
// altering the system prompt mid-task will break the prompt cache, but in the grand scheme this will not change often so it's better to not pollute user messages with it the way we have to with <potentially relevant details>
systemPrompt += addCustomInstructions(this.customInstructions)
@@ -898,6 +899,10 @@ export class Cline {
return `[${block.name} for '${block.params.path}']`
case "browser_action":
return `[${block.name} for '${block.params.action}']`
case "use_mcp_tool":
return `[${block.name} for '${block.params.server_name}']`
case "access_mcp_resource":
return `[${block.name} for '${block.params.server_name}']`
case "ask_followup_question":
return `[${block.name} for '${block.params.question}']`
case "attempt_completion":
@@ -1544,7 +1549,163 @@ export class Cline {
break
}
}
case "use_mcp_tool": {
const server_name: string | undefined = block.params.server_name
const tool_name: string | undefined = block.params.tool_name
const mcp_arguments: string | undefined = block.params.arguments
try {
if (block.partial) {
const partialMessage = JSON.stringify({
type: "use_mcp_tool",
serverName: removeClosingTag("server_name", server_name),
toolName: removeClosingTag("tool_name", tool_name),
arguments: removeClosingTag("arguments", mcp_arguments),
} satisfies ClineAskUseMcpServer)
await this.ask("use_mcp_server", partialMessage, block.partial).catch(() => {})
break
} else {
if (!server_name) {
this.consecutiveMistakeCount++
pushToolResult(
await this.sayAndCreateMissingParamError("use_mcp_tool", "server_name"),
)
break
}
if (!tool_name) {
this.consecutiveMistakeCount++
pushToolResult(
await this.sayAndCreateMissingParamError("use_mcp_tool", "tool_name"),
)
break
}
// arguments are optional, but if they are provided they must be valid JSON
// if (!mcp_arguments) {
// this.consecutiveMistakeCount++
// pushToolResult(await this.sayAndCreateMissingParamError("use_mcp_tool", "arguments"))
// break
// }
let parsedArguments: Record<string, unknown> | undefined
if (mcp_arguments) {
try {
parsedArguments = JSON.parse(mcp_arguments)
} catch (error) {
this.consecutiveMistakeCount++
await this.say(
"error",
`Cline tried to use ${tool_name} with an invalid JSON argument. Retrying...`,
)
pushToolResult(
formatResponse.toolError(
formatResponse.invalidMcpToolArgumentError(server_name, tool_name),
),
)
break
}
}
this.consecutiveMistakeCount = 0
const completeMessage = JSON.stringify({
type: "use_mcp_tool",
serverName: server_name,
toolName: tool_name,
arguments: mcp_arguments,
} satisfies ClineAskUseMcpServer)
const didApprove = await askApproval("use_mcp_server", completeMessage)
if (!didApprove) {
break
}
// now execute the tool
await this.say("mcp_server_request_started") // same as browser_action_result
const toolResult = await this.providerRef
.deref()
?.mcpHub?.callTool(server_name, tool_name, parsedArguments)
// TODO: add progress indicator and ability to parse images and non-text responses
const toolResultPretty =
(toolResult?.isError
? "Error: The tool call failed"
: toolResult?.content
.map((item) => {
if (item.type === "text") {
return item.text
}
if (item.type === "resource") {
const { blob, ...rest } = item.resource
return JSON.stringify(rest, null, 2)
}
return ""
})
.filter(Boolean)
.join("\n\n")) || "(Empty response)"
await this.say("mcp_server_response", toolResultPretty)
pushToolResult(formatResponse.toolResult(toolResultPretty))
break
}
} catch (error) {
await handleError("executing MCP tool", error)
break
}
}
case "access_mcp_resource": {
const server_name: string | undefined = block.params.server_name
const uri: string | undefined = block.params.uri
try {
if (block.partial) {
const partialMessage = JSON.stringify({
type: "access_mcp_resource",
serverName: removeClosingTag("server_name", server_name),
uri: removeClosingTag("uri", uri),
} satisfies ClineAskUseMcpServer)
await this.ask("use_mcp_server", partialMessage, block.partial).catch(() => {})
break
} else {
if (!server_name) {
this.consecutiveMistakeCount++
pushToolResult(
await this.sayAndCreateMissingParamError("access_mcp_resource", "server_name"),
)
break
}
if (!uri) {
this.consecutiveMistakeCount++
pushToolResult(
await this.sayAndCreateMissingParamError("access_mcp_resource", "uri"),
)
break
}
this.consecutiveMistakeCount = 0
const completeMessage = JSON.stringify({
type: "access_mcp_resource",
serverName: server_name,
uri,
} satisfies ClineAskUseMcpServer)
const didApprove = await askApproval("use_mcp_server", completeMessage)
if (!didApprove) {
break
}
// now execute the tool
await this.say("mcp_server_request_started")
const resourceResult = await this.providerRef
.deref()
?.mcpHub?.readResource(server_name, uri)
const resourceResultPretty =
resourceResult?.contents
.map((item) => {
if (item.text) {
return item.text
}
return ""
})
.filter(Boolean)
.join("\n\n") || "(Empty response)"
await this.say("mcp_server_response", resourceResultPretty)
pushToolResult(formatResponse.toolResult(resourceResultPretty))
break
}
} catch (error) {
await handleError("accessing MCP resource", error)
break
}
}
case "ask_followup_question": {
const question: string | undefined = block.params.question
try {

View File

@@ -16,6 +16,8 @@ export const toolUseNames = [
"list_files",
"list_code_definition_names",
"browser_action",
"use_mcp_tool",
"access_mcp_resource",
"ask_followup_question",
"attempt_completion",
] as const
@@ -34,6 +36,10 @@ export const toolParamNames = [
"url",
"coordinate",
"text",
"server_name",
"tool_name",
"arguments",
"uri",
"question",
"result",
] as const
@@ -84,6 +90,16 @@ export interface BrowserActionToolUse extends ToolUse {
params: Partial<Pick<Record<ToolParamName, string>, "action" | "url" | "coordinate" | "text">>
}
export interface UseMcpToolToolUse extends ToolUse {
name: "use_mcp_tool"
params: Partial<Pick<Record<ToolParamName, string>, "server_name" | "tool_name" | "arguments">>
}
export interface AccessMcpResourceToolUse extends ToolUse {
name: "access_mcp_resource"
params: Partial<Pick<Record<ToolParamName, string>, "server_name" | "uri">>
}
export interface AskFollowupQuestionToolUse extends ToolUse {
name: "ask_followup_question"
params: Partial<Pick<Record<ToolParamName, string>, "question">>

View File

@@ -28,6 +28,9 @@ Otherwise, if you have not completed the task and do not need additional informa
missingToolParameterError: (paramName: string) =>
`Missing value for required parameter '${paramName}'. Please retry with complete response.\n\n${toolUseInstructionsReminder}`,
invalidMcpToolArgumentError: (serverName: string, toolName: string) =>
`Invalid JSON argument used with ${serverName} for ${toolName}. Please retry with a properly formatted JSON argument.`,
toolResult: (
text: string,
images?: string[],

View File

@@ -1,10 +1,12 @@
import osName from "os-name"
import defaultShell from "default-shell"
import os from "os"
import { McpServer } from "../../shared/mcp"
export const SYSTEM_PROMPT = async (
cwd: string,
supportsComputerUse: boolean,
mcpServers: McpServer[] = [],
) => `You are Cline, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.
====
@@ -135,6 +137,35 @@ Usage:
: ""
}
## use_mcp_tool
Description: Request to use a tool provided by a connected MCP server. Each MCP server can provide multiple tools with different capabilities. Tools have defined input schemas that specify required and optional parameters.
Parameters:
- server_name: (required) The name of the MCP server providing the tool
- tool_name: (required) The name of the tool to execute
- arguments: (required) A JSON object containing the tool's input parameters, following the tool's input schema
Usage:
<use_mcp_tool>
<server_name>server name here</server_name>
<tool_name>tool name here</tool_name>
<arguments>
{
"param1": "value1",
"param2": "value2"
}
</arguments>
</use_mcp_tool>
## 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.
Parameters:
- server_name: (required) The name of the MCP server providing the resource
- uri: (required) The URI identifying the specific resource to access
Usage:
<access_mcp_resource>
<server_name>server name here</server_name>
<uri>resource URI here</uri>
</access_mcp_resource>
## ask_followup_question
Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth.
Parameters:
@@ -188,6 +219,26 @@ Your final result description here
</content>
</write_to_file>
## Example 3: Requesting to use an MCP tool
<use_mcp_tool>
<server_name>weather-server</server_name>
<tool_name>get_forecast</tool_name>
<arguments>
{
"city": "San Francisco",
"days": 5
}
</arguments>
</use_mcp_tool>
## Example 4: Requesting to access an MCP resource
<access_mcp_resource>
<server_name>weather-server</server_name>
<uri>weather://san-francisco/current</uri>
</access_mcp_resource>
# Tool Use Guidelines
1. In <thinking> tags, assess what information you already have and what information you need to proceed with the task.
@@ -209,6 +260,92 @@ It is crucial to proceed step-by-step, waiting for the user's message after each
By waiting for and carefully considering the user's response after each tool use, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.
====
MCP SERVERS
MCP (Model Context Protocol) servers provide additional capabilities through a standardized protocol. Each server can offer tools and resources that extend your abilities.
When a server is connected, you can:
1. Use the server's tools via the use_mcp_tool tool:
<use_mcp_tool>
<server_name>server name here</server_name>
<tool_name>tool name here</tool_name>
<arguments>
{
"param1": "value1",
"param2": "value2"
}
</arguments>
</use_mcp_tool>
2. Access the server's resources via the access_mcp_resource tool:
<access_mcp_resource>
<server_name>server name here</server_name>
<uri>resource URI here</uri>
</access_mcp_resource>
# Guidelines for MCP Usage
- Use one MCP operation per message and wait for confirmation before proceeding
- Handle any errors returned from MCP operations gracefully
# Connected MCP Servers
${
mcpServers.length > 0
? `${mcpServers
.filter((server) => server.status === "connected")
.map((server) => {
const tools =
server.tools
?.map((tool) => {
const schemaStr = tool.inputSchema
? ` Input Schema:
${JSON.stringify(tool.inputSchema, null, 2).split("\n").join("\n ")}`
: ""
return `- ${tool.name}: ${tool.description || "No description provided"}\n${schemaStr}`
})
.join("\n\n") || "No tools available"
const templates = server.resourceTemplates?.length
? server.resourceTemplates
.map(
(template) =>
`- ${template.uriTemplate} (${template.name}): ${template.description || "No description provided"}`,
)
.join("\n")
: "No resource templates available"
const resources = server.resources?.length
? server.resources
.map(
(resource) =>
`- ${resource.uri} (${resource.name}): ${resource.description || "No description provided"}`,
)
.join("\n")
: "No resources available"
return `## Server: ${server.name}
### Available Tools
${tools}
### Available Resources
#### Dynamic Resource Templates
${templates}
#### Static Resources
${resources}
`
})
.join("\n\n")}`
: "No MCP servers currently connected."
}
====
CAPABILITIES
@@ -225,6 +362,7 @@ CAPABILITIES
? "\n- You can use the browser_action tool to interact with websites (including html files and locally running development servers) through a Puppeteer-controlled browser when you feel it is necessary in accomplishing the user's task. This tool is particularly useful for web development tasks as it allows you to launch a browser, navigate to pages, interact with elements through clicks and keyboard input, and capture the results through screenshots and console logs. This tool may be useful at key stages of web development tasks-such as after implementing new features, making substantial changes, when troubleshooting issues, or to verify the result of your work. You can analyze the provided screenshots to ensure correct rendering or identify errors, and review console logs for runtime issues.\n - For example, if asked to add a component to a react website, you might create the necessary files, use execute_command to run the site locally, then use browser_action to launch the browser, navigate to the local server, and verify the component renders & functions correctly before closing the browser."
: ""
}
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
====
@@ -245,7 +383,7 @@ RULES
- The user may provide a file's contents directly in their message, in which case you shouldn't use the read_file tool to get the file contents again since you already have it.
- Your goal is to try to accomplish the user's task, NOT engage in a back and forth conversation.${
supportsComputerUse
? '\n- The user may ask generic non-development tasks, such as "what\'s the latest news" or "look up the weather in San Diego", in which case you might use the browser_action tool to complete the task if it makes sense to do so, rather than trying to create a website or using curl to answer the question.'
? '\n- The user may ask generic non-development tasks, such as "what\'s the latest news" or "look up the weather in San Diego", in which case you might use the browser_action tool to complete the task if it makes sense to do so, rather than trying to create a website or using curl to answer the question. However, if an available MCP server tool or resource can be used instead, you should prefer to use it over browser_action.'
: ""
}
- NEVER end attempt_completion result with a question or request to engage in further conversation! Formulate the end of your result in a way that is final and does not require further input from the user.
@@ -254,6 +392,7 @@ RULES
- At the end of each user message, you will automatically receive environment_details. This information is not written by the user themselves, but is auto-generated to provide potentially relevant context about the project structure and environment. While this information can be valuable for understanding the project context, do not treat it as a direct part of the user's request or response. Use it to inform your actions and decisions, but don't assume the user is explicitly asking about or referring to this information unless they clearly do so in their message. When using environment_details, explain your actions clearly to ensure the user understands, as they may not be aware of these details.
- Before executing commands, check the "Actively Running Terminals" section in environment_details. If present, consider how these active processes might impact your task. For example, if a local development server is already running, you wouldn't need to start it again. If no active terminals are listed, proceed with command execution as normal.
- When using the write_to_file tool, ALWAYS provide the COMPLETE file content in your response. This is NON-NEGOTIABLE. Partial updates or placeholders like '// rest of code unchanged' are STRICTLY FORBIDDEN. You MUST include ALL parts of the file, even if they haven't been modified. Failure to do so will result in incomplete or broken code, severely impacting the user's project.
- MCP operations should be used one at a time, similar to other tool usage. Wait for confirmation of success before proceeding with additional operations.
- It is critical you wait for the user's response after each tool use, in order to confirm the success of the tool use. For example, if asked to make a todo app, you would create a file, wait for the user's response it was created successfully, then create another file if needed, wait for the user's response it was created successfully, etc.${
supportsComputerUse
? " Then if you want to test your work, you might use browser_action to launch the site, wait for the user's response confirming the site was launched along with a screenshot, then perhaps e.g., click a button to test functionality if needed, wait for the user's response confirming the button was clicked along with a screenshot of the new state, before finally closing the browser."

View File

@@ -588,12 +588,18 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// await this.postMessageToWebview({ type: "action", action: "settingsButtonClicked" }) // bad ux if user is on welcome
}
async ensureCacheDirectoryExists(): Promise<string> {
private async ensureCacheDirectoryExists(): Promise<string> {
const cacheDir = path.join(this.context.globalStorageUri.fsPath, "cache")
await fs.mkdir(cacheDir, { recursive: true })
return cacheDir
}
async ensureSettingsDirectoryExists(): Promise<string> {
const settingsDir = path.join(this.context.globalStorageUri.fsPath, "settings")
await fs.mkdir(settingsDir, { recursive: true })
return settingsDir
}
async readOpenRouterModels(): Promise<Record<string, ModelInfo> | undefined> {
const openRouterModelsFilePath = path.join(
await this.ensureCacheDirectoryExists(),

View File

@@ -1,13 +1,26 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { StdioClientTransport, StdioServerParameters } from "@modelcontextprotocol/sdk/client/stdio.js"
import { ListResourcesResultSchema, ListToolsResultSchema } from "@modelcontextprotocol/sdk/types.js"
import {
ListResourcesResultSchema,
ListToolsResultSchema,
ListResourceTemplatesResultSchema,
ReadResourceResultSchema,
CallToolResultSchema,
} from "@modelcontextprotocol/sdk/types.js"
import deepEqual from "fast-deep-equal"
import * as fs from "fs/promises"
import * as path from "path"
import * as vscode from "vscode"
import { z } from "zod"
import { ClineProvider, GlobalFileNames } from "../../core/webview/ClineProvider"
import { McpResource, McpServer, McpTool } from "../../shared/mcp"
import {
McpResource,
McpResourceResponse,
McpResourceTemplate,
McpServer,
McpTool,
McpToolCallResponse,
} from "../../shared/mcp"
import { fileExistsAtPath } from "../../utils/fs"
import { arePathsEqual } from "../../utils/path"
@@ -46,7 +59,10 @@ export class McpHub {
if (!provider) {
throw new Error("Provider not available")
}
const mcpSettingsFilePath = path.join(await provider.ensureCacheDirectoryExists(), GlobalFileNames.mcpSettings)
const mcpSettingsFilePath = path.join(
await provider.ensureSettingsDirectoryExists(),
GlobalFileNames.mcpSettings,
)
const fileExists = await fileExistsAtPath(mcpSettingsFilePath)
if (!fileExists) {
await fs.writeFile(
@@ -177,9 +193,32 @@ export class McpHub {
this.connections.push(connection)
connection.server.status = "connected"
// After successful connection, fetch tools and resources
connection.server.tools = await this.fetchTools(name)
connection.server.resources = await this.fetchResources(name)
// // Set up notification handlers
// client.setNotificationHandler(
// // @ts-ignore-next-line
// { method: "notifications/tools/list_changed" },
// async () => {
// console.log(`Tools changed for server: ${name}`)
// connection.server.tools = await this.fetchTools(name)
// await this.notifyWebviewOfServerChanges()
// },
// )
// client.setNotificationHandler(
// // @ts-ignore-next-line
// { method: "notifications/resources/list_changed" },
// async () => {
// console.log(`Resources changed for server: ${name}`)
// connection.server.resources = await this.fetchResources(name)
// connection.server.resourceTemplates = await this.fetchResourceTemplates(name)
// await this.notifyWebviewOfServerChanges()
// },
// )
// Initial fetch of tools and resources
connection.server.tools = await this.fetchToolsList(name)
connection.server.resources = await this.fetchResourcesList(name)
connection.server.resourceTemplates = await this.fetchResourceTemplatesList(name)
await this.notifyWebviewOfServerChanges()
} catch (error) {
@@ -194,7 +233,7 @@ export class McpHub {
}
}
private async fetchTools(serverName: string): Promise<McpTool[]> {
private async fetchToolsList(serverName: string): Promise<McpTool[]> {
try {
const response = await this.connections
.find((conn) => conn.server.name === serverName)
@@ -206,7 +245,7 @@ export class McpHub {
}
}
private async fetchResources(serverName: string): Promise<McpResource[]> {
private async fetchResourcesList(serverName: string): Promise<McpResource[]> {
try {
const response = await this.connections
.find((conn) => conn.server.name === serverName)
@@ -218,10 +257,24 @@ export class McpHub {
}
}
private async fetchResourceTemplatesList(serverName: string): Promise<McpResourceTemplate[]> {
try {
const response = await this.connections
.find((conn) => conn.server.name === serverName)
?.client.request({ method: "resources/templates/list" }, ListResourceTemplatesResultSchema)
return response?.resourceTemplates || []
} catch (error) {
console.error(`Failed to fetch resource templates for ${serverName}:`, error)
return []
}
}
async deleteConnection(name: string): Promise<void> {
const connection = this.connections.find((conn) => conn.server.name === name)
if (connection) {
try {
// connection.client.removeNotificationHandler("notifications/tools/list_changed")
// connection.client.removeNotificationHandler("notifications/resources/list_changed")
await connection.transport.close()
await connection.client.close()
} catch (error) {
@@ -297,6 +350,45 @@ export class McpHub {
})
}
// Using server
async readResource(serverName: string, uri: string): Promise<McpResourceResponse> {
const connection = this.connections.find((conn) => conn.server.name === serverName)
if (!connection) {
throw new Error(`No connection found for server: ${serverName}`)
}
return await connection.client.request(
{
method: "resources/read",
params: {
uri,
},
},
ReadResourceResultSchema,
)
}
async callTool(
serverName: string,
toolName: string,
toolArguments?: Record<string, unknown>,
): Promise<McpToolCallResponse> {
const connection = this.connections.find((conn) => conn.server.name === serverName)
if (!connection) {
throw new Error(`No connection found for server: ${serverName}`)
}
return await connection.client.request(
{
method: "tools/call",
params: {
name: toolName,
arguments: toolArguments,
},
},
CallToolResultSchema,
)
}
async dispose(): Promise<void> {
for (const connection of this.connections) {
try {

View File

@@ -68,6 +68,7 @@ export type ClineAsk =
| "resume_completed_task"
| "mistake_limit_reached"
| "browser_action_launch"
| "use_mcp_server"
export type ClineSay =
| "task"
@@ -84,6 +85,8 @@ export type ClineSay =
| "shell_integration_warning"
| "browser_action"
| "browser_action_result"
| "mcp_server_request_started"
| "mcp_server_response"
export interface ClineSayTool {
tool:
@@ -118,6 +121,14 @@ export type BrowserActionResult = {
currentMousePosition?: string
}
export interface ClineAskUseMcpServer {
serverName: string
type: "use_mcp_tool" | "access_mcp_resource"
toolName?: string
arguments?: string
uri?: string
}
export interface ClineApiReqInfo {
request?: string
tokensIn?: number

View File

@@ -5,6 +5,7 @@ export type McpServer = {
error?: string
tools?: McpTool[]
resources?: McpResource[]
resourceTemplates?: McpResourceTemplate[]
}
export type McpTool = {
@@ -19,3 +20,45 @@ export type McpResource = {
mimeType?: string
description?: string
}
export type McpResourceTemplate = {
uriTemplate: string
name: string
description?: string
mimeType?: string
}
export type McpResourceResponse = {
_meta?: Record<string, any>
contents: Array<{
uri: string
mimeType?: string
text?: string
blob?: string
}>
}
export type McpToolCallResponse = {
_meta?: Record<string, any>
content: Array<
| {
type: "text"
text: string
}
| {
type: "image"
data: string
mimeType: string
}
| {
type: "resource"
resource: {
uri: string
mimeType?: string
text?: string
blob?: string
}
}
>
isError?: boolean
}

View File

@@ -2,13 +2,22 @@ import { VSCodeBadge, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/reac
import deepEqual from "fast-deep-equal"
import React, { memo, useEffect, useMemo, useRef } from "react"
import { useSize } from "react-use"
import { ClineApiReqInfo, ClineMessage, ClineSayTool } from "../../../../src/shared/ExtensionMessage"
import {
ClineApiReqInfo,
ClineAskUseMcpServer,
ClineMessage,
ClineSayTool,
} from "../../../../src/shared/ExtensionMessage"
import { COMMAND_OUTPUT_STRING } from "../../../../src/shared/combineCommandSequences"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { findMatchingResourceOrTemplate } from "../../utils/mcp"
import { vscode } from "../../utils/vscode"
import CodeAccordian, { removeLeadingNonAlphanumeric } from "../common/CodeAccordian"
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
import MarkdownBlock from "../common/MarkdownBlock"
import Thumbnails from "../common/Thumbnails"
import McpResourceRow from "../mcp/McpResourceRow"
import McpToolRow from "../mcp/McpToolRow"
import { highlightMentions } from "./TaskHeader"
interface ChatRowProps {
@@ -67,6 +76,7 @@ export const ChatRowContent = ({
lastModifiedMessage,
isLast,
}: ChatRowContentProps) => {
const { mcpServers } = useExtensionState()
const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => {
if (message.text != null && message.say === "api_req_started") {
const info: ClineApiReqInfo = JSON.parse(message.text)
@@ -81,6 +91,9 @@ export const ChatRowContent = ({
: undefined
const isCommandExecuting =
isLast && lastModifiedMessage?.ask === "command" && lastModifiedMessage?.text?.includes(COMMAND_OUTPUT_STRING)
const isMcpServerResponding = isLast && lastModifiedMessage?.say === "mcp_server_request_started"
const type = message.type === "ask" ? message.ask : message.say
const normalColor = "var(--vscode-foreground)"
@@ -117,6 +130,20 @@ export const ChatRowContent = ({
Cline wants to execute this command:
</span>,
]
case "use_mcp_server":
const mcpServerUse = JSON.parse(message.text || "{}") as ClineAskUseMcpServer
return [
isMcpServerResponding ? (
<ProgressIndicator />
) : (
<span
className="codicon codicon-server"
style={{ color: normalColor, marginBottom: "-1.5px" }}></span>
),
<span style={{ color: normalColor, fontWeight: "bold" }}>
Cline wants to use the <code>{mcpServerUse.serverName}</code> MCP server:
</span>,
]
case "completion_result":
return [
<span
@@ -181,7 +208,15 @@ export const ChatRowContent = ({
default:
return [null, null]
}
}, [type, cost, apiRequestFailedMessage, isCommandExecuting, apiReqCancelReason])
}, [
type,
cost,
apiRequestFailedMessage,
isCommandExecuting,
apiReqCancelReason,
isMcpServerResponding,
message.text,
])
const headerStyle: React.CSSProperties = {
display: "flex",
@@ -617,6 +652,23 @@ export const ChatRowContent = ({
</div>
</>
)
case "mcp_server_response":
return (
<>
<div style={{ paddingTop: 0 }}>
<div
style={{
marginBottom: "4px",
opacity: 0.8,
fontSize: "12px",
textTransform: "uppercase",
}}>
Response
</div>
<CodeBlock source={`${"```"}json\n${message.text}\n${"```"}`} />
</div>
</>
)
default:
return (
<>
@@ -716,6 +768,73 @@ export const ChatRowContent = ({
</div>
</>
)
case "use_mcp_server":
const useMcpServer = JSON.parse(message.text || "{}") as ClineAskUseMcpServer
const server = mcpServers.find((server) => server.name === useMcpServer.serverName)
return (
<>
<div style={headerStyle}>
{icon}
{title}
</div>
<div
style={{
background: "var(--vscode-textCodeBlock-background)",
borderRadius: "3px",
padding: "8px 10px",
marginTop: "8px",
}}>
{useMcpServer.type === "access_mcp_resource" && (
<McpResourceRow
item={{
// Always use the actual URI from the request
uri: useMcpServer.uri || "",
// Use the matched resource/template details, with fallbacks
...(findMatchingResourceOrTemplate(
useMcpServer.uri || "",
server?.resources,
server?.resourceTemplates,
) || {
name: "",
mimeType: "",
description: "",
}),
}}
/>
)}
{useMcpServer.type === "use_mcp_tool" && (
<>
<McpToolRow
tool={{
name: useMcpServer.toolName || "",
description:
server?.tools?.find((tool) => tool.name === useMcpServer.toolName)
?.description || "",
}}
/>
{useMcpServer.arguments && (
<div style={{ marginTop: "6px" }}>
<div
style={{
marginBottom: "4px",
opacity: 0.8,
fontSize: "11px",
textTransform: "uppercase",
}}>
Arguments
</div>
<CodeBlock
source={`${"```"}json\n${useMcpServer.arguments}\n${"```"}`}
/>
</div>
)}
</>
)}
</div>
</>
)
case "completion_result":
if (message.text) {
return (

View File

@@ -133,6 +133,13 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setPrimaryButtonText("Proceed While Running")
setSecondaryButtonText(undefined)
break
case "use_mcp_server":
setTextAreaDisabled(isPartial)
setClineAsk("use_mcp_server")
setEnableButtons(!isPartial)
setPrimaryButtonText("Approve")
setSecondaryButtonText("Reject")
break
case "completion_result":
// extension waiting for feedback. but we can just present a new task button
setTextAreaDisabled(isPartial)
@@ -179,6 +186,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
case "browser_action":
case "browser_action_result":
case "command_output":
case "mcp_server_request_started":
case "mcp_server_response":
case "completion_result":
case "tool":
break
@@ -247,6 +256,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
case "browser_action_launch":
case "command": // user can provide feedback to a tool or command use
case "command_output": // user can send input to command stdin
case "use_mcp_server":
case "completion_result": // if this happens then the user has feedback for the completion result
case "resume_task":
case "resume_completed_task":
@@ -288,6 +298,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
case "command_output":
case "tool":
case "browser_action_launch":
case "use_mcp_server":
case "resume_task":
case "mistake_limit_reached":
vscode.postMessage({ type: "askResponse", askResponse: "yesButtonClicked" })
@@ -321,6 +332,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
case "command":
case "tool":
case "browser_action_launch":
case "use_mcp_server":
// responds to the API with a "This operation failed" and lets it try again
vscode.postMessage({ type: "askResponse", askResponse: "noButtonClicked" })
break
@@ -436,6 +448,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
return false
}
break
case "mcp_server_request_started":
return false
}
return true
})

View File

@@ -0,0 +1,62 @@
import { McpResource, McpResourceTemplate } from "../../../../src/shared/mcp"
type McpResourceRowProps = {
item: McpResource | McpResourceTemplate
}
const McpResourceRow = ({ item }: McpResourceRowProps) => {
const isTemplate = "uriTemplate" in item
const uri = isTemplate ? item.uriTemplate : item.uri
return (
<div
key={uri}
style={{
padding: "8px 0",
}}>
<div
style={{
display: "flex",
alignItems: "center",
marginBottom: "4px",
}}>
<span
className={`codicon codicon-symbol-${isTemplate ? "template" : "file"}`}
style={{ marginRight: "6px" }}
/>
<span style={{ fontWeight: 500, wordBreak: "break-all" }}>{uri}</span>
</div>
<div
style={{
fontSize: "12px",
opacity: 0.8,
margin: "4px 0",
}}>
{item.name && item.description
? `${item.name}: ${item.description}`
: !item.name && item.description
? item.description
: !item.description && item.name
? item.name
: "No description"}
</div>
<div
style={{
fontSize: "12px",
}}>
<span style={{ opacity: 0.8 }}>Returns </span>
<code
style={{
color: "var(--vscode-textPreformat-foreground)",
background: "var(--vscode-textPreformat-background)",
padding: "1px 4px",
borderRadius: "3px",
}}>
{item.mimeType || "Unknown"}
</code>
</div>
</div>
)
}
export default McpResourceRow

View File

@@ -0,0 +1,75 @@
import { McpTool } from "../../../../src/shared/mcp"
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
type McpToolRowProps = {
tool: McpTool
}
const McpToolRow = ({ tool }: McpToolRowProps) => {
return (
<div
key={tool.name}
style={{
padding: "8px 0",
}}>
<div style={{ display: "flex" }}>
<span className="codicon codicon-symbol-method" style={{ marginRight: "6px" }}></span>
<span style={{ fontWeight: 500 }}>{tool.name}</span>
</div>
{tool.description && (
<div
style={{
marginLeft: "0px",
marginTop: "4px",
opacity: 0.8,
fontSize: "12px",
}}>
{tool.description}
</div>
)}
{tool.inputSchema && "properties" in tool.inputSchema && (
<div
style={{
marginTop: "8px",
fontSize: "12px",
background: CODE_BLOCK_BG_COLOR,
borderRadius: "3px",
padding: "8px",
}}>
<div style={{ marginBottom: "4px", opacity: 0.8, fontSize: "11px", textTransform: "uppercase" }}>
Parameters
</div>
{Object.entries(tool.inputSchema.properties as Record<string, any>).map(([paramName, schema]) => {
const isRequired =
tool.inputSchema &&
"required" in tool.inputSchema &&
Array.isArray(tool.inputSchema.required) &&
tool.inputSchema.required.includes(paramName)
return (
<div
key={paramName}
style={{
display: "flex",
alignItems: "baseline",
marginTop: "4px",
}}>
<code
style={{
color: "var(--vscode-textPreformat-foreground)",
marginRight: "8px",
}}>
{paramName}
{isRequired && <span style={{ color: "var(--vscode-errorForeground)" }}>*</span>}
</code>
<span style={{ opacity: 0.8 }}>{schema.description || "No description"}</span>
</div>
)
})}
</div>
)}
</div>
)
}
export default McpToolRow

View File

@@ -3,6 +3,8 @@ import { useState } from "react"
import { vscode } from "../../utils/vscode"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { McpServer } from "../../../../src/shared/mcp"
import McpToolRow from "./McpToolRow"
import McpResourceRow from "./McpResourceRow"
type McpViewProps = {
onDone: () => void
@@ -162,7 +164,7 @@ const ServerRow = ({ server }: { server: McpServer }) => {
display: "flex",
alignItems: "center",
padding: "8px",
background: "var(--vscode-list-hoverBackground)",
background: "var(--vscode-textCodeBlock-background)",
cursor: server.error ? "default" : "pointer",
borderRadius: isExpanded || server.error ? "4px 4px 0 0" : "4px",
}}
@@ -190,7 +192,7 @@ const ServerRow = ({ server }: { server: McpServer }) => {
style={{
padding: "8px",
fontSize: "13px",
background: "var(--vscode-list-hoverBackground)",
background: "var(--vscode-textCodeBlock-background)",
borderRadius: "0 0 4px 4px",
}}>
<div style={{ color: "var(--vscode-testing-iconFailed)", marginBottom: "8px" }}>{server.error}</div>
@@ -203,7 +205,7 @@ const ServerRow = ({ server }: { server: McpServer }) => {
isExpanded && (
<div
style={{
background: "var(--vscode-list-hoverBackground)",
background: "var(--vscode-textCodeBlock-background)",
padding: "0 12px 0 12px",
fontSize: "13px",
borderRadius: "0 0 4px 4px",
@@ -214,29 +216,10 @@ const ServerRow = ({ server }: { server: McpServer }) => {
<VSCodePanelView id="tools-view">
{server.tools && server.tools.length > 0 ? (
<div style={{ display: "flex", flexDirection: "column", gap: "3px" }}>
<div
style={{ display: "flex", flexDirection: "column", gap: "3px", width: "100%" }}>
{server.tools.map((tool) => (
<div
key={tool.name}
style={{
padding: "8px 0",
}}>
<div style={{ display: "flex" }}>
<span
className="codicon codicon-symbol-method"
style={{ marginRight: "6px" }}></span>
<span style={{ fontWeight: 500 }}>{tool.name}</span>
</div>
<div
style={{
marginLeft: "0px",
marginTop: "4px",
opacity: 0.8,
fontSize: "12px",
}}>
{tool.description}
</div>
</div>
<McpToolRow key={tool.name} tool={tool} />
))}
</div>
) : (
@@ -246,35 +229,19 @@ const ServerRow = ({ server }: { server: McpServer }) => {
)}
</VSCodePanelView>
{/* Resources Panel View */}
<VSCodePanelView id="resources-view">
{server.resources && server.resources.length > 0 ? (
<div style={{ display: "flex", flexDirection: "column", gap: "3px" }}>
{server.resources.map((resource) => (
<div
key={resource.uri}
style={{
padding: "8px 0",
}}>
<div style={{ display: "flex" }}>
<span
className="codicon codicon-symbol-file"
style={{ marginRight: "6px" }}></span>
<span style={{ fontWeight: 500 }}>{resource.name}</span>
</div>
<div style={{ marginTop: "6px", fontSize: "12px" }}>
<code
style={{
color: "var(--vscode-textPreformat-foreground)",
background: "var(--vscode-textPreformat-background)",
padding: "2px 4px",
borderRadius: "3px",
}}>
{resource.uri}
</code>
</div>
</div>
))}
{(server.resources && server.resources.length > 0) ||
(server.resourceTemplates && server.resourceTemplates.length > 0) ? (
<div
style={{ display: "flex", flexDirection: "column", gap: "3px", width: "100%" }}>
{[...(server.resourceTemplates || []), ...(server.resources || [])].map(
(item) => (
<McpResourceRow
key={"uriTemplate" in item ? item.uriTemplate : item.uri}
item={item}
/>
),
)}
</div>
) : (
<div style={{ padding: "10px 0", color: "var(--vscode-descriptionForeground)" }}>

View File

@@ -0,0 +1,47 @@
import { McpResource, McpResourceTemplate } from "../../../src/shared/mcp"
/**
* Matches a URI against an array of URI templates and returns the matching template
* @param uri The URI to match
* @param templates Array of URI templates to match against
* @returns The matching template or undefined if no match is found
*/
export function findMatchingTemplate(
uri: string,
templates: McpResourceTemplate[] = [],
): McpResourceTemplate | undefined {
return templates.find((template) => {
// Convert template to regex pattern
const pattern = template.uriTemplate
// Replace {param} with ([^/]+) to match any non-slash characters
.replace(/\{([^}]+)\}/g, "([^/]+)")
// Escape special regex characters except the ones we just added
.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
// Un-escape the capturing groups we added
.replace(/\\\(/g, "(")
.replace(/\\\)/g, ")")
const regex = new RegExp(`^${pattern}$`)
return regex.test(uri)
})
}
/**
* Finds either an exact resource match or a matching template for a given URI
* @param uri The URI to find a match for
* @param resources Array of concrete resources
* @param templates Array of resource templates
* @returns The matching resource, template, or undefined
*/
export function findMatchingResourceOrTemplate(
uri: string,
resources: McpResource[] = [],
templates: McpResourceTemplate[] = [],
): McpResource | McpResourceTemplate | undefined {
// First try to find an exact resource match
const exactMatch = resources.find((resource) => resource.uri === uri)
if (exactMatch) return exactMatch
// If no exact match, try to find a matching template
return findMatchingTemplate(uri, templates)
}