Skip to content

Commit f467163

Browse files
feat: add --manifest flag to pack command (#87)
Allow specifying an external manifest file path with `mcpb pack --manifest <path>`. The manifest resolves relative to CWD, is validated, injected into the bundle, and errors immediately if the specified file is not found. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1dca91e commit f467163

3 files changed

Lines changed: 127 additions & 18 deletions

File tree

src/cli/cli.ts

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -101,22 +101,33 @@ program
101101
program
102102
.command("pack [directory] [output]")
103103
.description("Pack a directory into an MCPB extension")
104-
.action((directory: string = process.cwd(), output?: string) => {
105-
void (async () => {
106-
try {
107-
const success = await packExtension({
108-
extensionPath: directory,
109-
outputPath: output,
110-
});
111-
process.exit(success ? 0 : 1);
112-
} catch (error) {
113-
console.error(
114-
`ERROR: ${error instanceof Error ? error.message : "Unknown error"}`,
115-
);
116-
process.exit(1);
117-
}
118-
})();
119-
});
104+
.option(
105+
"-m, --manifest <path>",
106+
"Path to manifest file (defaults to manifest.json in directory)",
107+
)
108+
.action(
109+
(
110+
directory: string = process.cwd(),
111+
output: string | undefined,
112+
options: { manifest?: string },
113+
) => {
114+
void (async () => {
115+
try {
116+
const success = await packExtension({
117+
extensionPath: directory,
118+
outputPath: output,
119+
manifestPath: options.manifest,
120+
});
121+
process.exit(success ? 0 : 1);
122+
} catch (error) {
123+
console.error(
124+
`ERROR: ${error instanceof Error ? error.message : "Unknown error"}`,
125+
);
126+
process.exit(1);
127+
}
128+
})();
129+
},
130+
);
120131

121132
// Unpack command
122133
program

src/cli/pack.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ interface PackOptions {
2222
extensionPath: string;
2323
outputPath?: string;
2424
silent?: boolean;
25+
manifestPath?: string;
2526
}
2627

2728
function formatFileSize(bytes: number): string {
@@ -50,6 +51,7 @@ export async function packExtension({
5051
extensionPath,
5152
outputPath,
5253
silent,
54+
manifestPath: customManifestPath,
5355
}: PackOptions): Promise<boolean> {
5456
const resolvedPath = resolve(extensionPath);
5557
const logger = getLogger({ silent });
@@ -60,9 +62,18 @@ export async function packExtension({
6062
return false;
6163
}
6264

63-
// Check if manifest exists
64-
const manifestPath = join(resolvedPath, "manifest.json");
65+
// Resolve manifest path
66+
const manifestPath = customManifestPath
67+
? resolve(customManifestPath)
68+
: join(resolvedPath, "manifest.json");
69+
6570
if (!existsSync(manifestPath)) {
71+
if (customManifestPath) {
72+
// When --manifest is explicitly provided, error immediately
73+
logger.error(`ERROR: Manifest file not found: ${customManifestPath}`);
74+
return false;
75+
}
76+
6677
logger.log(`No manifest.json found in ${extensionPath}`);
6778
const shouldInit = await confirm({
6879
message: "Would you like to create a manifest.json file?",
@@ -135,6 +146,15 @@ export async function packExtension({
135146
mcpbIgnorePatterns,
136147
);
137148

149+
// When using a custom manifest path, inject the manifest into the bundle
150+
if (customManifestPath) {
151+
const manifestStat = statSync(manifestPath);
152+
files["manifest.json"] = {
153+
data: readFileSync(manifestPath),
154+
mode: manifestStat.mode,
155+
};
156+
}
157+
138158
// Print package header
139159
logger.log(`\n📦 ${manifest.name}@${manifest.version}`);
140160

test/cli.test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,84 @@ describe("DXT CLI", () => {
258258
expect(originalFile2).toEqual(unpackedFile2);
259259
});
260260

261+
it("should pack with --manifest pointing to a separate directory", () => {
262+
const projectDir = join(__dirname, "temp-manifest-project");
263+
const manifestDir = join(__dirname, "temp-manifest-separate");
264+
const manifestPackedPath = join(__dirname, "test-manifest-flag.mcpb");
265+
266+
try {
267+
// Create project directory with source files (no manifest)
268+
fs.mkdirSync(join(projectDir, "server"), { recursive: true });
269+
fs.writeFileSync(
270+
join(projectDir, "server", "index.js"),
271+
"console.log('hello');",
272+
);
273+
274+
// Create separate manifest directory
275+
fs.mkdirSync(manifestDir, { recursive: true });
276+
fs.writeFileSync(
277+
join(manifestDir, "manifest.json"),
278+
JSON.stringify({
279+
manifest_version: DEFAULT_MANIFEST_VERSION,
280+
name: "Test Separate Manifest",
281+
version: "1.0.0",
282+
description: "Test with separate manifest",
283+
author: { name: "MCPB" },
284+
server: {
285+
type: "node",
286+
entry_point: "server/index.js",
287+
mcp_config: { command: "node" },
288+
},
289+
}),
290+
);
291+
292+
const result = execSync(
293+
`node ${cliPath} pack ${projectDir} ${manifestPackedPath} --manifest ${join(manifestDir, "manifest.json")}`,
294+
{ encoding: "utf-8" },
295+
);
296+
297+
expect(fs.existsSync(manifestPackedPath)).toBe(true);
298+
expect(result).toContain("Validating manifest");
299+
} finally {
300+
fs.rmSync(projectDir, { recursive: true, force: true });
301+
fs.rmSync(manifestDir, { recursive: true, force: true });
302+
if (fs.existsSync(manifestPackedPath)) {
303+
fs.unlinkSync(manifestPackedPath);
304+
}
305+
}
306+
});
307+
308+
it("should fail with --manifest pointing to nonexistent file", () => {
309+
const projectDir = join(__dirname, "temp-manifest-missing-project");
310+
311+
try {
312+
fs.mkdirSync(projectDir, { recursive: true });
313+
fs.writeFileSync(join(projectDir, "index.js"), "console.log('hello');");
314+
315+
expect(() => {
316+
execSync(
317+
`node ${cliPath} pack ${projectDir} /tmp/out.mcpb --manifest /nonexistent/manifest.json`,
318+
{ encoding: "utf-8", stdio: "pipe" },
319+
);
320+
}).toThrow();
321+
322+
try {
323+
execSync(
324+
`node ${cliPath} pack ${projectDir} /tmp/out.mcpb --manifest /nonexistent/manifest.json`,
325+
{ encoding: "utf-8", stdio: "pipe" },
326+
);
327+
} catch (error: unknown) {
328+
const execError = error as { stdout?: Buffer; stderr?: Buffer };
329+
const output =
330+
(execError.stdout?.toString() || "") +
331+
(execError.stderr?.toString() || "");
332+
expect(output).toContain("Manifest file not found");
333+
}
334+
} finally {
335+
fs.rmSync(projectDir, { recursive: true, force: true });
336+
}
337+
});
338+
261339
it("should preserve executable file permissions after packing and unpacking", () => {
262340
// Skip this test on Windows since it doesn't support Unix permissions
263341
if (process.platform === "win32") {

0 commit comments

Comments
 (0)