Add index.js monitoring and fix capturing server error output

This commit is contained in:
Saoud Rizwan
2024-12-10 14:26:25 -08:00
parent 61311e3f41
commit 1319105bb4
6 changed files with 237 additions and 201 deletions

126
package-lock.json generated
View File

@@ -20,7 +20,9 @@
"@vscode/codicons": "^0.0.36",
"axios": "^1.7.4",
"cheerio": "^1.0.0",
"chokidar": "^4.0.1",
"clone-deep": "^4.0.1",
"debounce": "^2.2.0",
"default-shell": "^2.2.0",
"delay": "^6.0.0",
"diff": "^5.2.0",
@@ -4851,6 +4853,44 @@
"node": ">=18"
}
},
"node_modules/@vscode/test-cli/node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/@vscode/test-cli/node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/@vscode/test-electron": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.4.1.tgz",
@@ -5485,28 +5525,18 @@
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz",
"integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==",
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 8.10.0"
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/chromium-bidi": {
@@ -5816,6 +5846,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/debounce": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/debounce/-/debounce-2.2.0.tgz",
"integrity": "sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
@@ -8443,6 +8485,31 @@
"node": ">= 14.0.0"
}
},
"node_modules/mocha/node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/mocha/node_modules/cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
@@ -8496,6 +8563,19 @@
"node": ">=10"
}
},
"node_modules/mocha/node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/mocha/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -9729,16 +9809,16 @@
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz",
"integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==",
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
"node": ">= 14.16.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/regexp.prototype.flags": {

View File

@@ -173,7 +173,9 @@
"@vscode/codicons": "^0.0.36",
"axios": "^1.7.4",
"cheerio": "^1.0.0",
"chokidar": "^4.0.1",
"clone-deep": "^4.0.1",
"debounce": "^2.2.0",
"default-shell": "^2.2.0",
"delay": "^6.0.0",
"diff": "^5.2.0",

View File

@@ -267,86 +267,46 @@ MCP SERVERS
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional capabilities through a standardized protocol. Each server can offer tools and resources that extend your capabilities.
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
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:
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"
return `- ${tool.name}: ${tool.description}\n${schemaStr}`
})
.join("\n\n")
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 templates = server.resourceTemplates
?.map((template) => `- ${template.uriTemplate} (${template.name}): ${template.description}`)
.join("\n")
const resources = server.resources?.length
? server.resources
.map(
(resource) =>
`- ${resource.uri} (${resource.name}): ${resource.description || "No description provided"}`,
)
.join("\n")
: "No resources available"
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(" ")}` : ""}\`)
### Available Tools
${tools}
### Available Resources
#### Resource Templates
${templates}
#### Direct Resources
${resources}
`
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."
: "(No MCP servers currently connected)"
}
## Creating an MCP Server
@@ -355,7 +315,7 @@ 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()}
Unless the user specifies otherwise, new MCP servers should be created in: ${await mcpHub.getMcpServersPath()}
### Example MCP Server
@@ -363,9 +323,10 @@ For example, if the user wanted to give you the ability to retrieve weather info
The following examples demonstrate 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:
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
@@ -382,8 +343,7 @@ weather-server/
"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')\"",
// The MCP Inspector is an interactive developer tool for testing and debugging MCP servers. It launches a web server that allows the user to interact with the server and test its capabilities. (You could also pass arguments or use with \`uv\` e.g., \`npx @modelcontextprotocol/inspector uvx <package-name> <args>\`)
"inspector": "npx @modelcontextprotocol/inspector build/index.js"
"inspector": "npx @modelcontextprotocol/inspector build/index.js" // The MCP Inspector is an interactive developer tool for testing and debugging MCP servers. It launches a web server that allows the user to interact with the server and test its capabilities. (You could also pass arguments or use with \`uv\` e.g., \`npx @modelcontextprotocol/inspector uvx <package-name> <args>\`)
}
...
}
@@ -450,7 +410,6 @@ class WeatherServer {
}
);
// Configure axios with defaults
this.axiosInstance = axios.create({
baseURL: 'http://api.openweathermap.org/data/2.5',
params: {
@@ -459,24 +418,18 @@ class WeatherServer {
},
});
this.setupHandlers();
this.setupErrorHandling();
}
private setupErrorHandling() {
// Setup handlers
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);
});
}
private setupHandlers() {
this.setupResourceHandlers();
this.setupToolHandlers();
}
// 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() {
this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
@@ -656,17 +609,17 @@ const server = new WeatherServer();
server.run().catch(console.error);
\`\`\`
(This is just an example, but you may use different dependencies, break this up into multiple files for example put the types in a separate types.ts file, etc.)
(Remember: This is just an exampleyou may use different dependencies, break the implementation up into multiple files, etc.)
3. Build and compile the executablejavascript file
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 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.
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 '${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.
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
{
@@ -683,73 +636,11 @@ npm run build
}
\`\`\`
(Note: the user may also ask you to install the MCP server to the Claude desktop app, in which case you would modify \`~/Library/Application\ Support/Claude/claude_desktop_config.json\` on macOS for example. It follows the same format of a top level \`mcpServers\` object and each server having \`command\`, \`args\`, and/or \`env\`)
(Note: the user may also ask you to install the MCP server to the Claude desktop app, in which case you would 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 system prompt.
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?" or "get a forecast for my upcoming trip to New York".
### Python Implementation
If the user requests it or there is a good reason to use Python over TypeScript for building an MCP server, you would follow the same core concepts as the TypeScript version, with these key differences:
1. Initial setup uses \`uv\` instead of \`npm\`:
\`\`\`bash
# Create and setup project
uvx create-mcp-server --path weather-server # if uv is not installed, you may guide the user to install it using https://docs.astral.sh/uv/getting-started/installation
cd weather-server
uv add httpx python-dotenv
\`\`\`
2. Project structure differences:
\`\`\`
weather-server/
└── src/
└── weather-server/
├── __init__.py # Contains main() entry point
└── server.py # Main server implementation
\`\`\`
3. Key implementation differences:
\`\`\`python:src/weather-server/server.py
from mcp.server import Server
from mcp.types import Resource, Tool, TextContent
from pydantic import AnyUrl
# Use AnyUrl instead of string for URI validation
@app.list_resources()
async def list_resources() -> list[Resource]:
uri = AnyUrl(f"weather://San Francisco/current")
return [Resource(uri=uri, ...)]
# Async handlers instead of Promise-based
@app.read_resource()
async def read_resource(uri: AnyUrl) -> str:
# ... implementation ...
# Use httpx instead of axios
async with httpx.AsyncClient() as client:
response = await client.get(url, params=params)
\`\`\`
4. Configuration file uses \`uv\` instead of \`node\`:
\`\`\`json:
{
"mcpServers": {
"weather": {
"command": "uv",
"args": ["--directory", "/path/to/weather-server", "run", "weather-service"],
"env": {
"OPENWEATHER_API_KEY": "your-api-key"
}
}
}
}
\`\`\`
8. 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
@@ -760,14 +651,14 @@ The user may ask to add tools or resources to an existing MCP server (listed und
.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:
"1. Open the MCP Servers view by selecting the server icon in the menu bar
2. Select the server they want to restart
3. Click the 'Restart Server' button"
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 provided above is 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

View File

@@ -67,12 +67,6 @@ 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"
@@ -534,13 +528,13 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// MCP
async ensureMcpServersDirectoryExists(): Promise<string> {
const mcpServersDir = path.join(this.context.globalStorageUri.fsPath, GlobalDirNames.mcpServers)
const mcpServersDir = path.join(os.homedir(), "Documents", "Cline", "MCP")
await fs.mkdir(mcpServersDir, { recursive: true })
return mcpServersDir
}
async ensureSettingsDirectoryExists(): Promise<string> {
const settingsDir = path.join(this.context.globalStorageUri.fsPath, GlobalDirNames.settings)
const settingsDir = path.join(this.context.globalStorageUri.fsPath, "settings")
await fs.mkdir(settingsDir, { recursive: true })
return settingsDir
}
@@ -610,7 +604,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
private async ensureCacheDirectoryExists(): Promise<string> {
const cacheDir = path.join(this.context.globalStorageUri.fsPath, GlobalDirNames.cache)
const cacheDir = path.join(this.context.globalStorageUri.fsPath, "cache")
await fs.mkdir(cacheDir, { recursive: true })
return cacheDir
}

View File

@@ -1,12 +1,14 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { StdioClientTransport, StdioServerParameters } from "@modelcontextprotocol/sdk/client/stdio.js"
import {
ListResourcesResultSchema,
ListToolsResultSchema,
ListResourceTemplatesResultSchema,
ReadResourceResultSchema,
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"
@@ -23,7 +25,6 @@ import {
} from "../../shared/mcp"
import { fileExistsAtPath } from "../../utils/fs"
import { arePathsEqual } from "../../utils/path"
import delay from "delay"
export type McpConnection = {
server: McpServer
@@ -44,8 +45,9 @@ const McpSettingsSchema = z.object({
export class McpHub {
private providerRef: WeakRef<ClineProvider>
private settingsWatcher?: vscode.FileSystemWatcher
private disposables: vscode.Disposable[] = []
private settingsWatcher?: vscode.FileSystemWatcher
private fileWatchers: Map<string, FSWatcher> = new Map()
connections: McpConnection[] = []
isConnecting: boolean = false
@@ -158,6 +160,7 @@ export class McpHub {
...(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) => {
@@ -165,7 +168,7 @@ export class McpHub {
const connection = this.connections.find((conn) => conn.server.name === name)
if (connection) {
connection.server.status = "disconnected"
connection.server.error = error.message
this.appendErrorMessage(connection, error.message)
}
await this.notifyWebviewOfServerChanges()
}
@@ -207,8 +210,24 @@ export class McpHub {
}
this.connections.push(connection)
await client.connect(transport)
connection.server.status = "connected"
// 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) {
this.appendErrorMessage(connection, errorOutput)
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(
@@ -232,6 +251,11 @@ export class McpHub {
// },
// )
// 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)
@@ -241,12 +265,17 @@ export class McpHub {
const connection = this.connections.find((conn) => conn.server.name === name)
if (connection) {
connection.server.status = "disconnected"
connection.server.error = error instanceof Error ? error.message : String(error)
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
@@ -254,7 +283,7 @@ export class McpHub {
?.client.request({ method: "tools/list" }, ListToolsResultSchema)
return response?.tools || []
} catch (error) {
console.error(`Failed to fetch tools for ${serverName}:`, error)
// console.error(`Failed to fetch tools for ${serverName}:`, error)
return []
}
}
@@ -266,7 +295,7 @@ export class McpHub {
?.client.request({ method: "resources/list" }, ListResourcesResultSchema)
return response?.resources || []
} catch (error) {
console.error(`Failed to fetch resources for ${serverName}:`, error)
// console.error(`Failed to fetch resources for ${serverName}:`, error)
return []
}
}
@@ -278,7 +307,7 @@ export class McpHub {
?.client.request({ method: "resources/templates/list" }, ListResourceTemplatesResultSchema)
return response?.resourceTemplates || []
} catch (error) {
console.error(`Failed to fetch resource templates for ${serverName}:`, error)
// console.error(`Failed to fetch resource templates for ${serverName}:`, error)
return []
}
}
@@ -289,6 +318,8 @@ export class McpHub {
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) {
@@ -300,6 +331,7 @@ export class McpHub {
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))
@@ -316,15 +348,17 @@ export class McpHub {
const currentConnection = this.connections.find((conn) => conn.server.name === name)
if (!currentConnection) {
// New server - connect
// 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 - reconnect
// 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}`)
@@ -338,6 +372,30 @@ export class McpHub {
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()
@@ -349,13 +407,16 @@ export class McpHub {
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)
}
@@ -423,6 +484,7 @@ export class McpHub {
}
async dispose(): Promise<void> {
this.removeAllFileWatchers()
for (const connection of this.connections) {
try {
await this.deleteConnection(connection.server.name)

View File

@@ -195,7 +195,14 @@ const ServerRow = ({ server }: { server: McpServer }) => {
borderRadius: "0 0 4px 4px",
width: "100%",
}}>
<div style={{ color: "var(--vscode-testing-iconFailed)", marginBottom: "8px", padding: "0 10px" }}>
<div
style={{
color: "var(--vscode-testing-iconFailed)",
marginBottom: "8px",
padding: "0 10px",
overflowWrap: "break-word",
wordBreak: "break-word",
}}>
{server.error}
</div>
<VSCodeButton