Fix: prevent mcp operations losing var placeholders (#7144)

This commit is contained in:
shamoon
2026-09-16 07:08:20 -07:00
committed by GitHub
parent c764d05a0c
commit 3598869ef0
2 changed files with 56 additions and 8 deletions
+20 -8
View File
@@ -118,9 +118,19 @@ function readConfig(file) {
return existsSync(path) ? readFileSync(path, "utf8") : "";
}
const PLACEHOLDER_PATTERN = /(["']?)\{\{([^{}]*)\}\}\1/g;
const HOMEPAGE_KEY_PATTERN = /HOMEPAGE_(?:VAR|FILE)_/;
const PLACEHOLDER_TOKEN_PATTERN = /__HOMEPAGE_MCP_PLACEHOLDER_(\d+)__/g;
// mask {{HOMEPAGE_*}} placeholders so they survive a parse/dump round-trip
function parseYamlConfig(file) {
const parsed = loadYaml(readConfig(file) || "");
return parsed ?? [];
const placeholders = [];
const masked = readConfig(file).replace(PLACEHOLDER_PATTERN, (match, quote, key) => {
if (!HOMEPAGE_KEY_PATTERN.test(key)) return match;
placeholders.push(match);
return `__HOMEPAGE_MCP_PLACEHOLDER_${placeholders.length - 1}__`;
});
return { data: loadYaml(masked) ?? [], placeholders };
}
function validateYaml(file, content) {
@@ -166,8 +176,10 @@ function ensureWriteEnabled() {
return null;
}
function dumpYamlConfig(file, content) {
const dumped = yaml.dump(content, { lineWidth: -1, noRefs: true });
function dumpYamlConfig(file, content, placeholders) {
const dumped = yaml
.dump(content, { lineWidth: -1, noRefs: true })
.replace(PLACEHOLDER_TOKEN_PATTERN, (match, index) => placeholders[index] ?? match);
mkdirSync(CONF_DIR, { recursive: true });
writeFileSync(configPath(file), dumped, "utf8");
return dumped;
@@ -192,7 +204,7 @@ function addService(args) {
};
}
const services = parseYamlConfig("services.yaml");
const { data: services, placeholders } = parseYamlConfig("services.yaml");
if (!Array.isArray(services)) {
throw new Error("services.yaml must contain a top-level array");
}
@@ -220,7 +232,7 @@ function addService(args) {
}
group[groupName].push({ [serviceName]: serviceConfig });
const content = dumpYamlConfig("services.yaml", services);
const content = dumpYamlConfig("services.yaml", services, placeholders);
return textContent(
JSON.stringify({ written: "services.yaml", added: { group: groupName, service: serviceName }, content }, null, 2),
);
@@ -242,7 +254,7 @@ function addInfoWidget(args) {
};
}
const widgets = parseYamlConfig("widgets.yaml");
const { data: widgets, placeholders } = parseYamlConfig("widgets.yaml");
if (!Array.isArray(widgets)) {
throw new Error("widgets.yaml must contain a top-level array");
}
@@ -252,7 +264,7 @@ function addInfoWidget(args) {
assertPlainObject(options, "options");
widgets.push({ [type]: options });
const content = dumpYamlConfig("widgets.yaml", widgets);
const content = dumpYamlConfig("widgets.yaml", widgets, placeholders);
return textContent(JSON.stringify({ written: "widgets.yaml", added: { type }, content }, null, 2));
}
+36
View File
@@ -184,6 +184,42 @@ describe("utils/mcp/homepage-mcp", () => {
);
});
it("preserves env placeholders when adding a service", async () => {
process.env.HOMEPAGE_MCP_ALLOW_WRITE = "true";
const configDir = mkdtempSync(path.join(tmpdir(), "homepage-mcp-test-"));
writeFileSync(
path.join(configDir, "services.yaml"),
"- Media:\n" +
" - Jellyfin:\n" +
" href: http://{{HOMEPAGE_VAR_HOST}}:8096\n" +
" widget:\n" +
" type: jellyfin\n" +
" key: {{HOMEPAGE_VAR_JELLYFIN_KEY}}\n" +
' password: "{{HOMEPAGE_FILE_PASSWORD}}"\n',
);
const mod = await loadMcpWithConfigDir(configDir);
const response = mod.handleMcpRequest({
jsonrpc: "2.0",
id: 11,
method: "tools/call",
params: { name: "add_service", arguments: { group: "Tools", name: "Grafana" } },
});
expect(response.result.isError).toBeUndefined();
expect(readFileSync(path.join(configDir, "services.yaml"), "utf8")).toBe(
"- Media:\n" +
" - Jellyfin:\n" +
" href: http://{{HOMEPAGE_VAR_HOST}}:8096\n" +
" widget:\n" +
" type: jellyfin\n" +
" key: {{HOMEPAGE_VAR_JELLYFIN_KEY}}\n" +
' password: "{{HOMEPAGE_FILE_PASSWORD}}"\n' +
"- Tools:\n" +
" - Grafana: {}\n",
);
});
it("does not add a duplicate service in the same group", async () => {
process.env.HOMEPAGE_MCP_ALLOW_WRITE = "true";
const configDir = mkdtempSync(path.join(tmpdir(), "homepage-mcp-test-"));