From 61311e3f41b9fa112cc4d200801c1e499388c77f Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Mon, 9 Dec 2024 13:13:06 -0800 Subject: [PATCH] Add mcp-servers dir --- src/core/Cline.ts | 22 +++++++++++++++------- src/core/prompts/system.ts | 20 ++++++++++++++------ src/core/webview/ClineProvider.ts | 31 +++++++++++++++++++++++-------- src/services/mcp/McpHub.ts | 13 +++++++++++++ 4 files changed, 65 insertions(+), 21 deletions(-) diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 839073c..a5a7f99 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -756,14 +756,22 @@ export class Cline { await pWaitFor(() => this.providerRef.deref()?.mcpHub?.isConnecting !== true, { timeout: 10_000 }).catch(() => { console.error("MCP servers failed to connect in time") }) - const mcpServers = this.providerRef.deref()?.mcpHub?.connections.map((conn) => conn.server) - console.log("mcpServers for system prompt:", JSON.stringify(mcpServers, null, 2)) - let systemPrompt = await SYSTEM_PROMPT( - cwd, - this.api.getModel().info.supportsComputerUse ?? false, - (await this.providerRef.deref()?.mcpHub?.getMcpSettingsFilePath()) || "(Unknown)", - mcpServers, + + const mcpHub = this.providerRef.deref()?.mcpHub + if (!mcpHub) { + throw new Error("MCP hub not available") + } + + console.log( + "mcpServers for system prompt:", + JSON.stringify( + mcpHub.connections.map((conn) => conn.server), + null, + 2, + ), ) + + let systemPrompt = await SYSTEM_PROMPT(cwd, this.api.getModel().info.supportsComputerUse ?? false, mcpHub) 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 systemPrompt += addCustomInstructions(this.customInstructions) diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index 9316087..e50ae40 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -2,12 +2,12 @@ import osName from "os-name" import defaultShell from "default-shell" import os from "os" import { McpServer } from "../../shared/mcp" +import { McpHub } from "../../services/mcp/McpHub" export const SYSTEM_PROMPT = async ( cwd: string, supportsComputerUse: boolean, - mcpSettingsPath: string, - mcpServers: McpServer[] = [], + mcpHub: McpHub, ) => `You are Cline, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. ==== @@ -295,8 +295,9 @@ When a server is connected, you can: # Connected MCP Servers ${ - mcpServers.length > 0 - ? `${mcpServers + mcpHub.getServers().length > 0 + ? `${mcpHub + .getServers() .filter((server) => server.status === "connected") .map((server) => { const tools = @@ -354,6 +355,8 @@ The user may ask you something along the lines of "add a tool" that does some fu 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: ${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 abilities. @@ -663,7 +666,7 @@ 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 markdown formatted links 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 '${mcpSettingsPath}'. 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. +5. Install the MCP Server by adding the MCP server configuration to the settings file located at '${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 { @@ -750,7 +753,12 @@ async with httpx.AsyncClient() as client: ## Editing MCP Servers -The user may ask to add tools or resources to an existing MCP server (listed under 'Connected MCP Servers' above: ${mcpServers.map((server) => server.name).join(", ") || "(None running currently)"}), or may more generally ask to add functionality that may make sense to add to an existing local MCP server rather than creating a new one. 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. +The user may ask to add tools or resources to an existing MCP server (listed under 'Connected MCP Servers' above: ${ + mcpHub + .getServers() + .map((server) => server.name) + .join(", ") || "(None running currently)" +}), or may more generally ask to add functionality that may make sense to add to an existing local MCP server rather than creating a new one. 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. If you edit a Connected MCP server, you will need to guide the user to restart the server manually for any changes to take effect. They would need to: diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 3bb77d6..440b779 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -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" @@ -20,7 +22,6 @@ import { Cline } from "../Cline" import { openMention } from "../mentions" import { getNonce } from "./getNonce" import { getUri } from "./getUri" -import { McpHub } from "../../services/mcp/McpHub" /* https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts @@ -66,6 +67,12 @@ export const GlobalFileNames = { mcpSettings: "cline_mcp_settings.json", } +export const GlobalDirNames = { + cache: "cache", + settings: "settings", + mcpServers: "mcp-servers", +} + export class ClineProvider implements vscode.WebviewViewProvider { public static readonly sideBarId = "claude-dev.SidebarProvider" // used in package.json as the view's id. This value cannot be changed due to how vscode caches views based on their id, and updating the id would break existing instances of the extension. public static readonly tabPanelId = "claude-dev.TabPanelProvider" @@ -524,6 +531,20 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.postStateToWebview() } + // MCP + + async ensureMcpServersDirectoryExists(): Promise { + const mcpServersDir = path.join(this.context.globalStorageUri.fsPath, GlobalDirNames.mcpServers) + await fs.mkdir(mcpServersDir, { recursive: true }) + return mcpServersDir + } + + async ensureSettingsDirectoryExists(): Promise { + const settingsDir = path.join(this.context.globalStorageUri.fsPath, GlobalDirNames.settings) + await fs.mkdir(settingsDir, { recursive: true }) + return settingsDir + } + // Ollama async getOllamaModels(baseUrl?: string) { @@ -589,17 +610,11 @@ export class ClineProvider implements vscode.WebviewViewProvider { } private async ensureCacheDirectoryExists(): Promise { - const cacheDir = path.join(this.context.globalStorageUri.fsPath, "cache") + const cacheDir = path.join(this.context.globalStorageUri.fsPath, GlobalDirNames.cache) await fs.mkdir(cacheDir, { recursive: true }) return cacheDir } - async ensureSettingsDirectoryExists(): Promise { - const settingsDir = path.join(this.context.globalStorageUri.fsPath, "settings") - await fs.mkdir(settingsDir, { recursive: true }) - return settingsDir - } - async readOpenRouterModels(): Promise | undefined> { const openRouterModelsFilePath = path.join( await this.ensureCacheDirectoryExists(), diff --git a/src/services/mcp/McpHub.ts b/src/services/mcp/McpHub.ts index 9a24b2c..1278ef6 100644 --- a/src/services/mcp/McpHub.ts +++ b/src/services/mcp/McpHub.ts @@ -55,6 +55,19 @@ export class McpHub { this.initializeMcpServers() } + getServers(): McpServer[] { + return this.connections.map((conn) => conn.server) + } + + async getMcpServersPath(): Promise { + const provider = this.providerRef.deref() + if (!provider) { + throw new Error("Provider not available") + } + const mcpServersPath = await provider.ensureMcpServersDirectoryExists() + return mcpServersPath + } + async getMcpSettingsFilePath(): Promise { const provider = this.providerRef.deref() if (!provider) {