mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-21 21:01:06 -05:00
Incorporate MCP changes (#93)
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
This commit is contained in:
@@ -29,6 +29,7 @@ import {
|
||||
ClineApiReqCancelReason,
|
||||
ClineApiReqInfo,
|
||||
ClineAsk,
|
||||
ClineAskUseMcpServer,
|
||||
ClineMessage,
|
||||
ClineSay,
|
||||
ClineSayBrowserAction,
|
||||
@@ -754,7 +755,17 @@ export class Cline {
|
||||
}
|
||||
|
||||
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
|
||||
const systemPrompt = await SYSTEM_PROMPT(cwd, this.api.getModel().info.supportsComputerUse ?? false, this.diffStrategy) + await addCustomInstructions(this.customInstructions ?? '', cwd)
|
||||
// Wait for MCP servers to be connected before generating system prompt
|
||||
await pWaitFor(() => this.providerRef.deref()?.mcpHub?.isConnecting !== true, { timeout: 10_000 }).catch(() => {
|
||||
console.error("MCP servers failed to connect in time")
|
||||
})
|
||||
|
||||
const mcpHub = this.providerRef.deref()?.mcpHub
|
||||
if (!mcpHub) {
|
||||
throw new Error("MCP hub not available")
|
||||
}
|
||||
|
||||
const systemPrompt = await SYSTEM_PROMPT(cwd, this.api.getModel().info.supportsComputerUse ?? false, mcpHub, this.diffStrategy) + await addCustomInstructions(this.customInstructions ?? '', cwd)
|
||||
|
||||
// If the previous API request's total token usage is close to the context window, truncate the conversation history to free up space for the new request
|
||||
if (previousApiReqIndex >= 0) {
|
||||
@@ -893,6 +904,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":
|
||||
@@ -1634,7 +1649,162 @@ 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:\n" : "") +
|
||||
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") || "(No 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 {
|
||||
|
||||
@@ -3,6 +3,44 @@ import { ClineProvider } from '../webview/ClineProvider';
|
||||
import { ApiConfiguration } from '../../shared/api';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
// Mock all MCP-related modules
|
||||
jest.mock('@modelcontextprotocol/sdk/types.js', () => ({
|
||||
CallToolResultSchema: {},
|
||||
ListResourcesResultSchema: {},
|
||||
ListResourceTemplatesResultSchema: {},
|
||||
ListToolsResultSchema: {},
|
||||
ReadResourceResultSchema: {},
|
||||
ErrorCode: {
|
||||
InvalidRequest: 'InvalidRequest',
|
||||
MethodNotFound: 'MethodNotFound',
|
||||
InternalError: 'InternalError'
|
||||
},
|
||||
McpError: class McpError extends Error {
|
||||
code: string;
|
||||
constructor(code: string, message: string) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
this.name = 'McpError';
|
||||
}
|
||||
}
|
||||
}), { virtual: true });
|
||||
|
||||
jest.mock('@modelcontextprotocol/sdk/client/index.js', () => ({
|
||||
Client: jest.fn().mockImplementation(() => ({
|
||||
connect: jest.fn().mockResolvedValue(undefined),
|
||||
close: jest.fn().mockResolvedValue(undefined),
|
||||
listTools: jest.fn().mockResolvedValue({ tools: [] }),
|
||||
callTool: jest.fn().mockResolvedValue({ content: [] })
|
||||
}))
|
||||
}), { virtual: true });
|
||||
|
||||
jest.mock('@modelcontextprotocol/sdk/client/stdio.js', () => ({
|
||||
StdioClientTransport: jest.fn().mockImplementation(() => ({
|
||||
connect: jest.fn().mockResolvedValue(undefined),
|
||||
close: jest.fn().mockResolvedValue(undefined)
|
||||
}))
|
||||
}), { virtual: true });
|
||||
|
||||
// Mock fileExistsAtPath
|
||||
jest.mock('../../utils/fs', () => ({
|
||||
fileExistsAtPath: jest.fn().mockImplementation((filePath) => {
|
||||
@@ -85,7 +123,11 @@ jest.mock('vscode', () => {
|
||||
}],
|
||||
onDidCreateFiles: jest.fn(() => mockDisposable),
|
||||
onDidDeleteFiles: jest.fn(() => mockDisposable),
|
||||
onDidRenameFiles: jest.fn(() => mockDisposable)
|
||||
onDidRenameFiles: jest.fn(() => mockDisposable),
|
||||
onDidSaveTextDocument: jest.fn(() => mockDisposable),
|
||||
onDidChangeTextDocument: jest.fn(() => mockDisposable),
|
||||
onDidOpenTextDocument: jest.fn(() => mockDisposable),
|
||||
onDidCloseTextDocument: jest.fn(() => mockDisposable)
|
||||
},
|
||||
env: {
|
||||
uriScheme: 'vscode',
|
||||
|
||||
@@ -17,6 +17,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
|
||||
@@ -35,6 +37,10 @@ export const toolParamNames = [
|
||||
"url",
|
||||
"coordinate",
|
||||
"text",
|
||||
"server_name",
|
||||
"tool_name",
|
||||
"arguments",
|
||||
"uri",
|
||||
"question",
|
||||
"result",
|
||||
"diff",
|
||||
@@ -86,6 +92,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">>
|
||||
|
||||
@@ -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[],
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import osName from "os-name"
|
||||
import { DiffStrategy } from "../diff/DiffStrategy"
|
||||
import defaultShell from "default-shell"
|
||||
import os from "os"
|
||||
import osName from "os-name"
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { DiffStrategy } from "../diff/DiffStrategy"
|
||||
import { McpHub } from "../../services/mcp/McpHub"
|
||||
|
||||
export const SYSTEM_PROMPT = async (
|
||||
cwd: string,
|
||||
supportsComputerUse: boolean,
|
||||
mcpHub: McpHub,
|
||||
diffStrategy?: DiffStrategy
|
||||
) => `You are Cline, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.
|
||||
|
||||
@@ -141,6 +143,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:
|
||||
@@ -194,6 +225,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.
|
||||
@@ -215,6 +266,411 @@ 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
|
||||
|
||||
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
|
||||
|
||||
# Connected MCP Servers
|
||||
|
||||
When a server is connected, you can use the server's tools via the \`use_mcp_tool\` tool, and access the server's resources via the \`access_mcp_resource\` tool.
|
||||
|
||||
${
|
||||
mcpHub.getServers().length > 0
|
||||
? `${mcpHub
|
||||
.getServers()
|
||||
.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}\n${schemaStr}`
|
||||
})
|
||||
.join("\n\n")
|
||||
|
||||
const templates = server.resourceTemplates
|
||||
?.map((template) => `- ${template.uriTemplate} (${template.name}): ${template.description}`)
|
||||
.join("\n")
|
||||
|
||||
const resources = server.resources
|
||||
?.map((resource) => `- ${resource.uri} (${resource.name}): ${resource.description}`)
|
||||
.join("\n")
|
||||
|
||||
const config = JSON.parse(server.config)
|
||||
|
||||
return (
|
||||
`## ${server.name} (\`${config.command}${config.args && Array.isArray(config.args) ? ` ${config.args.join(" ")}` : ""}\`)` +
|
||||
(tools ? `\n\n### Available Tools\n${tools}` : "") +
|
||||
(templates ? `\n\n### Resource Templates\n${templates}` : "") +
|
||||
(resources ? `\n\n### Direct Resources\n${resources}` : "")
|
||||
)
|
||||
})
|
||||
.join("\n\n")}`
|
||||
: "(No MCP servers currently connected)"
|
||||
}
|
||||
|
||||
## Creating an MCP Server
|
||||
|
||||
The user may ask you something along the lines of "add a tool" that does some function, in other words to create an MCP server that provides tools and resources that may connect to external APIs for example. You have the ability to create an MCP server and add it to a configuration file that will then expose the tools and resources for you to use with \`use_mcp_tool\` and \`access_mcp_resource\`.
|
||||
|
||||
When creating MCP servers, it's important to understand that they operate in a non-interactive environment. The server cannot initiate OAuth flows, open browser windows, or prompt for user input during runtime. All credentials and authentication tokens must be provided upfront through environment variables in the MCP settings configuration. For example, Spotify's API uses OAuth to get a refresh token for the user, but the MCP server cannot initiate this flow. While you can walk the user through obtaining an application client ID and secret, you may have to create a separate one-time setup script (like get-refresh-token.js) that captures and logs the final piece of the puzzle: the user's refresh token (i.e. you might run the script using execute_command which would open a browser for authentication, and then log the refresh token so that you can see it in the command output for you to use in the MCP settings configuration).
|
||||
|
||||
Unless the user specifies otherwise, new MCP servers should be created in: ${await mcpHub.getMcpServersPath()}
|
||||
|
||||
### Example MCP Server
|
||||
|
||||
For example, if the user wanted to give you the ability to retrieve weather information, you could create an MCP server that uses the OpenWeather API to get weather information, add it to the MCP settings configuration file, and then notice that you now have access to new tools and resources in the system prompt that you might use to show the user your new capabilities.
|
||||
|
||||
The following example demonstrates how to build an MCP server that provides weather data functionality. While this example shows how to implement resources, resource templates, and tools, in practice you should prefer using tools since they are more flexible and can handle dynamic parameters. The resource and resource template implementations are included here mainly for demonstration purposes of the different MCP capabilities, but a real weather server would likely just expose tools for fetching weather data. (The following steps are for macOS)
|
||||
|
||||
1. Use the \`create-typescript-server\` tool to bootstrap a new project in the default MCP servers directory:
|
||||
|
||||
\`\`\`bash
|
||||
cd ${await mcpHub.getMcpServersPath()}
|
||||
npx @modelcontextprotocol/create-server weather-server
|
||||
cd weather-server
|
||||
# Install dependencies
|
||||
npm install axios
|
||||
\`\`\`
|
||||
|
||||
This will create a new project with the following structure:
|
||||
|
||||
\`\`\`
|
||||
weather-server/
|
||||
├── package.json
|
||||
{
|
||||
...
|
||||
"type": "module", // added by default, uses ES module syntax (import/export) rather than CommonJS (require/module.exports) (Important to know if you create additional scripts in this server repository like a get-refresh-token.js script)
|
||||
"scripts": {
|
||||
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
|
||||
...
|
||||
}
|
||||
...
|
||||
}
|
||||
├── tsconfig.json
|
||||
└── src/
|
||||
└── weather-server/
|
||||
└── index.ts # Main server implementation
|
||||
\`\`\`
|
||||
|
||||
2. Replace \`src/index.ts\` with the following:
|
||||
|
||||
\`\`\`typescript
|
||||
#!/usr/bin/env node
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ErrorCode,
|
||||
ListResourcesRequestSchema,
|
||||
ListResourceTemplatesRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
McpError,
|
||||
ReadResourceRequestSchema,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import axios from 'axios';
|
||||
|
||||
const API_KEY = process.env.OPENWEATHER_API_KEY; // provided by MCP config
|
||||
if (!API_KEY) {
|
||||
throw new Error('OPENWEATHER_API_KEY environment variable is required');
|
||||
}
|
||||
|
||||
interface OpenWeatherResponse {
|
||||
main: {
|
||||
temp: number;
|
||||
humidity: number;
|
||||
};
|
||||
weather: [{ description: string }];
|
||||
wind: { speed: number };
|
||||
dt_txt?: string;
|
||||
}
|
||||
|
||||
const isValidForecastArgs = (
|
||||
args: any
|
||||
): args is { city: string; days?: number } =>
|
||||
typeof args === 'object' &&
|
||||
args !== null &&
|
||||
typeof args.city === 'string' &&
|
||||
(args.days === undefined || typeof args.days === 'number');
|
||||
|
||||
class WeatherServer {
|
||||
private server: Server;
|
||||
private axiosInstance;
|
||||
|
||||
constructor() {
|
||||
this.server = new Server(
|
||||
{
|
||||
name: 'example-weather-server',
|
||||
version: '0.1.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
resources: {},
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
this.axiosInstance = axios.create({
|
||||
baseURL: 'http://api.openweathermap.org/data/2.5',
|
||||
params: {
|
||||
appid: API_KEY,
|
||||
units: 'metric',
|
||||
},
|
||||
});
|
||||
|
||||
this.setupResourceHandlers();
|
||||
this.setupToolHandlers();
|
||||
|
||||
// Error handling
|
||||
this.server.onerror = (error) => console.error('[MCP Error]', error);
|
||||
process.on('SIGINT', async () => {
|
||||
await this.server.close();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
// MCP Resources represent any kind of UTF-8 encoded data that an MCP server wants to make available to clients, such as database records, API responses, log files, and more. Servers define direct resources with a static URI or dynamic resources with a URI template that follows the format \`[protocol]://[host]/[path]\`.
|
||||
private setupResourceHandlers() {
|
||||
// For static resources, servers can expose a list of resources:
|
||||
this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
||||
resources: [
|
||||
// This is a poor example since you could use the resource template to get the same information but this demonstrates how to define a static resource
|
||||
{
|
||||
uri: \`weather://San Francisco/current\`, // Unique identifier for San Francisco weather resource
|
||||
name: \`Current weather in San Francisco\`, // Human-readable name
|
||||
mimeType: 'application/json', // Optional MIME type
|
||||
// Optional description
|
||||
description:
|
||||
'Real-time weather data for San Francisco including temperature, conditions, humidity, and wind speed',
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
// For dynamic resources, servers can expose resource templates:
|
||||
this.server.setRequestHandler(
|
||||
ListResourceTemplatesRequestSchema,
|
||||
async () => ({
|
||||
resourceTemplates: [
|
||||
{
|
||||
uriTemplate: 'weather://{city}/current', // URI template (RFC 6570)
|
||||
name: 'Current weather for a given city', // Human-readable name
|
||||
mimeType: 'application/json', // Optional MIME type
|
||||
description: 'Real-time weather data for a specified city', // Optional description
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
// ReadResourceRequestSchema is used for both static resources and dynamic resource templates
|
||||
this.server.setRequestHandler(
|
||||
ReadResourceRequestSchema,
|
||||
async (request) => {
|
||||
const match = request.params.uri.match(
|
||||
/^weather:\/\/([^/]+)\/current$/
|
||||
);
|
||||
if (!match) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidRequest,
|
||||
\`Invalid URI format: \${request.params.uri}\`
|
||||
);
|
||||
}
|
||||
const city = decodeURIComponent(match[1]);
|
||||
|
||||
try {
|
||||
const response = await this.axiosInstance.get(
|
||||
'weather', // current weather
|
||||
{
|
||||
params: { q: city },
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri: request.params.uri,
|
||||
mimeType: 'application/json',
|
||||
text: JSON.stringify(
|
||||
{
|
||||
temperature: response.data.main.temp,
|
||||
conditions: response.data.weather[0].description,
|
||||
humidity: response.data.main.humidity,
|
||||
wind_speed: response.data.wind.speed,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InternalError,
|
||||
\`Weather API error: \${
|
||||
error.response?.data.message ?? error.message
|
||||
}\`
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/* MCP Tools enable servers to expose executable functionality to the system. Through these tools, you can interact with external systems, perform computations, and take actions in the real world.
|
||||
* - Like resources, tools are identified by unique names and can include descriptions to guide their usage. However, unlike resources, tools represent dynamic operations that can modify state or interact with external systems.
|
||||
* - While resources and tools are similar, you should prefer to create tools over resources when possible as they provide more flexibility.
|
||||
*/
|
||||
private setupToolHandlers() {
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: [
|
||||
{
|
||||
name: 'get_forecast', // Unique identifier
|
||||
description: 'Get weather forecast for a city', // Human-readable description
|
||||
inputSchema: {
|
||||
// JSON Schema for parameters
|
||||
type: 'object',
|
||||
properties: {
|
||||
city: {
|
||||
type: 'string',
|
||||
description: 'City name',
|
||||
},
|
||||
days: {
|
||||
type: 'number',
|
||||
description: 'Number of days (1-5)',
|
||||
minimum: 1,
|
||||
maximum: 5,
|
||||
},
|
||||
},
|
||||
required: ['city'], // Array of required property names
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
if (request.params.name !== 'get_forecast') {
|
||||
throw new McpError(
|
||||
ErrorCode.MethodNotFound,
|
||||
\`Unknown tool: \${request.params.name}\`
|
||||
);
|
||||
}
|
||||
|
||||
if (!isValidForecastArgs(request.params.arguments)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
'Invalid forecast arguments'
|
||||
);
|
||||
}
|
||||
|
||||
const city = request.params.arguments.city;
|
||||
const days = Math.min(request.params.arguments.days || 3, 5);
|
||||
|
||||
try {
|
||||
const response = await this.axiosInstance.get<{
|
||||
list: OpenWeatherResponse[];
|
||||
}>('forecast', {
|
||||
params: {
|
||||
q: city,
|
||||
cnt: days * 8,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(response.data.list, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: \`Weather API error: \${
|
||||
error.response?.data.message ?? error.message
|
||||
}\`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run() {
|
||||
const transport = new StdioServerTransport();
|
||||
await this.server.connect(transport);
|
||||
console.error('Weather MCP server running on stdio');
|
||||
}
|
||||
}
|
||||
|
||||
const server = new WeatherServer();
|
||||
server.run().catch(console.error);
|
||||
\`\`\`
|
||||
|
||||
(Remember: This is just an example–you may use different dependencies, break the implementation up into multiple files, etc.)
|
||||
|
||||
3. Build and compile the executable JavaScript file
|
||||
|
||||
\`\`\`bash
|
||||
npm run build
|
||||
\`\`\`
|
||||
|
||||
4. Whenever you need an environment variable such as an API key to configure the MCP server, walk the user through the process of getting the key. For example, they may need to create an account and go to a developer dashboard to generate the key. Provide step-by-step instructions and URLs to make it easy for the user to retrieve the necessary information. Then use the ask_followup_question tool to ask the user for the key, in this case the OpenWeather API key.
|
||||
|
||||
5. Install the MCP Server by adding the MCP server configuration to the settings file located at '${await mcpHub.getMcpSettingsFilePath()}'. The settings file may have other MCP servers already configured, so you would read it first and then add your new server to the existing \`mcpServers\` object.
|
||||
|
||||
\`\`\`json
|
||||
{
|
||||
"mcpServers": {
|
||||
...,
|
||||
"weather": {
|
||||
"command": "node",
|
||||
"args": ["/path/to/weather-server/build/index.js"],
|
||||
"env": {
|
||||
"OPENWEATHER_API_KEY": "user-provided-api-key"
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
(Note: the user may also ask you to install the MCP server to the Claude desktop app, in which case you would read then modify \`~/Library/Application\ Support/Claude/claude_desktop_config.json\` on macOS for example. It follows the same format of a top level \`mcpServers\` object.)
|
||||
|
||||
6. After you have edited the MCP settings configuration file, the system will automatically run all the servers and expose the available tools and resources in the 'Connected MCP Servers' section.
|
||||
|
||||
7. Now that you have access to these new tools and resources, you may suggest ways the user can command you to invoke them - for example, with this new weather tool now available, you can invite the user to ask "what's the weather in San Francisco?"
|
||||
|
||||
## Editing MCP Servers
|
||||
|
||||
The user may ask to add tools or resources that may make sense to add to an existing MCP server (listed under 'Connected MCP Servers' above: ${
|
||||
mcpHub
|
||||
.getServers()
|
||||
.map((server) => server.name)
|
||||
.join(", ") || "(None running currently)"
|
||||
}, e.g. if it would use the same API. This would be possible if you can locate the MCP server repository on the user's system by looking at the server arguments for a filepath. You might then use list_files and read_file to explore the files in the repository, and use write_to_file to make changes to the files.
|
||||
|
||||
However some MCP servers may be running from installed packages rather than a local repository, in which case it may make more sense to create a new MCP server.
|
||||
|
||||
# MCP Servers Are Not Always Necessary
|
||||
|
||||
The user may not always request the use or creation of MCP servers. Instead, they might provide tasks that can be completed with existing tools. While using the MCP SDK to extend your capabilities can be useful, it's important to understand that this is just one specialized type of task you can accomplish. You should only implement MCP servers when the user explicitly requests it (e.g., "add a tool that...").
|
||||
|
||||
Remember: The MCP documentation and example provided above are to help you understand and work with existing MCP servers or create new ones when requested by the user. You already have access to tools and capabilities that can be used to accomplish a wide range of tasks.
|
||||
|
||||
====
|
||||
|
||||
CAPABILITIES
|
||||
@@ -231,6 +687,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.
|
||||
|
||||
====
|
||||
|
||||
@@ -252,7 +709,7 @@ ${diffStrategy ? "- Prefer to use apply_diff over write_to_file when making chan
|
||||
- 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.
|
||||
@@ -261,6 +718,7 @@ ${diffStrategy ? "- Prefer to use apply_diff over write_to_file when making chan
|
||||
- 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."
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import axios from "axios"
|
||||
import fs from "fs/promises"
|
||||
import os from "os"
|
||||
import pWaitFor from "p-wait-for"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
@@ -10,6 +11,7 @@ import { openFile, openImage } from "../../integrations/misc/open-file"
|
||||
import { selectImages } from "../../integrations/misc/process-images"
|
||||
import { getTheme } from "../../integrations/theme/getTheme"
|
||||
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
|
||||
import { McpHub } from "../../services/mcp/McpHub"
|
||||
import { ApiProvider, ModelInfo } from "../../shared/api"
|
||||
import { findLast } from "../../shared/array"
|
||||
import { ExtensionMessage } from "../../shared/ExtensionMessage"
|
||||
@@ -70,6 +72,7 @@ export const GlobalFileNames = {
|
||||
apiConversationHistory: "api_conversation_history.json",
|
||||
uiMessages: "ui_messages.json",
|
||||
openRouterModels: "openrouter_models.json",
|
||||
mcpSettings: "cline_mcp_settings.json",
|
||||
}
|
||||
|
||||
export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
@@ -80,7 +83,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
private view?: vscode.WebviewView | vscode.WebviewPanel
|
||||
private cline?: Cline
|
||||
private workspaceTracker?: WorkspaceTracker
|
||||
private latestAnnouncementId = "oct-28-2024" // update to some unique identifier when we add a new announcement
|
||||
mcpHub?: McpHub
|
||||
private latestAnnouncementId = "dec-10-2024" // update to some unique identifier when we add a new announcement
|
||||
|
||||
constructor(
|
||||
readonly context: vscode.ExtensionContext,
|
||||
@@ -89,6 +93,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
this.outputChannel.appendLine("ClineProvider instantiated")
|
||||
ClineProvider.activeInstances.add(this)
|
||||
this.workspaceTracker = new WorkspaceTracker(this)
|
||||
this.mcpHub = new McpHub(this)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -112,6 +117,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
}
|
||||
this.workspaceTracker?.dispose()
|
||||
this.workspaceTracker = undefined
|
||||
this.mcpHub?.dispose()
|
||||
this.mcpHub = undefined
|
||||
this.outputChannel.appendLine("Disposed all disposables")
|
||||
ClineProvider.activeInstances.delete(this)
|
||||
}
|
||||
@@ -528,6 +535,21 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
.getConfiguration('roo-cline')
|
||||
.update('allowedCommands', message.commands, vscode.ConfigurationTarget.Global);
|
||||
break;
|
||||
case "openMcpSettings": {
|
||||
const mcpSettingsFilePath = await this.mcpHub?.getMcpSettingsFilePath()
|
||||
if (mcpSettingsFilePath) {
|
||||
openFile(mcpSettingsFilePath)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "restartMcpServer": {
|
||||
try {
|
||||
await this.mcpHub?.restartConnection(message.text!)
|
||||
} catch (error) {
|
||||
console.error(`Failed to retry connection for ${message.text}:`, error)
|
||||
}
|
||||
break
|
||||
}
|
||||
// Add more switch case statements here as more webview message commands
|
||||
// are created within the webview context (i.e. inside media/main.js)
|
||||
case "playSound":
|
||||
@@ -563,6 +585,24 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
|
||||
// MCP
|
||||
|
||||
async ensureMcpServersDirectoryExists(): Promise<string> {
|
||||
const mcpServersDir = path.join(os.homedir(), "Documents", "Cline", "MCP")
|
||||
try {
|
||||
await fs.mkdir(mcpServersDir, { recursive: true })
|
||||
} catch (error) {
|
||||
return "~/Documents/Cline/MCP" // in case creating a directory in documents fails for whatever reason (e.g. permissions) - this is fine since this path is only ever used in the system prompt
|
||||
}
|
||||
return mcpServersDir
|
||||
}
|
||||
|
||||
async ensureSettingsDirectoryExists(): Promise<string> {
|
||||
const settingsDir = path.join(this.context.globalStorageUri.fsPath, "settings")
|
||||
await fs.mkdir(settingsDir, { recursive: true })
|
||||
return settingsDir
|
||||
}
|
||||
|
||||
// Ollama
|
||||
|
||||
async getOllamaModels(baseUrl?: string) {
|
||||
|
||||
@@ -3,6 +3,53 @@ import * as vscode from 'vscode'
|
||||
import { ExtensionMessage, ExtensionState } from '../../../shared/ExtensionMessage'
|
||||
import { setSoundEnabled } from '../../../utils/sound'
|
||||
|
||||
// Mock delay module
|
||||
jest.mock('delay', () => {
|
||||
const delayFn = (ms: number) => Promise.resolve();
|
||||
delayFn.createDelay = () => delayFn;
|
||||
delayFn.reject = () => Promise.reject(new Error('Delay rejected'));
|
||||
delayFn.range = () => Promise.resolve();
|
||||
return delayFn;
|
||||
});
|
||||
|
||||
// Mock MCP-related modules
|
||||
jest.mock('@modelcontextprotocol/sdk/types.js', () => ({
|
||||
CallToolResultSchema: {},
|
||||
ListResourcesResultSchema: {},
|
||||
ListResourceTemplatesResultSchema: {},
|
||||
ListToolsResultSchema: {},
|
||||
ReadResourceResultSchema: {},
|
||||
ErrorCode: {
|
||||
InvalidRequest: 'InvalidRequest',
|
||||
MethodNotFound: 'MethodNotFound',
|
||||
InternalError: 'InternalError'
|
||||
},
|
||||
McpError: class McpError extends Error {
|
||||
code: string;
|
||||
constructor(code: string, message: string) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
this.name = 'McpError';
|
||||
}
|
||||
}
|
||||
}), { virtual: true });
|
||||
|
||||
jest.mock('@modelcontextprotocol/sdk/client/index.js', () => ({
|
||||
Client: jest.fn().mockImplementation(() => ({
|
||||
connect: jest.fn().mockResolvedValue(undefined),
|
||||
close: jest.fn().mockResolvedValue(undefined),
|
||||
listTools: jest.fn().mockResolvedValue({ tools: [] }),
|
||||
callTool: jest.fn().mockResolvedValue({ content: [] })
|
||||
}))
|
||||
}), { virtual: true });
|
||||
|
||||
jest.mock('@modelcontextprotocol/sdk/client/stdio.js', () => ({
|
||||
StdioClientTransport: jest.fn().mockImplementation(() => ({
|
||||
connect: jest.fn().mockResolvedValue(undefined),
|
||||
close: jest.fn().mockResolvedValue(undefined)
|
||||
}))
|
||||
}), { virtual: true });
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('vscode', () => ({
|
||||
ExtensionContext: jest.fn(),
|
||||
@@ -19,7 +66,11 @@ jest.mock('vscode', () => ({
|
||||
}),
|
||||
onDidChangeConfiguration: jest.fn().mockImplementation((callback) => ({
|
||||
dispose: jest.fn()
|
||||
}))
|
||||
})),
|
||||
onDidSaveTextDocument: jest.fn(() => ({ dispose: jest.fn() })),
|
||||
onDidChangeTextDocument: jest.fn(() => ({ dispose: jest.fn() })),
|
||||
onDidOpenTextDocument: jest.fn(() => ({ dispose: jest.fn() })),
|
||||
onDidCloseTextDocument: jest.fn(() => ({ dispose: jest.fn() }))
|
||||
},
|
||||
env: {
|
||||
uriScheme: 'vscode'
|
||||
|
||||
@@ -53,6 +53,12 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
}),
|
||||
)
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("roo-cline.mcpButtonClicked", () => {
|
||||
sidebarProvider.postMessageToWebview({ type: "action", action: "mcpButtonClicked" })
|
||||
}),
|
||||
)
|
||||
|
||||
const openClineInNewTab = async () => {
|
||||
outputChannel.appendLine("Opening Cline in new tab")
|
||||
// (this example uses webviewProvider activation event which is necessary to deserialize cached webview, but since we use retainContextWhenHidden, we don't need to use that event)
|
||||
|
||||
508
src/services/mcp/McpHub.ts
Normal file
508
src/services/mcp/McpHub.ts
Normal file
@@ -0,0 +1,508 @@
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
|
||||
import { StdioClientTransport, StdioServerParameters } from "@modelcontextprotocol/sdk/client/stdio.js"
|
||||
import {
|
||||
CallToolResultSchema,
|
||||
ListResourcesResultSchema,
|
||||
ListResourceTemplatesResultSchema,
|
||||
ListToolsResultSchema,
|
||||
ReadResourceResultSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js"
|
||||
import chokidar, { FSWatcher } from "chokidar"
|
||||
import delay from "delay"
|
||||
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,
|
||||
McpResourceResponse,
|
||||
McpResourceTemplate,
|
||||
McpServer,
|
||||
McpTool,
|
||||
McpToolCallResponse,
|
||||
} from "../../shared/mcp"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { arePathsEqual } from "../../utils/path"
|
||||
|
||||
export type McpConnection = {
|
||||
server: McpServer
|
||||
client: Client
|
||||
transport: StdioClientTransport
|
||||
}
|
||||
|
||||
// StdioServerParameters
|
||||
const StdioConfigSchema = z.object({
|
||||
command: z.string(),
|
||||
args: z.array(z.string()).optional(),
|
||||
env: z.record(z.string()).optional(),
|
||||
})
|
||||
|
||||
const McpSettingsSchema = z.object({
|
||||
mcpServers: z.record(StdioConfigSchema),
|
||||
})
|
||||
|
||||
export class McpHub {
|
||||
private providerRef: WeakRef<ClineProvider>
|
||||
private disposables: vscode.Disposable[] = []
|
||||
private settingsWatcher?: vscode.FileSystemWatcher
|
||||
private fileWatchers: Map<string, FSWatcher> = new Map()
|
||||
connections: McpConnection[] = []
|
||||
isConnecting: boolean = false
|
||||
|
||||
constructor(provider: ClineProvider) {
|
||||
this.providerRef = new WeakRef(provider)
|
||||
this.watchMcpSettingsFile()
|
||||
this.initializeMcpServers()
|
||||
}
|
||||
|
||||
getServers(): McpServer[] {
|
||||
return this.connections.map((conn) => conn.server)
|
||||
}
|
||||
|
||||
async getMcpServersPath(): Promise<string> {
|
||||
const provider = this.providerRef.deref()
|
||||
if (!provider) {
|
||||
throw new Error("Provider not available")
|
||||
}
|
||||
const mcpServersPath = await provider.ensureMcpServersDirectoryExists()
|
||||
return mcpServersPath
|
||||
}
|
||||
|
||||
async getMcpSettingsFilePath(): Promise<string> {
|
||||
const provider = this.providerRef.deref()
|
||||
if (!provider) {
|
||||
throw new Error("Provider not available")
|
||||
}
|
||||
const mcpSettingsFilePath = path.join(
|
||||
await provider.ensureSettingsDirectoryExists(),
|
||||
GlobalFileNames.mcpSettings,
|
||||
)
|
||||
const fileExists = await fileExistsAtPath(mcpSettingsFilePath)
|
||||
if (!fileExists) {
|
||||
await fs.writeFile(
|
||||
mcpSettingsFilePath,
|
||||
`{
|
||||
"mcpServers": {
|
||||
|
||||
}
|
||||
}`,
|
||||
)
|
||||
}
|
||||
return mcpSettingsFilePath
|
||||
}
|
||||
|
||||
private async watchMcpSettingsFile(): Promise<void> {
|
||||
const settingsPath = await this.getMcpSettingsFilePath()
|
||||
this.disposables.push(
|
||||
vscode.workspace.onDidSaveTextDocument(async (document) => {
|
||||
if (arePathsEqual(document.uri.fsPath, settingsPath)) {
|
||||
const content = await fs.readFile(settingsPath, "utf-8")
|
||||
const errorMessage =
|
||||
"Invalid MCP settings format. Please ensure your settings follow the correct JSON format."
|
||||
let config: any
|
||||
try {
|
||||
config = JSON.parse(content)
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(errorMessage)
|
||||
return
|
||||
}
|
||||
const result = McpSettingsSchema.safeParse(config)
|
||||
if (!result.success) {
|
||||
vscode.window.showErrorMessage(errorMessage)
|
||||
return
|
||||
}
|
||||
try {
|
||||
vscode.window.showInformationMessage("Updating MCP servers...")
|
||||
await this.updateServerConnections(result.data.mcpServers || {})
|
||||
vscode.window.showInformationMessage("MCP servers updated")
|
||||
} catch (error) {
|
||||
console.error("Failed to process MCP settings change:", error)
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
private async initializeMcpServers(): Promise<void> {
|
||||
try {
|
||||
const settingsPath = await this.getMcpSettingsFilePath()
|
||||
const content = await fs.readFile(settingsPath, "utf-8")
|
||||
const config = JSON.parse(content)
|
||||
await this.updateServerConnections(config.mcpServers || {})
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize MCP servers:", error)
|
||||
}
|
||||
}
|
||||
|
||||
private async connectToServer(name: string, config: StdioServerParameters): Promise<void> {
|
||||
// Remove existing connection if it exists (should never happen, the connection should be deleted beforehand)
|
||||
this.connections = this.connections.filter((conn) => conn.server.name !== name)
|
||||
|
||||
try {
|
||||
// Each MCP server requires its own transport connection and has unique capabilities, configurations, and error handling. Having separate clients also allows proper scoping of resources/tools and independent server management like reconnection.
|
||||
const client = new Client(
|
||||
{
|
||||
name: "Cline",
|
||||
version: this.providerRef.deref()?.context.extension?.packageJSON?.version ?? "1.0.0",
|
||||
},
|
||||
{
|
||||
capabilities: {},
|
||||
},
|
||||
)
|
||||
|
||||
const transport = new StdioClientTransport({
|
||||
command: config.command,
|
||||
args: config.args,
|
||||
env: {
|
||||
...config.env,
|
||||
...(process.env.PATH ? { PATH: process.env.PATH } : {}),
|
||||
// ...(process.env.NODE_PATH ? { NODE_PATH: process.env.NODE_PATH } : {}),
|
||||
},
|
||||
stderr: "pipe", // necessary for stderr to be available
|
||||
})
|
||||
|
||||
transport.onerror = async (error) => {
|
||||
console.error(`Transport error for "${name}":`, error)
|
||||
const connection = this.connections.find((conn) => conn.server.name === name)
|
||||
if (connection) {
|
||||
connection.server.status = "disconnected"
|
||||
this.appendErrorMessage(connection, error.message)
|
||||
}
|
||||
await this.notifyWebviewOfServerChanges()
|
||||
}
|
||||
|
||||
transport.onclose = async () => {
|
||||
const connection = this.connections.find((conn) => conn.server.name === name)
|
||||
if (connection) {
|
||||
connection.server.status = "disconnected"
|
||||
}
|
||||
await this.notifyWebviewOfServerChanges()
|
||||
}
|
||||
|
||||
// If the config is invalid, show an error
|
||||
if (!StdioConfigSchema.safeParse(config).success) {
|
||||
console.error(`Invalid config for "${name}": missing or invalid parameters`)
|
||||
const connection: McpConnection = {
|
||||
server: {
|
||||
name,
|
||||
config: JSON.stringify(config),
|
||||
status: "disconnected",
|
||||
error: "Invalid config: missing or invalid parameters",
|
||||
},
|
||||
client,
|
||||
transport,
|
||||
}
|
||||
this.connections.push(connection)
|
||||
return
|
||||
}
|
||||
|
||||
// valid schema
|
||||
const connection: McpConnection = {
|
||||
server: {
|
||||
name,
|
||||
config: JSON.stringify(config),
|
||||
status: "connecting",
|
||||
},
|
||||
client,
|
||||
transport,
|
||||
}
|
||||
this.connections.push(connection)
|
||||
|
||||
// transport.stderr is only available after the process has been started. However we can't start it separately from the .connect() call because it also starts the transport. And we can't place this after the connect call since we need to capture the stderr stream before the connection is established, in order to capture errors during the connection process.
|
||||
// As a workaround, we start the transport ourselves, and then monkey-patch the start method to no-op so that .connect() doesn't try to start it again.
|
||||
await transport.start()
|
||||
const stderrStream = transport.stderr
|
||||
if (stderrStream) {
|
||||
stderrStream.on("data", async (data: Buffer) => {
|
||||
const errorOutput = data.toString()
|
||||
console.error(`Server "${name}" stderr:`, errorOutput)
|
||||
const connection = this.connections.find((conn) => conn.server.name === name)
|
||||
if (connection) {
|
||||
// NOTE: we do not set server status to "disconnected" because stderr logs do not necessarily mean the server crashed or disconnected, it could just be informational. In fact when the server first starts up, it immediately logs "<name> server running on stdio" to stderr.
|
||||
this.appendErrorMessage(connection, errorOutput)
|
||||
// Only need to update webview right away if it's already disconnected
|
||||
if (connection.server.status === "disconnected") {
|
||||
await this.notifyWebviewOfServerChanges()
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.error(`No stderr stream for ${name}`)
|
||||
}
|
||||
transport.start = async () => {} // No-op now, .connect() won't fail
|
||||
|
||||
// // 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()
|
||||
// },
|
||||
// )
|
||||
|
||||
// Connect
|
||||
await client.connect(transport)
|
||||
connection.server.status = "connected"
|
||||
connection.server.error = ""
|
||||
|
||||
// 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)
|
||||
} catch (error) {
|
||||
// Update status with error
|
||||
const connection = this.connections.find((conn) => conn.server.name === name)
|
||||
if (connection) {
|
||||
connection.server.status = "disconnected"
|
||||
this.appendErrorMessage(connection, error instanceof Error ? error.message : String(error))
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private appendErrorMessage(connection: McpConnection, error: string) {
|
||||
const newError = connection.server.error ? `${connection.server.error}\n${error}` : error
|
||||
connection.server.error = newError //.slice(0, 800)
|
||||
}
|
||||
|
||||
private async fetchToolsList(serverName: string): Promise<McpTool[]> {
|
||||
try {
|
||||
const response = await this.connections
|
||||
.find((conn) => conn.server.name === serverName)
|
||||
?.client.request({ method: "tools/list" }, ListToolsResultSchema)
|
||||
return response?.tools || []
|
||||
} catch (error) {
|
||||
// console.error(`Failed to fetch tools for ${serverName}:`, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchResourcesList(serverName: string): Promise<McpResource[]> {
|
||||
try {
|
||||
const response = await this.connections
|
||||
.find((conn) => conn.server.name === serverName)
|
||||
?.client.request({ method: "resources/list" }, ListResourcesResultSchema)
|
||||
return response?.resources || []
|
||||
} catch (error) {
|
||||
// console.error(`Failed to fetch resources for ${serverName}:`, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
// connection.client.removeNotificationHandler("notifications/stderr")
|
||||
// connection.client.removeNotificationHandler("notifications/stderr")
|
||||
await connection.transport.close()
|
||||
await connection.client.close()
|
||||
} catch (error) {
|
||||
console.error(`Failed to close transport for ${name}:`, error)
|
||||
}
|
||||
this.connections = this.connections.filter((conn) => conn.server.name !== name)
|
||||
}
|
||||
}
|
||||
|
||||
async updateServerConnections(newServers: Record<string, any>): Promise<void> {
|
||||
this.isConnecting = true
|
||||
this.removeAllFileWatchers()
|
||||
const currentNames = new Set(this.connections.map((conn) => conn.server.name))
|
||||
const newNames = new Set(Object.keys(newServers))
|
||||
|
||||
// Delete removed servers
|
||||
for (const name of currentNames) {
|
||||
if (!newNames.has(name)) {
|
||||
await this.deleteConnection(name)
|
||||
console.log(`Deleted MCP server: ${name}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Update or add servers
|
||||
for (const [name, config] of Object.entries(newServers)) {
|
||||
const currentConnection = this.connections.find((conn) => conn.server.name === name)
|
||||
|
||||
if (!currentConnection) {
|
||||
// New server
|
||||
try {
|
||||
this.setupFileWatcher(name, config)
|
||||
await this.connectToServer(name, config)
|
||||
} catch (error) {
|
||||
console.error(`Failed to connect to new MCP server ${name}:`, error)
|
||||
}
|
||||
} else if (!deepEqual(JSON.parse(currentConnection.server.config), config)) {
|
||||
// Existing server with changed config
|
||||
try {
|
||||
this.setupFileWatcher(name, config)
|
||||
await this.deleteConnection(name)
|
||||
await this.connectToServer(name, config)
|
||||
console.log(`Reconnected MCP server with updated config: ${name}`)
|
||||
} catch (error) {
|
||||
console.error(`Failed to reconnect MCP server ${name}:`, error)
|
||||
}
|
||||
}
|
||||
// If server exists with same config, do nothing
|
||||
}
|
||||
await this.notifyWebviewOfServerChanges()
|
||||
this.isConnecting = false
|
||||
}
|
||||
|
||||
private setupFileWatcher(name: string, config: any) {
|
||||
const filePath = config.args?.find((arg: string) => arg.includes("build/index.js"))
|
||||
if (filePath) {
|
||||
// we use chokidar instead of onDidSaveTextDocument because it doesn't require the file to be open in the editor. The settings config is better suited for onDidSave since that will be manually updated by the user or Cline (and we want to detect save events, not every file change)
|
||||
const watcher = chokidar.watch(filePath, {
|
||||
// persistent: true,
|
||||
// ignoreInitial: true,
|
||||
// awaitWriteFinish: true, // This helps with atomic writes
|
||||
})
|
||||
|
||||
watcher.on("change", () => {
|
||||
console.log(`Detected change in ${filePath}. Restarting server ${name}...`)
|
||||
this.restartConnection(name)
|
||||
})
|
||||
|
||||
this.fileWatchers.set(name, watcher)
|
||||
}
|
||||
}
|
||||
|
||||
private removeAllFileWatchers() {
|
||||
this.fileWatchers.forEach((watcher) => watcher.close())
|
||||
this.fileWatchers.clear()
|
||||
}
|
||||
|
||||
async restartConnection(serverName: string): Promise<void> {
|
||||
this.isConnecting = true
|
||||
const provider = this.providerRef.deref()
|
||||
if (!provider) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get existing connection and update its status
|
||||
const connection = this.connections.find((conn) => conn.server.name === serverName)
|
||||
const config = connection?.server.config
|
||||
if (config) {
|
||||
vscode.window.showInformationMessage(`Restarting ${serverName} MCP server...`)
|
||||
connection.server.status = "connecting"
|
||||
connection.server.error = ""
|
||||
await this.notifyWebviewOfServerChanges()
|
||||
await delay(500) // artificial delay to show user that server is restarting
|
||||
try {
|
||||
await this.deleteConnection(serverName)
|
||||
// Try to connect again using existing config
|
||||
await this.connectToServer(serverName, JSON.parse(config))
|
||||
vscode.window.showInformationMessage(`${serverName} MCP server connected`)
|
||||
} catch (error) {
|
||||
console.error(`Failed to restart connection for ${serverName}:`, error)
|
||||
vscode.window.showErrorMessage(`Failed to connect to ${serverName} MCP server`)
|
||||
}
|
||||
}
|
||||
|
||||
await this.notifyWebviewOfServerChanges()
|
||||
this.isConnecting = false
|
||||
}
|
||||
|
||||
private async notifyWebviewOfServerChanges(): Promise<void> {
|
||||
// servers should always be sorted in the order they are defined in the settings file
|
||||
const settingsPath = await this.getMcpSettingsFilePath()
|
||||
const content = await fs.readFile(settingsPath, "utf-8")
|
||||
const config = JSON.parse(content)
|
||||
const serverOrder = Object.keys(config.mcpServers || {})
|
||||
await this.providerRef.deref()?.postMessageToWebview({
|
||||
type: "mcpServers",
|
||||
mcpServers: [...this.connections]
|
||||
.sort((a, b) => {
|
||||
const indexA = serverOrder.indexOf(a.server.name)
|
||||
const indexB = serverOrder.indexOf(b.server.name)
|
||||
return indexA - indexB
|
||||
})
|
||||
.map((connection) => connection.server),
|
||||
})
|
||||
}
|
||||
|
||||
// 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}. Please make sure to use MCP servers available under 'Connected MCP Servers'.`,
|
||||
)
|
||||
}
|
||||
return await connection.client.request(
|
||||
{
|
||||
method: "tools/call",
|
||||
params: {
|
||||
name: toolName,
|
||||
arguments: toolArguments,
|
||||
},
|
||||
},
|
||||
CallToolResultSchema,
|
||||
)
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
this.removeAllFileWatchers()
|
||||
for (const connection of this.connections) {
|
||||
try {
|
||||
await this.deleteConnection(connection.server.name)
|
||||
} catch (error) {
|
||||
console.error(`Failed to close connection for ${connection.server.name}:`, error)
|
||||
}
|
||||
}
|
||||
this.connections = []
|
||||
if (this.settingsWatcher) {
|
||||
this.settingsWatcher.dispose()
|
||||
}
|
||||
this.disposables.forEach((d) => d.dispose())
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { ApiConfiguration, ModelInfo } from "./api"
|
||||
import { HistoryItem } from "./HistoryItem"
|
||||
import { McpServer } from "./mcp"
|
||||
|
||||
// webview will hold state
|
||||
export interface ExtensionMessage {
|
||||
@@ -16,8 +17,14 @@ export interface ExtensionMessage {
|
||||
| "invoke"
|
||||
| "partialMessage"
|
||||
| "openRouterModels"
|
||||
| "mcpServers"
|
||||
text?: string
|
||||
action?: "chatButtonClicked" | "settingsButtonClicked" | "historyButtonClicked" | "didBecomeVisible"
|
||||
action?:
|
||||
| "chatButtonClicked"
|
||||
| "mcpButtonClicked"
|
||||
| "settingsButtonClicked"
|
||||
| "historyButtonClicked"
|
||||
| "didBecomeVisible"
|
||||
invoke?: "sendMessage" | "primaryButtonClick" | "secondaryButtonClick"
|
||||
state?: ExtensionState
|
||||
images?: string[]
|
||||
@@ -26,6 +33,7 @@ export interface ExtensionMessage {
|
||||
filePaths?: string[]
|
||||
partialMessage?: ClineMessage
|
||||
openRouterModels?: Record<string, ModelInfo>
|
||||
mcpServers?: McpServer[]
|
||||
}
|
||||
|
||||
export interface ExtensionState {
|
||||
@@ -66,6 +74,7 @@ export type ClineAsk =
|
||||
| "resume_completed_task"
|
||||
| "mistake_limit_reached"
|
||||
| "browser_action_launch"
|
||||
| "use_mcp_server"
|
||||
|
||||
export type ClineSay =
|
||||
| "task"
|
||||
@@ -83,6 +92,8 @@ export type ClineSay =
|
||||
| "browser_action"
|
||||
| "browser_action_result"
|
||||
| "command"
|
||||
| "mcp_server_request_started"
|
||||
| "mcp_server_response"
|
||||
|
||||
export interface ClineSayTool {
|
||||
tool:
|
||||
@@ -118,6 +129,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
|
||||
|
||||
@@ -32,6 +32,8 @@ export interface WebviewMessage {
|
||||
| "playSound"
|
||||
| "soundEnabled"
|
||||
| "diffEnabled"
|
||||
| "openMcpSettings"
|
||||
| "restartMcpServer"
|
||||
text?: string
|
||||
askResponse?: ClineAskResponse
|
||||
apiConfiguration?: ApiConfiguration
|
||||
|
||||
@@ -236,8 +236,16 @@ export const openAiModelInfoSaneDefaults: ModelInfo = {
|
||||
// Gemini
|
||||
// https://ai.google.dev/gemini-api/docs/models/gemini
|
||||
export type GeminiModelId = keyof typeof geminiModels
|
||||
export const geminiDefaultModelId: GeminiModelId = "gemini-1.5-flash-002"
|
||||
export const geminiDefaultModelId: GeminiModelId = "gemini-2.0-flash-exp"
|
||||
export const geminiModels = {
|
||||
"gemini-2.0-flash-exp": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
"gemini-1.5-flash-002": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 1_048_576,
|
||||
@@ -286,14 +294,6 @@ export const geminiModels = {
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
"gemini-2.0-flash-exp": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
||||
// OpenAI Native
|
||||
|
||||
64
src/shared/mcp.ts
Normal file
64
src/shared/mcp.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
export type McpServer = {
|
||||
name: string
|
||||
config: string
|
||||
status: "connected" | "connecting" | "disconnected"
|
||||
error?: string
|
||||
tools?: McpTool[]
|
||||
resources?: McpResource[]
|
||||
resourceTemplates?: McpResourceTemplate[]
|
||||
}
|
||||
|
||||
export type McpTool = {
|
||||
name: string
|
||||
description?: string
|
||||
inputSchema?: object
|
||||
}
|
||||
|
||||
export type McpResource = {
|
||||
uri: string
|
||||
name: string
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user