Skip to content

Commit cb0deb2

Browse files
committed
Align with the original mcp functions
1 parent 6bccb80 commit cb0deb2

File tree

6 files changed

+224
-126
lines changed

6 files changed

+224
-126
lines changed

src/core/prompts/instructions/create-mcp-server.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export async function createMCPServerInstructions(
1111
1212
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).
1313
14-
Unless the user specifies otherwise, new local MCP servers should be created in: /mock/settings/path
14+
Unless the user specifies otherwise, new local MCP servers should be created in: ${await mcpHub.getMcpServersPath()}
1515
1616
### MCP Server Types and Configuration
1717
@@ -60,7 +60,7 @@ The following example demonstrates how to build a local MCP server that provides
6060
1. Use the \`create-typescript-server\` tool to bootstrap a new project in the default MCP servers directory:
6161
6262
\`\`\`bash
63-
cd /mock/settings/path
63+
cd ${await mcpHub.getMcpServersPath()}
6464
npx @modelcontextprotocol/create-server weather-server
6565
cd weather-server
6666
# Install dependencies
@@ -360,7 +360,7 @@ npm run build
360360
361361
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.
362362
363-
5. Install the MCP Server by adding the MCP server configuration to the settings file located at '/mock/settings/path/cline_mcp_settings.json'. 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.
363+
5. Install the MCP Server by adding the MCP server configuration to the settings file located at '${await mcpHub.getGlobalMcpSettingsFilePath()}'. 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.
364364
365365
IMPORTANT: Regardless of what else you see in the MCP settings file, you must default any new MCP servers you create to disabled=false and alwaysAllow=[].
366366

src/core/webview/ClineProvider.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
110110
McpServerManager.getInstance(this.context, this)
111111
.then((hub) => {
112112
this.mcpHub = hub
113+
this.mcpHub.registerClient()
113114
})
114115
.catch((error) => {
115116
this.log(`Failed to initialize MCP Hub: ${error}`)
@@ -216,7 +217,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
216217

217218
this._workspaceTracker?.dispose()
218219
this._workspaceTracker = undefined
219-
await this.mcpHub?.dispose()
220+
await this.mcpHub?.unregisterClient()
220221
this.mcpHub = undefined
221222
this.customModesManager?.dispose()
222223
this.log("Disposed all disposables")

src/services/mcp/McpHub.ts

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import * as vscode from "vscode"
22
import { ClineProvider } from "../../core/webview/ClineProvider"
3-
import { ConfigManager } from "./config"
43
import type { ConfigChangeEvent } from "./config"
5-
import { ConnectionFactory, ConnectionManager, StdioHandler, SseHandler } from "./connection"
6-
import { McpServer, McpToolCallResponse, McpResourceResponse, ServerConfig, ConfigSource, McpConnection } from "./types"
4+
import { ConfigManager } from "./config"
5+
import { ConnectionFactory, ConnectionManager, SseHandler, StdioHandler } from "./connection"
6+
import { ConfigSource, McpConnection, McpResourceResponse, McpServer, McpToolCallResponse, ServerConfig } from "./types"
77

88
export class McpHub {
99
private configManager: ConfigManager
1010
private connectionManager: ConnectionManager
1111
private disposables: vscode.Disposable[] = []
1212
private providerRef: WeakRef<ClineProvider>
1313
private isConnectingFlag = false
14+
private refCount: number = 0 // Reference counter for active clients
15+
private isDisposed = false // Flag to prevent multiple disposals
1416

1517
constructor(private provider: ClineProvider) {
1618
this.providerRef = new WeakRef(provider)
@@ -47,6 +49,41 @@ export class McpHub {
4749
void this.configManager.watchConfigFiles(provider)
4850
}
4951

52+
/**
53+
* Registers a client (e.g., ClineProvider) using this hub.
54+
* Increments the reference count.
55+
*/
56+
public registerClient(): void {
57+
this.refCount++
58+
console.log(`McpHub: Client registered. Ref count: ${this.refCount}`)
59+
}
60+
61+
/**
62+
* Unregisters a client. Decrements the reference count.
63+
* If the count reaches zero, disposes the hub.
64+
*/
65+
public async unregisterClient(): Promise<void> {
66+
this.refCount--
67+
console.log(`McpHub: Client unregistered. Ref count: ${this.refCount}`)
68+
if (this.refCount <= 0) {
69+
console.log("McpHub: Last client unregistered. Disposing hub.")
70+
await this.dispose()
71+
}
72+
}
73+
74+
/**
75+
* Get the path where MCP servers should be stored
76+
* @returns Path to MCP servers directory
77+
*/
78+
async getMcpServersPath(): Promise<string> {
79+
const provider = this.providerRef.deref()
80+
if (!provider) {
81+
throw new Error("Provider not available")
82+
}
83+
const mcpServersPath = await provider.ensureMcpServersDirectoryExists()
84+
return mcpServersPath
85+
}
86+
5087
/**
5188
* Execute a server action with common checks.
5289
*/
@@ -73,7 +110,9 @@ export class McpHub {
73110
? await this.configManager.getGlobalConfigPath(this.provider)
74111
: await this.configManager.getProjectConfigPath()
75112
if (!configPath) throw new Error(`Cannot get config path for source: ${source}`)
76-
return configPath
113+
// Normalize path for cross-platform compatibility
114+
// Use a consistent path format for both reading and writing
115+
return process.platform === "win32" ? configPath.replace(/\\/g, "/") : configPath
77116
}
78117

79118
/**
@@ -111,6 +150,14 @@ export class McpHub {
111150
return this.configManager.getGlobalConfigPath(provider)
112151
}
113152

153+
async getGlobalMcpSettingsFilePath(): Promise<string> {
154+
const provider = this.providerRef.deref()
155+
if (!provider) {
156+
throw new Error("Provider not available")
157+
}
158+
return this.getGlobalConfigPath(provider)
159+
}
160+
114161
async callTool(
115162
serverName: string,
116163
toolName: string,
@@ -292,7 +339,35 @@ export class McpHub {
292339
}
293340

294341
async dispose(): Promise<void> {
295-
await this.connectionManager.dispose()
296-
this.disposables.forEach((d) => d.dispose())
342+
// Prevent multiple disposals
343+
if (this.isDisposed) {
344+
console.log("McpHub: Already disposed.")
345+
return
346+
}
347+
348+
// Check for active clients
349+
if (this.refCount > 0) {
350+
console.log(`McpHub: Cannot dispose, still has ${this.refCount} active clients`)
351+
return
352+
}
353+
354+
console.log("McpHub: Disposing...")
355+
this.isDisposed = true
356+
357+
try {
358+
// Dispose connection manager (includes file watchers and connections)
359+
await this.connectionManager.dispose()
360+
} catch (error) {
361+
console.error("Failed to dispose connection manager:", error)
362+
}
363+
364+
// Dispose all other disposables
365+
for (const disposable of this.disposables) {
366+
try {
367+
disposable.dispose()
368+
} catch (error) {
369+
console.error("Failed to dispose disposable:", error)
370+
}
371+
}
297372
}
298373
}

src/services/mcp/config/ConfigManager.ts

Lines changed: 3 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { GlobalFileNames } from "../../../shared/globalFileNames"
99
import { fileExistsAtPath } from "../../../utils/fs"
1010
import { ServerConfig, McpServer, ConfigSource } from "../types"
1111
import { ConfigChangeEvent, ConfigChangeListener } from "./types"
12+
import { safeParseSeverConfig } from "./validation"
1213

1314
/**
1415
* Configuration Manager
@@ -27,26 +28,7 @@ export class ConfigManager {
2728
/** Configuration file path cache */
2829
private configPaths: Partial<Record<ConfigSource, string>> = {}
2930

30-
// Validation schemas
31-
private readonly ServerConfigSchema = z.object({
32-
type: z.enum(["stdio", "sse"]),
33-
command: z.string().optional(),
34-
args: z.array(z.string()).optional(),
35-
env: z.record(z.string()).optional(),
36-
url: z.string().optional(),
37-
headers: z.record(z.string()).optional(),
38-
disabled: z.boolean().optional(),
39-
timeout: z
40-
.number()
41-
.optional()
42-
.refine(
43-
(val) => val === undefined || (val >= 0 && val <= 3600),
44-
(val) => ({ message: `Timeout must be between 0 and 3600 seconds, got ${val}` }),
45-
),
46-
alwaysAllow: z.array(z.string()).optional(),
47-
watchPaths: z.array(z.string()).optional(),
48-
})
49-
31+
// Validation schema for MCP settings
5032
private readonly McpSettingsSchema = z.object({
5133
mcpServers: z.record(z.any()),
5234
})
@@ -131,12 +113,6 @@ export class ConfigManager {
131113
}
132114
}
133115

134-
private inferServerType(config: Record<string, unknown>): "stdio" | "sse" | undefined {
135-
if (config.command) return "stdio"
136-
if (config.url) return "sse"
137-
return undefined
138-
}
139-
140116
/**
141117
* Validate server configuration
142118
* @param config Configuration object to validate
@@ -146,23 +122,7 @@ export class ConfigManager {
146122
public validateServerConfig(config: unknown, serverName?: string): ServerConfig {
147123
try {
148124
const configCopy = { ...(config as Record<string, unknown>) }
149-
150-
if (!configCopy.type) {
151-
configCopy.type = this.inferServerType(configCopy)
152-
}
153-
154-
const hasStdioFields = configCopy.command !== undefined
155-
const hasSseFields = configCopy.url !== undefined
156-
157-
if (hasStdioFields && hasSseFields) {
158-
throw new Error(t("common:errors.invalid_mcp_config"))
159-
}
160-
161-
if (!hasStdioFields && !hasSseFields) {
162-
throw new Error(t("common:errors.invalid_mcp_config"))
163-
}
164-
165-
const result = this.ServerConfigSchema.safeParse(configCopy)
125+
const result = safeParseSeverConfig(configCopy)
166126
if (!result.success) {
167127
const errors = result.error.errors.map((err) => `${err.path.join(".")}: ${err.message}`).join(", ")
168128
throw new Error(t("common:errors.invalid_mcp_settings_validation", { errorMessages: errors }))
@@ -190,14 +150,6 @@ export class ConfigManager {
190150
throw new Error(t("common:errors.invalid_mcp_settings_syntax"))
191151
}
192152

193-
if (config.mcpServers && typeof config.mcpServers === "object") {
194-
Object.values(config.mcpServers).forEach((server: any) => {
195-
if (!server.type) {
196-
server.type = this.inferServerType(server)
197-
}
198-
})
199-
}
200-
201153
const result = this.McpSettingsSchema.safeParse(config)
202154
if (!result.success) {
203155
const errors = result.error.errors.map((err) => `${err.path.join(".")}: ${err.message}`).join("\n")
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { z } from "zod"
2+
import { ServerConfig } from "../types"
3+
import * as vscode from "vscode"
4+
5+
const typeErrorMessage = "Server type must match the provided configuration"
6+
7+
const BaseConfigSchema = z.object({
8+
disabled: z.boolean().optional(),
9+
timeout: z.number().optional(),
10+
alwaysAllow: z.array(z.string()).optional(),
11+
watchPaths: z.array(z.string()).optional(),
12+
})
13+
14+
const createServerConfigSchema = () => {
15+
return z.union([
16+
// Stdio config (has command field)
17+
BaseConfigSchema.extend({
18+
type: z.enum(["stdio"]).optional(),
19+
command: z.string().min(1, "Command cannot be empty"),
20+
args: z.array(z.string()).optional(),
21+
cwd: z.string().default(() => vscode.workspace.workspaceFolders?.at(0)?.uri.fsPath ?? process.cwd()),
22+
env: z.record(z.string()).optional(),
23+
// Ensure no SSE fields are present
24+
url: z.undefined().optional(),
25+
headers: z.undefined().optional(),
26+
})
27+
.transform((data) => ({
28+
...data,
29+
type: "stdio" as const,
30+
}))
31+
.refine((data) => data.type === undefined || data.type === "stdio", { message: typeErrorMessage }),
32+
33+
// SSE config (has url field)
34+
BaseConfigSchema.extend({
35+
type: z.enum(["sse"]).optional(),
36+
url: z.string().url("URL must be a valid URL format"),
37+
headers: z.record(z.string()).optional(),
38+
// Ensure no stdio fields are present
39+
command: z.undefined().optional(),
40+
args: z.undefined().optional(),
41+
cwd: z.undefined().optional(),
42+
env: z.undefined().optional(),
43+
})
44+
.transform((data) => ({
45+
...data,
46+
type: "sse" as const,
47+
}))
48+
.refine((data) => data.type === undefined || data.type === "sse", { message: typeErrorMessage }),
49+
])
50+
}
51+
52+
/**
53+
* Validates a server configuration object.
54+
* @param config The configuration object to validate
55+
* @returns The validated server configuration
56+
* @throws {ZodError} If validation fails
57+
*/
58+
export const validateServerConfig = (config: unknown): ServerConfig => {
59+
return createServerConfigSchema().parse(config)
60+
}
61+
62+
/**
63+
* Safely validates a server configuration object.
64+
* @param config The configuration object to validate
65+
* @returns The validation result
66+
*/
67+
export const safeParseSeverConfig = (config: unknown): z.SafeParseReturnType<unknown, ServerConfig> => {
68+
return createServerConfigSchema().safeParse(config)
69+
}

0 commit comments

Comments
 (0)