fix: enforce executable permissions on binary entry points during pack

When server.type is "binary", mcpb pack now ensures the entry_point
file has execute bits (|0o111) in the ZIP external_attr, regardless
of the source filesystem permissions. This prevents EACCES failures
when Claude Desktop extracts Windows-built binary MCPBs (which store
644 because Windows has no Unix execute bit).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Thompson 2026-03-27 09:18:30 -05:00
commit 122ef0e9e9
No known key found for this signature in database
GPG key ID: 1D66D91917EE02E6

View file

@ -357,5 +357,82 @@ describe("DXT CLI", () => {
}
}
});
it("should add group+other execute bits on binary entry points during pack", () => {
// Skip this test on Windows since it doesn't support Unix permissions
if (process.platform === "win32") {
return;
}
const tempBinDir = join(__dirname, "temp-binary-entry-test");
const binPackedFilePath = join(__dirname, "test-binary-entry.dxt");
const binUnpackedDir = join(__dirname, "temp-binary-entry-unpack");
try {
// Create a binary-type extension with owner-only execute (0o700)
// This passes validation (has some +x) but pack should add
// group+other execute bits for compatibility with CD extraction
fs.mkdirSync(join(tempBinDir, "server"), { recursive: true });
fs.writeFileSync(
join(tempBinDir, "manifest.json"),
JSON.stringify({
manifest_version: DEFAULT_MANIFEST_VERSION,
name: "Test Binary Extension",
version: "1.0.0",
description: "A test extension with binary entry point",
author: {
name: "MCPB",
},
server: {
type: "binary",
entry_point: "server/myserver",
mcp_config: {
command: "${__dirname}/server/myserver",
},
},
}),
);
// Create binary entry point with 700 (owner-only execute)
fs.writeFileSync(
join(tempBinDir, "server", "myserver"),
"#!/bin/sh\necho binary",
);
fs.chmodSync(join(tempBinDir, "server", "myserver"), 0o700);
// Create a regular file for comparison (should stay 644)
fs.writeFileSync(join(tempBinDir, "config.json"), "{}");
fs.chmodSync(join(tempBinDir, "config.json"), 0o644);
// Pack the extension
execSync(`node ${cliPath} pack ${tempBinDir} ${binPackedFilePath}`, {
encoding: "utf-8",
});
// Unpack to check stored permissions
execSync(
`node ${cliPath} unpack ${binPackedFilePath} ${binUnpackedDir}`,
{
encoding: "utf-8",
},
);
// Binary entry point should have full execute bits (700 | 111 = 711)
const entryStats = fs.statSync(
join(binUnpackedDir, "server", "myserver"),
);
expect(entryStats.mode & 0o777).toBe(0o711);
// Non-entry-point file should retain original permissions
const configStats = fs.statSync(join(binUnpackedDir, "config.json"));
expect(configStats.mode & 0o777).toBe(0o644);
} finally {
fs.rmSync(tempBinDir, { recursive: true, force: true });
fs.rmSync(binUnpackedDir, { recursive: true, force: true });
if (fs.existsSync(binPackedFilePath)) {
fs.unlinkSync(binPackedFilePath);
}
}
});
});
});