-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmcp-migrator.ts
More file actions
175 lines (147 loc) · 5.59 KB
/
Copy pathmcp-migrator.ts
File metadata and controls
175 lines (147 loc) · 5.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
import * as fs from "fs/promises"
import * as path from "path"
import { Config } from "../config/config"
import { Log } from "../util/log"
import { Filesystem } from "../util/filesystem"
import { KilocodePaths } from "./paths"
export namespace McpMigrator {
const log = Log.create({ service: "kilocode.mcp-migrator" })
// Remote transport types used by the Kilocode extension
const REMOTE_TYPES = new Set(["streamable-http", "sse"])
function isRemote(server: KilocodeMcpServer): boolean {
return !!server.type && REMOTE_TYPES.has(server.type)
}
// Kilocode MCP server structure
export interface KilocodeMcpServer {
command?: string
args?: string[]
env?: Record<string, string>
disabled?: boolean
alwaysAllow?: string[]
// Remote server fields
type?: string
url?: string
headers?: Record<string, string>
}
export interface KilocodeMcpSettings {
mcpServers: Record<string, KilocodeMcpServer>
}
export interface MigrationResult {
mcp: Record<string, Config.Mcp>
warnings: string[]
skipped: Array<{ name: string; reason: string }>
}
export async function readMcpSettings(filepath: string): Promise<KilocodeMcpSettings | null> {
if (!(await Filesystem.exists(filepath))) return null
const content = await fs.readFile(filepath, "utf-8")
return JSON.parse(content) as KilocodeMcpSettings
}
export function convertServer(name: string, server: KilocodeMcpServer): Config.Mcp | null {
// Skip disabled servers
if (server.disabled) return null
if (isRemote(server)) {
if (!server.url) {
log.warn("remote MCP server missing url, skipping", { name })
return null
}
const config: Config.Mcp = {
type: "remote",
url: server.url,
...(server.headers && Object.keys(server.headers).length > 0 && { headers: server.headers }),
}
return config
}
if (!server.command) {
log.warn("local MCP server missing command, skipping", { name })
return null
}
// Build command array: [command, ...args]
const command = [server.command, ...(server.args ?? [])]
// Build the MCP config object
const config: Config.Mcp = {
type: "local",
command,
...(server.env && Object.keys(server.env).length > 0 && { environment: server.env }),
}
return config
}
export async function migrate(options?: {
projectDir?: string
skipGlobalPaths?: boolean
}): Promise<MigrationResult> {
const warnings: string[] = []
const skipped: Array<{ name: string; reason: string }> = []
const mcp: Record<string, Config.Mcp> = {}
const allServers: Array<{ name: string; server: KilocodeMcpServer }> = []
if (!options?.skipGlobalPaths) {
// 1. VSCode extension global storage (primary location for global MCP settings)
const vscodeSettingsPath = path.join(KilocodePaths.vscodeGlobalStorage(), "settings", "mcp_settings.json")
const vscodeSettings = await readMcpSettings(vscodeSettingsPath)
if (vscodeSettings?.mcpServers) {
for (const [name, server] of Object.entries(vscodeSettings.mcpServers)) {
allServers.push({ name, server })
}
}
}
// 2. Project-level MCP settings (if projectDir provided)
// The Kilocode extension uses ".kilocode/mcp.json" for project-level settings
// (not "mcp_settings.json" which is only used for global settings)
if (options?.projectDir) {
const projectSettingsPath = path.join(options.projectDir, ".kilocode", "mcp.json")
const projectSettings = await readMcpSettings(projectSettingsPath)
if (projectSettings?.mcpServers) {
for (const [name, server] of Object.entries(projectSettings.mcpServers)) {
allServers.push({ name, server }) // Later entries win in deduplication
}
}
}
// Deduplicate by name (later entries win - project overrides global)
const serversByName = new Map<string, KilocodeMcpServer>()
for (const { name, server } of allServers) {
serversByName.set(name, server)
}
// Convert each server
for (const [name, server] of serversByName) {
if (server.disabled) {
skipped.push({ name, reason: "Server is disabled" })
continue
}
// Warn about alwaysAllow permissions that cannot be migrated
if (server.alwaysAllow && server.alwaysAllow.length > 0) {
warnings.push(
`MCP server '${name}' has alwaysAllow permissions that cannot be migrated: ${server.alwaysAllow.join(", ")}`,
)
}
const converted = convertServer(name, server)
if (converted) {
mcp[name] = converted
}
}
return { mcp, warnings, skipped }
}
/**
* Load Kilocode MCP servers and return them as an opencode config partial.
* This function handles all logging internally, so callers just need to merge the result.
*/
export async function loadMcpConfig(projectDir: string): Promise<Record<string, Config.Mcp>> {
try {
const result = await migrate({ projectDir })
if (Object.keys(result.mcp).length > 0) {
log.debug("loaded kilocode MCP servers", {
count: Object.keys(result.mcp).length,
servers: Object.keys(result.mcp),
})
}
for (const skipped of result.skipped) {
log.debug("skipped kilocode MCP server", { name: skipped.name, reason: skipped.reason })
}
for (const warning of result.warnings) {
log.warn("kilocode MCP migration warning", { warning })
}
return result.mcp
} catch (err) {
log.warn("failed to load kilocode MCP servers", { error: err })
return {}
}
}
}