mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-21 14:07:20 +08:00
Merge pull request #8142 from Kilo-Org/mark/reimplement-mcp-removal
feat(vscode): reimplement MCP removal in agent behaviour settings
This commit is contained in:
@@ -35,7 +35,7 @@ import {
|
||||
import { GitOps } from "./agent-manager/GitOps"
|
||||
import { GitStatsPoller, type LocalStats } from "./agent-manager/GitStatsPoller"
|
||||
import { getWorkspaceRoot } from "./review-utils"
|
||||
import { MarketplaceService } from "./services/marketplace"
|
||||
import { MarketplaceService, type MarketplaceItem, type RemoveResult } from "./services/marketplace"
|
||||
import { resolveProjectDirectory } from "./project-directory"
|
||||
import { getBusySessionCount, seedSessionStatuses } from "./session-status"
|
||||
import { retry } from "./services/cli-backend/retry"
|
||||
@@ -970,12 +970,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
break
|
||||
}
|
||||
case "removeInstalledMarketplaceItem": {
|
||||
const workspace = this.getProjectDirectory(this.currentSession?.id)
|
||||
const scope = message.mpInstallOptions?.target ?? "project"
|
||||
const result = await this.getMarketplace().remove(message.mpItem, scope, workspace)
|
||||
if (result.success) {
|
||||
await this.invalidateAfterMarketplaceChange(scope)
|
||||
}
|
||||
const result = await this.removeMarketplaceItem(message.mpItem, scope)
|
||||
this.postMessage({
|
||||
type: "marketplaceRemoveResult",
|
||||
success: result.success,
|
||||
@@ -1747,59 +1743,87 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
*/
|
||||
private async handleRemoveMode(name: string): Promise<void> {
|
||||
if (!this.client) return
|
||||
let removed = false
|
||||
|
||||
// 1. Try CLI removal (handles .md files and legacy .kilocodemodes)
|
||||
try {
|
||||
const dir = this.getWorkspaceDirectory()
|
||||
const result = await this.client.kilocode.removeAgent({ name, directory: dir })
|
||||
if (!result.error) removed = true
|
||||
if (!result.error) {
|
||||
this.cachedAgentsMessage = null
|
||||
await this.fetchAndSendAgents()
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
// CLI removal failed — agent may be in kilo.json instead
|
||||
}
|
||||
|
||||
// 2. Try removing from kilo.json (handles marketplace-installed modes)
|
||||
if (!removed) {
|
||||
const workspace = this.getProjectDirectory(this.currentSession?.id)
|
||||
const mp = this.getMarketplace()
|
||||
const stub = { id: name, type: "mode" as const, name, description: "", content: "" }
|
||||
const project = await mp.remove(stub, "project", workspace)
|
||||
const global = await mp.remove(stub, "global", workspace)
|
||||
if (project.success || global.success) {
|
||||
await this.disposeCliInstance("global")
|
||||
removed = true
|
||||
}
|
||||
}
|
||||
|
||||
const stub = { id: name, type: "mode" as const, name, description: "", content: "" }
|
||||
const removed = await this.removeMarketplaceItemFromAllScopes(stub)
|
||||
if (!removed) {
|
||||
console.error("[Kilo New] KiloProvider: Failed to remove mode:", name)
|
||||
}
|
||||
|
||||
this.cachedAgentsMessage = null
|
||||
await this.fetchAndSendAgents()
|
||||
}
|
||||
|
||||
private async handleRemoveMcp(name: string): Promise<void> {
|
||||
const workspace = this.getProjectDirectory(this.currentSession?.id)
|
||||
const mp = this.getMarketplace()
|
||||
// Remove from legacy files first so that the subsequent invalidation
|
||||
// causes the CLI to re-read config without the legacy entry.
|
||||
await this.removeLegacyMcp(name)
|
||||
|
||||
const stub = { id: name, type: "mcp" as const, name, description: "", url: "", content: "" }
|
||||
|
||||
// Remove from both scopes — an MCP could exist in project, global, or both
|
||||
const project = await mp.remove(stub, "project", workspace)
|
||||
const global = await mp.remove(stub, "global", workspace)
|
||||
|
||||
if (project.success || global.success) {
|
||||
// Use global scope when removed from global (or both) so the global
|
||||
// config cache is also invalidated; project scope is a subset.
|
||||
const scope = global.success ? "global" : "project"
|
||||
await this.disposeCliInstance(scope)
|
||||
this.cachedConfigMessage = null
|
||||
await this.fetchAndSendConfig()
|
||||
} else {
|
||||
const removed = await this.removeMarketplaceItemFromAllScopes(stub)
|
||||
if (!removed) {
|
||||
console.error("[Kilo New] KiloProvider: Failed to remove MCP server:", name)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an MCP server from legacy config files (.kilo/mcp.json, .kilocode/mcp.json,
|
||||
* and the VS Code global storage mcp_settings.json). These files are read by the
|
||||
* CLI-side McpMigrator and merged into config at the lowest precedence level.
|
||||
* Returns true if the entry was found and removed from at least one file.
|
||||
*/
|
||||
private async removeLegacyMcp(name: string): Promise<boolean> {
|
||||
const workspace = this.getProjectDirectory(this.currentSession?.id)
|
||||
const files: vscode.Uri[] = []
|
||||
|
||||
// Project-level legacy files
|
||||
if (workspace) {
|
||||
files.push(vscode.Uri.file(path.join(workspace, ".kilo", "mcp.json")))
|
||||
files.push(vscode.Uri.file(path.join(workspace, ".kilocode", "mcp.json")))
|
||||
}
|
||||
|
||||
// Global legacy file (VS Code extension global storage)
|
||||
const storage = this.extensionContext?.globalStorageUri
|
||||
if (storage) {
|
||||
files.push(vscode.Uri.joinPath(storage, "settings", "mcp_settings.json"))
|
||||
}
|
||||
|
||||
let removed = false
|
||||
for (const uri of files) {
|
||||
const bytes = await vscode.workspace.fs.readFile(uri).then(
|
||||
(b) => b,
|
||||
() => null,
|
||||
)
|
||||
if (!bytes) continue
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(Buffer.from(bytes).toString("utf8")) as Record<string, unknown>
|
||||
const servers = parsed.mcpServers as Record<string, unknown> | undefined
|
||||
if (!servers?.[name]) continue
|
||||
|
||||
delete servers[name]
|
||||
const content = Buffer.from(JSON.stringify(parsed, null, 2), "utf8")
|
||||
await vscode.workspace.fs.writeFile(uri, content)
|
||||
removed = true
|
||||
} catch (err) {
|
||||
console.warn("[Kilo New] KiloProvider: Failed to remove legacy MCP from", uri.fsPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return removed
|
||||
}
|
||||
|
||||
private async fetchAndSendMcpStatus(): Promise<void> {
|
||||
if (!this.client) {
|
||||
if (this.cachedMcpStatusMessage) {
|
||||
@@ -1846,23 +1870,35 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose the CLI backend instance so it re-reads config from disk.
|
||||
* Call after any marketplace install/remove that writes config files directly.
|
||||
* Global-scope changes need global.dispose() to also reset the global config cache.
|
||||
* Remove a marketplace item from a single scope and invalidate CLI caches.
|
||||
*/
|
||||
private async disposeCliInstance(scope: "project" | "global"): Promise<void> {
|
||||
if (!this.client) return
|
||||
if (scope === "global") {
|
||||
await this.client.global.dispose().catch((e: unknown) => {
|
||||
console.warn("[Kilo New] global.dispose() after marketplace change failed:", e)
|
||||
})
|
||||
private async removeMarketplaceItem(item: MarketplaceItem, scope: "project" | "global"): Promise<RemoveResult> {
|
||||
const workspace = this.getProjectDirectory(this.currentSession?.id)
|
||||
const result = await this.getMarketplace().remove(item, scope, workspace)
|
||||
if (result.success) {
|
||||
await this.invalidateAfterMarketplaceChange(scope)
|
||||
}
|
||||
// Always dispose the per-project instance so it rebuilds state from
|
||||
// the (possibly updated) global + project config on the next request.
|
||||
const dir = this.getWorkspaceDirectory()
|
||||
await this.client.instance.dispose({ directory: dir }).catch((e: unknown) => {
|
||||
console.warn("[Kilo New] instance.dispose() after marketplace change failed:", e)
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a marketplace item from both project and global scopes.
|
||||
* mp.remove returns success even when the entry doesn't exist (no-op),
|
||||
* so we must attempt both scopes to cover dual-scope installations.
|
||||
* Returns true if at least one scope removal succeeded.
|
||||
*/
|
||||
private async removeMarketplaceItemFromAllScopes(item: MarketplaceItem): Promise<boolean> {
|
||||
const workspace = this.getProjectDirectory(this.currentSession?.id)
|
||||
const mp = this.getMarketplace()
|
||||
const project = await mp.remove(item, "project", workspace)
|
||||
const global = await mp.remove(item, "global", workspace)
|
||||
|
||||
if (project.success || global.success) {
|
||||
const scope = global.success ? "global" : "project"
|
||||
await this.invalidateAfterMarketplaceChange(scope)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user