Skip to content

Commit 139a449

Browse files
daniel-lxsvalekseev
authored andcommitted
fix: normalize Windows paths in MCP variable injection (RooCodeInc#4739) (RooCodeInc#4741)
1 parent faf62ac commit 139a449

File tree

2 files changed

+120
-9
lines changed

2 files changed

+120
-9
lines changed

src/utils/__tests__/config.spec.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,5 +147,109 @@ describe("injectVariables", () => {
147147
expect(result).toEqual("Hello ")
148148
})
149149

150+
it("should normalize Windows paths with backslashes to use forward slashes in JSON objects", async () => {
151+
const config = {
152+
command: "mcp-server",
153+
args: ["${workspaceFolder}"],
154+
}
155+
const result = await injectVariables(config, { workspaceFolder: "C:\\Users\\project" })
156+
expect(result).toEqual({
157+
command: "mcp-server",
158+
args: ["C:/Users/project"],
159+
})
160+
})
161+
162+
it("should handle complex Windows paths in nested objects", async () => {
163+
const config = {
164+
servers: {
165+
git: {
166+
command: "node",
167+
args: ["${workspaceFolder}\\scripts\\mcp.js", "${workspaceFolder}\\data"],
168+
},
169+
},
170+
}
171+
const result = await injectVariables(config, { workspaceFolder: "C:\\Program Files\\My Project" })
172+
expect(result).toEqual({
173+
servers: {
174+
git: {
175+
command: "node",
176+
args: ["C:/Program Files/My Project\\scripts\\mcp.js", "C:/Program Files/My Project\\data"],
177+
},
178+
},
179+
})
180+
})
181+
182+
it("should handle Windows paths when entire path is a variable", async () => {
183+
const config = {
184+
servers: {
185+
git: {
186+
command: "node",
187+
args: ["${scriptPath}", "${dataPath}"],
188+
},
189+
},
190+
}
191+
const result = await injectVariables(config, {
192+
scriptPath: "C:\\Program Files\\My Project\\scripts\\mcp.js",
193+
dataPath: "C:\\Program Files\\My Project\\data",
194+
})
195+
expect(result).toEqual({
196+
servers: {
197+
git: {
198+
command: "node",
199+
args: ["C:/Program Files/My Project/scripts/mcp.js", "C:/Program Files/My Project/data"],
200+
},
201+
},
202+
})
203+
})
204+
205+
it("should normalize backslashes in plain string replacements", async () => {
206+
const result = await injectVariables("Path: ${path}", { path: "C:\\Users\\test" })
207+
expect(result).toEqual("Path: C:/Users/test")
208+
})
209+
210+
it("should handle paths with mixed slashes", async () => {
211+
const config = {
212+
path: "${testPath}",
213+
}
214+
const result = await injectVariables(config, { testPath: "C:\\Users/test/mixed\\path" })
215+
expect(result).toEqual({
216+
path: "C:/Users/test/mixed/path",
217+
})
218+
})
219+
220+
it("should not affect non-path strings", async () => {
221+
const config = {
222+
message: "This is a string with a backslash \\ and a value: ${myValue}",
223+
}
224+
const result = await injectVariables(config, { myValue: "test" })
225+
expect(result).toEqual({
226+
message: "This is a string with a backslash \\ and a value: test",
227+
})
228+
})
229+
230+
it("should handle various non-path variables correctly", async () => {
231+
const config = {
232+
apiKey: "${key}",
233+
url: "${endpoint}",
234+
port: "${port}",
235+
enabled: "${enabled}",
236+
description: "${desc}",
237+
}
238+
const result = await injectVariables(config, {
239+
key: "sk-1234567890abcdef",
240+
endpoint: "https://api.example.com",
241+
port: "8080",
242+
enabled: "true",
243+
desc: "This is a description with special chars: @#$%^&*()",
244+
})
245+
expect(result).toEqual({
246+
apiKey: "sk-1234567890abcdef",
247+
url: "https://api.example.com",
248+
port: "8080",
249+
enabled: "true",
250+
description: "This is a description with special chars: @#$%^&*()",
251+
})
252+
})
253+
150254
// Variable maps are already tested by `injectEnv` tests above.
151255
})

src/utils/config.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,23 +37,30 @@ export async function injectVariables<C extends InjectableConfigType>(
3737
variables: Record<string, undefined | null | string | Record<string, undefined | null | string>>,
3838
propNotFoundValue?: any,
3939
) {
40-
// Use simple regex replace for now, will see if object traversal and recursion is needed here (e.g: for non-serializable objects)
4140
const isObject = typeof config === "object"
42-
let _config: string = isObject ? JSON.stringify(config) : config
41+
let configString: string = isObject ? JSON.stringify(config) : config
4342

44-
// Intentionally using `== null` to match null | undefined
4543
for (const [key, value] of Object.entries(variables)) {
4644
if (value == null) continue
4745

48-
if (typeof value === "string") _config = _config.replace(new RegExp(`\\$\\{${key}\\}`, "g"), value)
49-
else
50-
_config = _config.replace(new RegExp(`\\$\\{${key}:([\\w]+)\\}`, "g"), (match, name) => {
51-
if (value[name] == null)
46+
if (typeof value === "string") {
47+
// Normalize paths to forward slashes for cross-platform compatibility
48+
configString = configString.replace(new RegExp(`\\$\\{${key}\\}`, "g"), value.toPosix())
49+
} else {
50+
// Handle nested variables (e.g., ${env:VAR_NAME})
51+
configString = configString.replace(new RegExp(`\\$\\{${key}:([\\w]+)\\}`, "g"), (match, name) => {
52+
const nestedValue = value[name]
53+
54+
if (nestedValue == null) {
5255
console.warn(`[injectVariables] variable "${name}" referenced but not found in "${key}"`)
56+
return propNotFoundValue ?? match
57+
}
5358

54-
return value[name] ?? propNotFoundValue ?? match
59+
// Normalize paths for string values
60+
return typeof nestedValue === "string" ? nestedValue.toPosix() : nestedValue
5561
})
62+
}
5663
}
5764

58-
return (isObject ? JSON.parse(_config) : _config) as C extends string ? string : C
65+
return (isObject ? JSON.parse(configString) : configString) as C extends string ? string : C
5966
}

0 commit comments

Comments
 (0)