Skip to content

Commit a2c331b

Browse files
Merge pull request #208 from modelcontextprotocol/feat/pack-manifest-flag
feat: add --manifest flag to pack command
2 parents a3bd971 + f467163 commit a2c331b

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?",
@@ -139,6 +150,15 @@ export async function packExtension({
139150
mcpbIgnorePatterns,
140151
);
141152

153+
// When using a custom manifest path, inject the manifest into the bundle
154+
if (customManifestPath) {
155+
const manifestStat = statSync(manifestPath);
156+
files["manifest.json"] = {
157+
data: readFileSync(manifestPath),
158+
mode: manifestStat.mode,
159+
};
160+
}
161+
142162
// Print package header
143163
logger.log(`\n📦 ${manifest.name}@${manifest.version}`);
144164

test/cli.test.ts

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

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

0 commit comments

Comments
 (0)