Skip to content

Commit 77f5c4f

Browse files
author
Waldek Herka
committed
fix(lib): include MCP servers in generated deployment manifest
The generate-manifest.js script was not copying the mcp/mcpServers field from collection YAML files to the generated deployment-manifest.yml. This caused GitHub Release bundles to have no MCP server definitions, resulting in the extension skipping MCP installation. Changes: - Extract mcpServers from collection.mcpServers or collection.mcp.items - Include mcpServers in the deployment manifest when present - Add logging for MCP servers count in build output - Bump package version to 1.0.0 (first stable release with MCP support) This matches the behavior of AwesomeCopilotAdapter which supports both mcp.items (schema format) and mcpServers (manifest format). Fixes MCP servers not being installed from GitHub Release bundles. Collections will need to rebuild and republish releases for the fix to take effect.
1 parent dfc91d4 commit 77f5c4f

File tree

3 files changed

+243
-1
lines changed

3 files changed

+243
-1
lines changed

lib/bin/generate-manifest.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ try {
117117
.replace(/^@[^/]+\//, '');
118118
}
119119

120+
// Extract MCP servers from either 'mcp.items' or 'mcpServers' field (matching AwesomeCopilotAdapter)
121+
const mcpServers = collection.mcpServers || (collection.mcp && collection.mcp.items);
122+
120123
// Create deployment manifest
121124
const manifest = {
122125
id: manifestId,
@@ -130,6 +133,7 @@ try {
130133
repository: packageJson.repository?.url?.replace(/^git\+/, '').replace(/\.git$/, '') || '',
131134
prompts: prompts,
132135
dependencies: [],
136+
...(mcpServers && Object.keys(mcpServers).length > 0 ? { mcpServers } : {}),
133137
};
134138

135139
// Write deployment manifest
@@ -150,6 +154,11 @@ try {
150154
const typeLabel = type.charAt(0).toUpperCase() + type.slice(1) + 's';
151155
console.log(` ${typeLabel}: ${typesCounts[type]}`);
152156
});
157+
158+
// Log MCP servers count if present
159+
if (mcpServers && Object.keys(mcpServers).length > 0) {
160+
console.log(` MCP Servers: ${Object.keys(mcpServers).length}`);
161+
}
153162
} catch (error) {
154163
console.error('❌ Error generating deployment manifest:', error.message);
155164
process.exit(1);

lib/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@prompt-registry/collection-scripts",
3-
"version": "1.0.0",
3+
"version": "1.0.1",
44
"description": "Shared scripts for building, validating, and publishing Copilot prompt collections",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

lib/test/generate-manifest.test.ts

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
/**
2+
* Generate Manifest Script Tests
3+
*
4+
* Tests for the generate-manifest.js CLI script that creates deployment manifests
5+
* from collection YAML files.
6+
*/
7+
import * as assert from 'assert';
8+
import * as fs from 'fs';
9+
import * as path from 'path';
10+
import * as os from 'os';
11+
import * as yaml from 'js-yaml';
12+
import { spawnSync } from 'child_process';
13+
14+
function createTempDir(prefix: string): string {
15+
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
16+
}
17+
18+
function writeFile(root: string, relativePath: string, content: string): void {
19+
const fullPath = path.join(root, relativePath);
20+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
21+
fs.writeFileSync(fullPath, content);
22+
}
23+
24+
function cleanup(dir: string): void {
25+
fs.rmSync(dir, { recursive: true, force: true });
26+
}
27+
28+
describe('Generate Manifest Script', () => {
29+
let tempDir: string;
30+
const scriptPath = path.join(__dirname, '../bin/generate-manifest.js');
31+
32+
beforeEach(() => {
33+
tempDir = createTempDir('generate-manifest-test-');
34+
});
35+
36+
afterEach(() => {
37+
cleanup(tempDir);
38+
});
39+
40+
describe('MCP Servers', () => {
41+
it('should include MCP servers from mcp.items field', () => {
42+
// Create collection with MCP servers in mcp.items format (schema format)
43+
const collectionYaml = `
44+
id: test-collection
45+
name: Test Collection
46+
description: A test collection with MCP servers
47+
version: "1.0.0"
48+
items:
49+
- path: prompts/test.prompt.md
50+
kind: prompt
51+
mcp:
52+
items:
53+
test-server:
54+
command: node
55+
args:
56+
- server.js
57+
env:
58+
API_KEY: test-key
59+
another-server:
60+
command: python
61+
args:
62+
- mcp_server.py
63+
`;
64+
65+
writeFile(tempDir, 'collections/test.collection.yml', collectionYaml);
66+
writeFile(tempDir, 'prompts/test.prompt.md', '# Test Prompt\n\nTest content');
67+
68+
const outFile = path.join(tempDir, 'deployment-manifest.yml');
69+
70+
// Run the generate-manifest script
71+
const result = spawnSync('node', [scriptPath, '1.0.0', '--collection-file', 'collections/test.collection.yml', '--out', outFile], {
72+
cwd: tempDir,
73+
encoding: 'utf8'
74+
});
75+
76+
assert.strictEqual(result.status, 0, `Script failed: ${result.stderr}`);
77+
assert.ok(fs.existsSync(outFile), 'Manifest file should be created');
78+
79+
// Parse the generated manifest
80+
const manifestContent = fs.readFileSync(outFile, 'utf8');
81+
const manifest = yaml.load(manifestContent) as any;
82+
83+
// Verify MCP servers are included
84+
assert.ok(manifest.mcpServers, 'Manifest should include mcpServers field');
85+
assert.strictEqual(Object.keys(manifest.mcpServers).length, 2, 'Should have 2 MCP servers');
86+
assert.ok(manifest.mcpServers['test-server'], 'Should include test-server');
87+
assert.ok(manifest.mcpServers['another-server'], 'Should include another-server');
88+
89+
// Verify server configuration
90+
assert.strictEqual(manifest.mcpServers['test-server'].command, 'node');
91+
assert.deepStrictEqual(manifest.mcpServers['test-server'].args, ['server.js']);
92+
assert.deepStrictEqual(manifest.mcpServers['test-server'].env, { API_KEY: 'test-key' });
93+
});
94+
95+
it('should include MCP servers from mcpServers field (legacy format)', () => {
96+
// Create collection with MCP servers in mcpServers format (manifest format)
97+
const collectionYaml = `
98+
id: legacy-collection
99+
name: Legacy Collection
100+
description: A collection with legacy mcpServers format
101+
version: "1.0.0"
102+
items:
103+
- path: prompts/test.prompt.md
104+
kind: prompt
105+
mcpServers:
106+
legacy-server:
107+
command: npx
108+
args:
109+
- legacy-mcp
110+
`;
111+
112+
writeFile(tempDir, 'collections/legacy.collection.yml', collectionYaml);
113+
writeFile(tempDir, 'prompts/test.prompt.md', '# Test Prompt\n\nTest content');
114+
115+
const outFile = path.join(tempDir, 'deployment-manifest.yml');
116+
117+
const result = spawnSync('node', [scriptPath, '1.0.0', '--collection-file', 'collections/legacy.collection.yml', '--out', outFile], {
118+
cwd: tempDir,
119+
encoding: 'utf8'
120+
});
121+
122+
assert.strictEqual(result.status, 0, `Script failed: ${result.stderr}`);
123+
124+
const manifestContent = fs.readFileSync(outFile, 'utf8');
125+
const manifest = yaml.load(manifestContent) as any;
126+
127+
assert.ok(manifest.mcpServers, 'Manifest should include mcpServers field');
128+
assert.ok(manifest.mcpServers['legacy-server'], 'Should include legacy-server');
129+
assert.strictEqual(manifest.mcpServers['legacy-server'].command, 'npx');
130+
});
131+
132+
it('should not include mcpServers field when no MCP servers defined', () => {
133+
const collectionYaml = `
134+
id: no-mcp-collection
135+
name: No MCP Collection
136+
description: A collection without MCP servers
137+
version: "1.0.0"
138+
items:
139+
- path: prompts/test.prompt.md
140+
kind: prompt
141+
`;
142+
143+
writeFile(tempDir, 'collections/no-mcp.collection.yml', collectionYaml);
144+
writeFile(tempDir, 'prompts/test.prompt.md', '# Test Prompt\n\nTest content');
145+
146+
const outFile = path.join(tempDir, 'deployment-manifest.yml');
147+
148+
const result = spawnSync('node', [scriptPath, '1.0.0', '--collection-file', 'collections/no-mcp.collection.yml', '--out', outFile], {
149+
cwd: tempDir,
150+
encoding: 'utf8'
151+
});
152+
153+
assert.strictEqual(result.status, 0, `Script failed: ${result.stderr}`);
154+
155+
const manifestContent = fs.readFileSync(outFile, 'utf8');
156+
const manifest = yaml.load(manifestContent) as any;
157+
158+
assert.strictEqual(manifest.mcpServers, undefined, 'Manifest should not include mcpServers field when none defined');
159+
});
160+
161+
it('should log MCP servers count when present', () => {
162+
const collectionYaml = `
163+
id: logged-collection
164+
name: Logged Collection
165+
description: A collection to test logging
166+
version: "1.0.0"
167+
items:
168+
- path: prompts/test.prompt.md
169+
kind: prompt
170+
mcp:
171+
items:
172+
server-one:
173+
command: node
174+
args: [server.js]
175+
server-two:
176+
command: python
177+
args: [server.py]
178+
server-three:
179+
command: npx
180+
args: [mcp-server]
181+
`;
182+
183+
writeFile(tempDir, 'collections/logged.collection.yml', collectionYaml);
184+
writeFile(tempDir, 'prompts/test.prompt.md', '# Test Prompt\n\nTest content');
185+
186+
const outFile = path.join(tempDir, 'deployment-manifest.yml');
187+
188+
const result = spawnSync('node', [scriptPath, '1.0.0', '--collection-file', 'collections/logged.collection.yml', '--out', outFile], {
189+
cwd: tempDir,
190+
encoding: 'utf8'
191+
});
192+
193+
assert.strictEqual(result.status, 0, `Script failed: ${result.stderr}`);
194+
assert.ok(result.stdout.includes('MCP Servers: 3'), 'Should log MCP servers count in output');
195+
});
196+
});
197+
198+
describe('Basic Manifest Generation', () => {
199+
it('should generate valid manifest from collection', () => {
200+
const collectionYaml = `
201+
id: basic-collection
202+
name: Basic Collection
203+
description: A basic test collection
204+
version: "1.0.0"
205+
items:
206+
- path: prompts/test.prompt.md
207+
kind: prompt
208+
`;
209+
210+
writeFile(tempDir, 'collections/basic.collection.yml', collectionYaml);
211+
writeFile(tempDir, 'prompts/test.prompt.md', '# Test Prompt\n\nTest content');
212+
213+
const outFile = path.join(tempDir, 'deployment-manifest.yml');
214+
215+
const result = spawnSync('node', [scriptPath, '1.0.0', '--collection-file', 'collections/basic.collection.yml', '--out', outFile], {
216+
cwd: tempDir,
217+
encoding: 'utf8'
218+
});
219+
220+
assert.strictEqual(result.status, 0, `Script failed: ${result.stderr}`);
221+
assert.ok(fs.existsSync(outFile), 'Manifest file should be created');
222+
223+
const manifestContent = fs.readFileSync(outFile, 'utf8');
224+
const manifest = yaml.load(manifestContent) as any;
225+
226+
assert.strictEqual(manifest.id, 'basic-collection');
227+
assert.strictEqual(manifest.version, '1.0.0');
228+
assert.strictEqual(manifest.name, 'Basic Collection');
229+
assert.ok(Array.isArray(manifest.prompts));
230+
assert.strictEqual(manifest.prompts.length, 1);
231+
});
232+
});
233+
});

0 commit comments

Comments
 (0)