mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-21 12:51:17 -05:00
Add use_mcp_tool and access_mcp_resource tools
This commit is contained in:
@@ -28,6 +28,7 @@ import {
|
|||||||
ClineApiReqCancelReason,
|
ClineApiReqCancelReason,
|
||||||
ClineApiReqInfo,
|
ClineApiReqInfo,
|
||||||
ClineAsk,
|
ClineAsk,
|
||||||
|
ClineAskUseMcpServer,
|
||||||
ClineMessage,
|
ClineMessage,
|
||||||
ClineSay,
|
ClineSay,
|
||||||
ClineSayBrowserAction,
|
ClineSayBrowserAction,
|
||||||
@@ -757,7 +758,7 @@ export class Cline {
|
|||||||
})
|
})
|
||||||
const mcpServers = this.providerRef.deref()?.mcpHub?.connections.map((conn) => conn.server)
|
const mcpServers = this.providerRef.deref()?.mcpHub?.connections.map((conn) => conn.server)
|
||||||
console.log("mcpServers for system prompt:", JSON.stringify(mcpServers, null, 2))
|
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()) {
|
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>
|
// 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)
|
systemPrompt += addCustomInstructions(this.customInstructions)
|
||||||
@@ -898,6 +899,10 @@ export class Cline {
|
|||||||
return `[${block.name} for '${block.params.path}']`
|
return `[${block.name} for '${block.params.path}']`
|
||||||
case "browser_action":
|
case "browser_action":
|
||||||
return `[${block.name} for '${block.params.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":
|
case "ask_followup_question":
|
||||||
return `[${block.name} for '${block.params.question}']`
|
return `[${block.name} for '${block.params.question}']`
|
||||||
case "attempt_completion":
|
case "attempt_completion":
|
||||||
@@ -1544,7 +1549,163 @@ export class Cline {
|
|||||||
break
|
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": {
|
case "ask_followup_question": {
|
||||||
const question: string | undefined = block.params.question
|
const question: string | undefined = block.params.question
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export const toolUseNames = [
|
|||||||
"list_files",
|
"list_files",
|
||||||
"list_code_definition_names",
|
"list_code_definition_names",
|
||||||
"browser_action",
|
"browser_action",
|
||||||
|
"use_mcp_tool",
|
||||||
|
"access_mcp_resource",
|
||||||
"ask_followup_question",
|
"ask_followup_question",
|
||||||
"attempt_completion",
|
"attempt_completion",
|
||||||
] as const
|
] as const
|
||||||
@@ -34,6 +36,10 @@ export const toolParamNames = [
|
|||||||
"url",
|
"url",
|
||||||
"coordinate",
|
"coordinate",
|
||||||
"text",
|
"text",
|
||||||
|
"server_name",
|
||||||
|
"tool_name",
|
||||||
|
"arguments",
|
||||||
|
"uri",
|
||||||
"question",
|
"question",
|
||||||
"result",
|
"result",
|
||||||
] as const
|
] as const
|
||||||
@@ -84,6 +90,16 @@ export interface BrowserActionToolUse extends ToolUse {
|
|||||||
params: Partial<Pick<Record<ToolParamName, string>, "action" | "url" | "coordinate" | "text">>
|
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 {
|
export interface AskFollowupQuestionToolUse extends ToolUse {
|
||||||
name: "ask_followup_question"
|
name: "ask_followup_question"
|
||||||
params: Partial<Pick<Record<ToolParamName, string>, "question">>
|
params: Partial<Pick<Record<ToolParamName, string>, "question">>
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ Otherwise, if you have not completed the task and do not need additional informa
|
|||||||
missingToolParameterError: (paramName: string) =>
|
missingToolParameterError: (paramName: string) =>
|
||||||
`Missing value for required parameter '${paramName}'. Please retry with complete response.\n\n${toolUseInstructionsReminder}`,
|
`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: (
|
toolResult: (
|
||||||
text: string,
|
text: string,
|
||||||
images?: string[],
|
images?: string[],
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import osName from "os-name"
|
import osName from "os-name"
|
||||||
import defaultShell from "default-shell"
|
import defaultShell from "default-shell"
|
||||||
import os from "os"
|
import os from "os"
|
||||||
|
import { McpServer } from "../../shared/mcp"
|
||||||
|
|
||||||
export const SYSTEM_PROMPT = async (
|
export const SYSTEM_PROMPT = async (
|
||||||
cwd: string,
|
cwd: string,
|
||||||
supportsComputerUse: boolean,
|
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.
|
) => `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
|
## 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.
|
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:
|
Parameters:
|
||||||
@@ -188,6 +219,26 @@ Your final result description here
|
|||||||
</content>
|
</content>
|
||||||
</write_to_file>
|
</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
|
# Tool Use Guidelines
|
||||||
|
|
||||||
1. In <thinking> tags, assess what information you already have and what information you need to proceed with the task.
|
1. In <thinking> tags, assess what information you already have and what information you need to proceed with the task.
|
||||||
@@ -211,6 +262,92 @@ By waiting for and carefully considering the user's response after each tool use
|
|||||||
|
|
||||||
====
|
====
|
||||||
|
|
||||||
|
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
|
CAPABILITIES
|
||||||
|
|
||||||
- You have access to tools that let you execute CLI commands on the user's computer, list files, view source code definitions, regex search${
|
- You have access to tools that let you execute CLI commands on the user's computer, list files, view source code definitions, regex search${
|
||||||
@@ -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."
|
? "\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.
|
- 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.${
|
- Your goal is to try to accomplish the user's task, NOT engage in a back and forth conversation.${
|
||||||
supportsComputerUse
|
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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.${
|
- 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
|
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."
|
? " 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."
|
||||||
|
|||||||
@@ -588,12 +588,18 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
// await this.postMessageToWebview({ type: "action", action: "settingsButtonClicked" }) // bad ux if user is on welcome
|
// 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")
|
const cacheDir = path.join(this.context.globalStorageUri.fsPath, "cache")
|
||||||
await fs.mkdir(cacheDir, { recursive: true })
|
await fs.mkdir(cacheDir, { recursive: true })
|
||||||
return cacheDir
|
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> {
|
async readOpenRouterModels(): Promise<Record<string, ModelInfo> | undefined> {
|
||||||
const openRouterModelsFilePath = path.join(
|
const openRouterModelsFilePath = path.join(
|
||||||
await this.ensureCacheDirectoryExists(),
|
await this.ensureCacheDirectoryExists(),
|
||||||
|
|||||||
@@ -1,13 +1,26 @@
|
|||||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
|
||||||
import { StdioClientTransport, StdioServerParameters } from "@modelcontextprotocol/sdk/client/stdio.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 deepEqual from "fast-deep-equal"
|
||||||
import * as fs from "fs/promises"
|
import * as fs from "fs/promises"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import * as vscode from "vscode"
|
import * as vscode from "vscode"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { ClineProvider, GlobalFileNames } from "../../core/webview/ClineProvider"
|
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 { fileExistsAtPath } from "../../utils/fs"
|
||||||
import { arePathsEqual } from "../../utils/path"
|
import { arePathsEqual } from "../../utils/path"
|
||||||
|
|
||||||
@@ -46,7 +59,10 @@ export class McpHub {
|
|||||||
if (!provider) {
|
if (!provider) {
|
||||||
throw new Error("Provider not available")
|
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)
|
const fileExists = await fileExistsAtPath(mcpSettingsFilePath)
|
||||||
if (!fileExists) {
|
if (!fileExists) {
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
@@ -177,9 +193,32 @@ export class McpHub {
|
|||||||
this.connections.push(connection)
|
this.connections.push(connection)
|
||||||
connection.server.status = "connected"
|
connection.server.status = "connected"
|
||||||
|
|
||||||
// After successful connection, fetch tools and resources
|
// // Set up notification handlers
|
||||||
connection.server.tools = await this.fetchTools(name)
|
// client.setNotificationHandler(
|
||||||
connection.server.resources = await this.fetchResources(name)
|
// // @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()
|
await this.notifyWebviewOfServerChanges()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -194,7 +233,7 @@ export class McpHub {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchTools(serverName: string): Promise<McpTool[]> {
|
private async fetchToolsList(serverName: string): Promise<McpTool[]> {
|
||||||
try {
|
try {
|
||||||
const response = await this.connections
|
const response = await this.connections
|
||||||
.find((conn) => conn.server.name === serverName)
|
.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 {
|
try {
|
||||||
const response = await this.connections
|
const response = await this.connections
|
||||||
.find((conn) => conn.server.name === serverName)
|
.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> {
|
async deleteConnection(name: string): Promise<void> {
|
||||||
const connection = this.connections.find((conn) => conn.server.name === name)
|
const connection = this.connections.find((conn) => conn.server.name === name)
|
||||||
if (connection) {
|
if (connection) {
|
||||||
try {
|
try {
|
||||||
|
// connection.client.removeNotificationHandler("notifications/tools/list_changed")
|
||||||
|
// connection.client.removeNotificationHandler("notifications/resources/list_changed")
|
||||||
await connection.transport.close()
|
await connection.transport.close()
|
||||||
await connection.client.close()
|
await connection.client.close()
|
||||||
} catch (error) {
|
} 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> {
|
async dispose(): Promise<void> {
|
||||||
for (const connection of this.connections) {
|
for (const connection of this.connections) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export type ClineAsk =
|
|||||||
| "resume_completed_task"
|
| "resume_completed_task"
|
||||||
| "mistake_limit_reached"
|
| "mistake_limit_reached"
|
||||||
| "browser_action_launch"
|
| "browser_action_launch"
|
||||||
|
| "use_mcp_server"
|
||||||
|
|
||||||
export type ClineSay =
|
export type ClineSay =
|
||||||
| "task"
|
| "task"
|
||||||
@@ -84,6 +85,8 @@ export type ClineSay =
|
|||||||
| "shell_integration_warning"
|
| "shell_integration_warning"
|
||||||
| "browser_action"
|
| "browser_action"
|
||||||
| "browser_action_result"
|
| "browser_action_result"
|
||||||
|
| "mcp_server_request_started"
|
||||||
|
| "mcp_server_response"
|
||||||
|
|
||||||
export interface ClineSayTool {
|
export interface ClineSayTool {
|
||||||
tool:
|
tool:
|
||||||
@@ -118,6 +121,14 @@ export type BrowserActionResult = {
|
|||||||
currentMousePosition?: string
|
currentMousePosition?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ClineAskUseMcpServer {
|
||||||
|
serverName: string
|
||||||
|
type: "use_mcp_tool" | "access_mcp_resource"
|
||||||
|
toolName?: string
|
||||||
|
arguments?: string
|
||||||
|
uri?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface ClineApiReqInfo {
|
export interface ClineApiReqInfo {
|
||||||
request?: string
|
request?: string
|
||||||
tokensIn?: number
|
tokensIn?: number
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export type McpServer = {
|
|||||||
error?: string
|
error?: string
|
||||||
tools?: McpTool[]
|
tools?: McpTool[]
|
||||||
resources?: McpResource[]
|
resources?: McpResource[]
|
||||||
|
resourceTemplates?: McpResourceTemplate[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type McpTool = {
|
export type McpTool = {
|
||||||
@@ -19,3 +20,45 @@ export type McpResource = {
|
|||||||
mimeType?: string
|
mimeType?: string
|
||||||
description?: 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,13 +2,22 @@ import { VSCodeBadge, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/reac
|
|||||||
import deepEqual from "fast-deep-equal"
|
import deepEqual from "fast-deep-equal"
|
||||||
import React, { memo, useEffect, useMemo, useRef } from "react"
|
import React, { memo, useEffect, useMemo, useRef } from "react"
|
||||||
import { useSize } from "react-use"
|
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 { COMMAND_OUTPUT_STRING } from "../../../../src/shared/combineCommandSequences"
|
||||||
|
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||||
|
import { findMatchingResourceOrTemplate } from "../../utils/mcp"
|
||||||
import { vscode } from "../../utils/vscode"
|
import { vscode } from "../../utils/vscode"
|
||||||
import CodeAccordian, { removeLeadingNonAlphanumeric } from "../common/CodeAccordian"
|
import CodeAccordian, { removeLeadingNonAlphanumeric } from "../common/CodeAccordian"
|
||||||
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
|
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
|
||||||
import MarkdownBlock from "../common/MarkdownBlock"
|
import MarkdownBlock from "../common/MarkdownBlock"
|
||||||
import Thumbnails from "../common/Thumbnails"
|
import Thumbnails from "../common/Thumbnails"
|
||||||
|
import McpResourceRow from "../mcp/McpResourceRow"
|
||||||
|
import McpToolRow from "../mcp/McpToolRow"
|
||||||
import { highlightMentions } from "./TaskHeader"
|
import { highlightMentions } from "./TaskHeader"
|
||||||
|
|
||||||
interface ChatRowProps {
|
interface ChatRowProps {
|
||||||
@@ -67,6 +76,7 @@ export const ChatRowContent = ({
|
|||||||
lastModifiedMessage,
|
lastModifiedMessage,
|
||||||
isLast,
|
isLast,
|
||||||
}: ChatRowContentProps) => {
|
}: ChatRowContentProps) => {
|
||||||
|
const { mcpServers } = useExtensionState()
|
||||||
const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => {
|
const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => {
|
||||||
if (message.text != null && message.say === "api_req_started") {
|
if (message.text != null && message.say === "api_req_started") {
|
||||||
const info: ClineApiReqInfo = JSON.parse(message.text)
|
const info: ClineApiReqInfo = JSON.parse(message.text)
|
||||||
@@ -81,6 +91,9 @@ export const ChatRowContent = ({
|
|||||||
: undefined
|
: undefined
|
||||||
const isCommandExecuting =
|
const isCommandExecuting =
|
||||||
isLast && lastModifiedMessage?.ask === "command" && lastModifiedMessage?.text?.includes(COMMAND_OUTPUT_STRING)
|
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 type = message.type === "ask" ? message.ask : message.say
|
||||||
|
|
||||||
const normalColor = "var(--vscode-foreground)"
|
const normalColor = "var(--vscode-foreground)"
|
||||||
@@ -117,6 +130,20 @@ export const ChatRowContent = ({
|
|||||||
Cline wants to execute this command:
|
Cline wants to execute this command:
|
||||||
</span>,
|
</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":
|
case "completion_result":
|
||||||
return [
|
return [
|
||||||
<span
|
<span
|
||||||
@@ -181,7 +208,15 @@ export const ChatRowContent = ({
|
|||||||
default:
|
default:
|
||||||
return [null, null]
|
return [null, null]
|
||||||
}
|
}
|
||||||
}, [type, cost, apiRequestFailedMessage, isCommandExecuting, apiReqCancelReason])
|
}, [
|
||||||
|
type,
|
||||||
|
cost,
|
||||||
|
apiRequestFailedMessage,
|
||||||
|
isCommandExecuting,
|
||||||
|
apiReqCancelReason,
|
||||||
|
isMcpServerResponding,
|
||||||
|
message.text,
|
||||||
|
])
|
||||||
|
|
||||||
const headerStyle: React.CSSProperties = {
|
const headerStyle: React.CSSProperties = {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -617,6 +652,23 @@ export const ChatRowContent = ({
|
|||||||
</div>
|
</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:
|
default:
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -716,6 +768,73 @@ export const ChatRowContent = ({
|
|||||||
</div>
|
</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":
|
case "completion_result":
|
||||||
if (message.text) {
|
if (message.text) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -133,6 +133,13 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
setPrimaryButtonText("Proceed While Running")
|
setPrimaryButtonText("Proceed While Running")
|
||||||
setSecondaryButtonText(undefined)
|
setSecondaryButtonText(undefined)
|
||||||
break
|
break
|
||||||
|
case "use_mcp_server":
|
||||||
|
setTextAreaDisabled(isPartial)
|
||||||
|
setClineAsk("use_mcp_server")
|
||||||
|
setEnableButtons(!isPartial)
|
||||||
|
setPrimaryButtonText("Approve")
|
||||||
|
setSecondaryButtonText("Reject")
|
||||||
|
break
|
||||||
case "completion_result":
|
case "completion_result":
|
||||||
// extension waiting for feedback. but we can just present a new task button
|
// extension waiting for feedback. but we can just present a new task button
|
||||||
setTextAreaDisabled(isPartial)
|
setTextAreaDisabled(isPartial)
|
||||||
@@ -179,6 +186,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
case "browser_action":
|
case "browser_action":
|
||||||
case "browser_action_result":
|
case "browser_action_result":
|
||||||
case "command_output":
|
case "command_output":
|
||||||
|
case "mcp_server_request_started":
|
||||||
|
case "mcp_server_response":
|
||||||
case "completion_result":
|
case "completion_result":
|
||||||
case "tool":
|
case "tool":
|
||||||
break
|
break
|
||||||
@@ -247,6 +256,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
case "browser_action_launch":
|
case "browser_action_launch":
|
||||||
case "command": // user can provide feedback to a tool or command use
|
case "command": // user can provide feedback to a tool or command use
|
||||||
case "command_output": // user can send input to command stdin
|
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 "completion_result": // if this happens then the user has feedback for the completion result
|
||||||
case "resume_task":
|
case "resume_task":
|
||||||
case "resume_completed_task":
|
case "resume_completed_task":
|
||||||
@@ -288,6 +298,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
case "command_output":
|
case "command_output":
|
||||||
case "tool":
|
case "tool":
|
||||||
case "browser_action_launch":
|
case "browser_action_launch":
|
||||||
|
case "use_mcp_server":
|
||||||
case "resume_task":
|
case "resume_task":
|
||||||
case "mistake_limit_reached":
|
case "mistake_limit_reached":
|
||||||
vscode.postMessage({ type: "askResponse", askResponse: "yesButtonClicked" })
|
vscode.postMessage({ type: "askResponse", askResponse: "yesButtonClicked" })
|
||||||
@@ -321,6 +332,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
case "command":
|
case "command":
|
||||||
case "tool":
|
case "tool":
|
||||||
case "browser_action_launch":
|
case "browser_action_launch":
|
||||||
|
case "use_mcp_server":
|
||||||
// responds to the API with a "This operation failed" and lets it try again
|
// responds to the API with a "This operation failed" and lets it try again
|
||||||
vscode.postMessage({ type: "askResponse", askResponse: "noButtonClicked" })
|
vscode.postMessage({ type: "askResponse", askResponse: "noButtonClicked" })
|
||||||
break
|
break
|
||||||
@@ -436,6 +448,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
case "mcp_server_request_started":
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|||||||
62
webview-ui/src/components/mcp/McpResourceRow.tsx
Normal file
62
webview-ui/src/components/mcp/McpResourceRow.tsx
Normal 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
|
||||||
75
webview-ui/src/components/mcp/McpToolRow.tsx
Normal file
75
webview-ui/src/components/mcp/McpToolRow.tsx
Normal 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
|
||||||
@@ -3,6 +3,8 @@ import { useState } from "react"
|
|||||||
import { vscode } from "../../utils/vscode"
|
import { vscode } from "../../utils/vscode"
|
||||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||||
import { McpServer } from "../../../../src/shared/mcp"
|
import { McpServer } from "../../../../src/shared/mcp"
|
||||||
|
import McpToolRow from "./McpToolRow"
|
||||||
|
import McpResourceRow from "./McpResourceRow"
|
||||||
|
|
||||||
type McpViewProps = {
|
type McpViewProps = {
|
||||||
onDone: () => void
|
onDone: () => void
|
||||||
@@ -162,7 +164,7 @@ const ServerRow = ({ server }: { server: McpServer }) => {
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
padding: "8px",
|
padding: "8px",
|
||||||
background: "var(--vscode-list-hoverBackground)",
|
background: "var(--vscode-textCodeBlock-background)",
|
||||||
cursor: server.error ? "default" : "pointer",
|
cursor: server.error ? "default" : "pointer",
|
||||||
borderRadius: isExpanded || server.error ? "4px 4px 0 0" : "4px",
|
borderRadius: isExpanded || server.error ? "4px 4px 0 0" : "4px",
|
||||||
}}
|
}}
|
||||||
@@ -190,7 +192,7 @@ const ServerRow = ({ server }: { server: McpServer }) => {
|
|||||||
style={{
|
style={{
|
||||||
padding: "8px",
|
padding: "8px",
|
||||||
fontSize: "13px",
|
fontSize: "13px",
|
||||||
background: "var(--vscode-list-hoverBackground)",
|
background: "var(--vscode-textCodeBlock-background)",
|
||||||
borderRadius: "0 0 4px 4px",
|
borderRadius: "0 0 4px 4px",
|
||||||
}}>
|
}}>
|
||||||
<div style={{ color: "var(--vscode-testing-iconFailed)", marginBottom: "8px" }}>{server.error}</div>
|
<div style={{ color: "var(--vscode-testing-iconFailed)", marginBottom: "8px" }}>{server.error}</div>
|
||||||
@@ -203,7 +205,7 @@ const ServerRow = ({ server }: { server: McpServer }) => {
|
|||||||
isExpanded && (
|
isExpanded && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
background: "var(--vscode-list-hoverBackground)",
|
background: "var(--vscode-textCodeBlock-background)",
|
||||||
padding: "0 12px 0 12px",
|
padding: "0 12px 0 12px",
|
||||||
fontSize: "13px",
|
fontSize: "13px",
|
||||||
borderRadius: "0 0 4px 4px",
|
borderRadius: "0 0 4px 4px",
|
||||||
@@ -214,29 +216,10 @@ const ServerRow = ({ server }: { server: McpServer }) => {
|
|||||||
|
|
||||||
<VSCodePanelView id="tools-view">
|
<VSCodePanelView id="tools-view">
|
||||||
{server.tools && server.tools.length > 0 ? (
|
{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) => (
|
{server.tools.map((tool) => (
|
||||||
<div
|
<McpToolRow key={tool.name} tool={tool} />
|
||||||
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>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -246,35 +229,19 @@ const ServerRow = ({ server }: { server: McpServer }) => {
|
|||||||
)}
|
)}
|
||||||
</VSCodePanelView>
|
</VSCodePanelView>
|
||||||
|
|
||||||
{/* Resources Panel View */}
|
|
||||||
<VSCodePanelView id="resources-view">
|
<VSCodePanelView id="resources-view">
|
||||||
{server.resources && server.resources.length > 0 ? (
|
{(server.resources && server.resources.length > 0) ||
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "3px" }}>
|
(server.resourceTemplates && server.resourceTemplates.length > 0) ? (
|
||||||
{server.resources.map((resource) => (
|
<div
|
||||||
<div
|
style={{ display: "flex", flexDirection: "column", gap: "3px", width: "100%" }}>
|
||||||
key={resource.uri}
|
{[...(server.resourceTemplates || []), ...(server.resources || [])].map(
|
||||||
style={{
|
(item) => (
|
||||||
padding: "8px 0",
|
<McpResourceRow
|
||||||
}}>
|
key={"uriTemplate" in item ? item.uriTemplate : item.uri}
|
||||||
<div style={{ display: "flex" }}>
|
item={item}
|
||||||
<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>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ padding: "10px 0", color: "var(--vscode-descriptionForeground)" }}>
|
<div style={{ padding: "10px 0", color: "var(--vscode-descriptionForeground)" }}>
|
||||||
|
|||||||
47
webview-ui/src/utils/mcp.ts
Normal file
47
webview-ui/src/utils/mcp.ts
Normal 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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user