expxagents 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/mcps/_catalog.yaml +17 -0
- package/assets/mcps/figma.mcp.yaml +35 -0
- package/assets/mcps/github.mcp.yaml +44 -0
- package/assets/mcps/linear.mcp.yaml +37 -0
- package/assets/mcps/notion.mcp.yaml +37 -0
- package/assets/mcps/pencil.mcp.yaml +32 -0
- package/assets/mcps/postgresql.mcp.yaml +39 -0
- package/assets/mcps/sentry.mcp.yaml +41 -0
- package/assets/mcps/slack.mcp.yaml +37 -0
- package/assets/mcps/vercel.mcp.yaml +39 -0
- package/dist/cli/src/commands/doctor.js +26 -10
- package/dist/cli/src/commands/init.js +35 -15
- package/dist/cli/src/commands/mcp.d.ts +2 -0
- package/dist/cli/src/commands/mcp.js +155 -0
- package/dist/cli/src/index.js +2 -0
- package/dist/cli/src/mcp/__tests__/catalog.test.d.ts +1 -0
- package/dist/cli/src/mcp/__tests__/catalog.test.js +101 -0
- package/dist/cli/src/mcp/__tests__/detect.test.d.ts +1 -0
- package/dist/cli/src/mcp/__tests__/detect.test.js +84 -0
- package/dist/cli/src/mcp/__tests__/setup.test.d.ts +1 -0
- package/dist/cli/src/mcp/__tests__/setup.test.js +75 -0
- package/dist/cli/src/mcp/__tests__/validate.test.d.ts +1 -0
- package/dist/cli/src/mcp/__tests__/validate.test.js +42 -0
- package/dist/cli/src/mcp/catalog.d.ts +4 -0
- package/dist/cli/src/mcp/catalog.js +53 -0
- package/dist/cli/src/mcp/detect.d.ts +9 -0
- package/dist/cli/src/mcp/detect.js +75 -0
- package/dist/cli/src/mcp/setup.d.ts +4 -0
- package/dist/cli/src/mcp/setup.js +56 -0
- package/dist/cli/src/mcp/types.d.ts +68 -0
- package/dist/cli/src/mcp/types.js +1 -0
- package/dist/cli/src/mcp/validate.d.ts +2 -0
- package/dist/cli/src/mcp/validate.js +23 -0
- package/dist/cli/src/pencil/detect.d.ts +6 -11
- package/dist/cli/src/pencil/detect.js +8 -39
- package/dist/core/squad-loader.d.ts +1 -0
- package/dist/core/squad-loader.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { loadMcpCatalog, loadMcpDefinition, listAllMcpIds } from '../catalog.js';
|
|
6
|
+
let tmpDir;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-catalog-test-'));
|
|
9
|
+
});
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
12
|
+
});
|
|
13
|
+
function writeCatalog(content) {
|
|
14
|
+
fs.writeFileSync(path.join(tmpDir, '_catalog.yaml'), content, 'utf-8');
|
|
15
|
+
}
|
|
16
|
+
function writeDefinition(id, content) {
|
|
17
|
+
fs.writeFileSync(path.join(tmpDir, `${id}.mcp.yaml`), content, 'utf-8');
|
|
18
|
+
}
|
|
19
|
+
describe('loadMcpCatalog', () => {
|
|
20
|
+
it('loads and parses _catalog.yaml', () => {
|
|
21
|
+
writeCatalog(`
|
|
22
|
+
catalog:
|
|
23
|
+
version: "1.0.0"
|
|
24
|
+
categories:
|
|
25
|
+
- name: development
|
|
26
|
+
mcps: [github]
|
|
27
|
+
- name: communication
|
|
28
|
+
mcps: [slack]
|
|
29
|
+
`);
|
|
30
|
+
const catalog = loadMcpCatalog(tmpDir);
|
|
31
|
+
expect(catalog.version).toBe('1.0.0');
|
|
32
|
+
expect(catalog.categories).toHaveLength(2);
|
|
33
|
+
expect(catalog.categories[0].name).toBe('development');
|
|
34
|
+
expect(catalog.categories[0].mcps).toEqual(['github']);
|
|
35
|
+
});
|
|
36
|
+
it('returns empty categories when catalog is missing', () => {
|
|
37
|
+
const catalog = loadMcpCatalog(tmpDir);
|
|
38
|
+
expect(catalog.categories).toEqual([]);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe('listAllMcpIds', () => {
|
|
42
|
+
it('flattens all MCP IDs from categories', () => {
|
|
43
|
+
writeCatalog(`
|
|
44
|
+
catalog:
|
|
45
|
+
version: "1.0.0"
|
|
46
|
+
categories:
|
|
47
|
+
- name: dev
|
|
48
|
+
mcps: [github, postgresql]
|
|
49
|
+
- name: comm
|
|
50
|
+
mcps: [slack]
|
|
51
|
+
`);
|
|
52
|
+
const ids = listAllMcpIds(tmpDir);
|
|
53
|
+
expect(ids).toEqual(['github', 'postgresql', 'slack']);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe('loadMcpDefinition', () => {
|
|
57
|
+
it('loads individual MCP definition by ID', () => {
|
|
58
|
+
writeDefinition('github', `
|
|
59
|
+
id: github
|
|
60
|
+
name: GitHub
|
|
61
|
+
description: PRs and issues
|
|
62
|
+
category: development
|
|
63
|
+
detection:
|
|
64
|
+
method: npm
|
|
65
|
+
npm:
|
|
66
|
+
package: "@modelcontextprotocol/server-github"
|
|
67
|
+
server:
|
|
68
|
+
command: npx
|
|
69
|
+
args: ["-y", "@modelcontextprotocol/server-github"]
|
|
70
|
+
env:
|
|
71
|
+
GITHUB_PERSONAL_ACCESS_TOKEN: "\${GITHUB_PERSONAL_ACCESS_TOKEN}"
|
|
72
|
+
auth:
|
|
73
|
+
- key: GITHUB_PERSONAL_ACCESS_TOKEN
|
|
74
|
+
label: "GitHub Token"
|
|
75
|
+
hint: "Create at github.com/settings/tokens"
|
|
76
|
+
required: true
|
|
77
|
+
tools:
|
|
78
|
+
- name: list_issues
|
|
79
|
+
description: List issues
|
|
80
|
+
relevant_sectors:
|
|
81
|
+
- development
|
|
82
|
+
`);
|
|
83
|
+
const def = loadMcpDefinition(tmpDir, 'github');
|
|
84
|
+
expect(def).not.toBeNull();
|
|
85
|
+
expect(def.id).toBe('github');
|
|
86
|
+
expect(def.name).toBe('GitHub');
|
|
87
|
+
expect(def.detection.method).toBe('npm');
|
|
88
|
+
expect(def.auth).toHaveLength(1);
|
|
89
|
+
expect(def.auth[0].key).toBe('GITHUB_PERSONAL_ACCESS_TOKEN');
|
|
90
|
+
expect(def.server.args).toContain('-y');
|
|
91
|
+
});
|
|
92
|
+
it('returns null for unknown MCP ID', () => {
|
|
93
|
+
const def = loadMcpDefinition(tmpDir, 'nonexistent');
|
|
94
|
+
expect(def).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
it('handles malformed YAML gracefully', () => {
|
|
97
|
+
writeDefinition('broken', '{{{{not yaml');
|
|
98
|
+
const def = loadMcpDefinition(tmpDir, 'broken');
|
|
99
|
+
expect(def).toBeNull();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { detectMcp, resolvePlatformBinary, detectExtension } from '../detect.js';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
function makeDef(overrides) {
|
|
7
|
+
return {
|
|
8
|
+
name: overrides.id,
|
|
9
|
+
description: '',
|
|
10
|
+
category: 'test',
|
|
11
|
+
server: { command: 'npx', args: [], env: {} },
|
|
12
|
+
auth: [],
|
|
13
|
+
tools: [],
|
|
14
|
+
relevant_sectors: [],
|
|
15
|
+
...overrides,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
describe('resolvePlatformBinary', () => {
|
|
19
|
+
it('resolves darwin + arm64', () => {
|
|
20
|
+
expect(resolvePlatformBinary('darwin', 'arm64')).toBe('mcp-server-darwin-arm64');
|
|
21
|
+
});
|
|
22
|
+
it('resolves darwin + x64', () => {
|
|
23
|
+
expect(resolvePlatformBinary('darwin', 'x64')).toBe('mcp-server-darwin-x64');
|
|
24
|
+
});
|
|
25
|
+
it('resolves linux + x64', () => {
|
|
26
|
+
expect(resolvePlatformBinary('linux', 'x64')).toBe('mcp-server-linux-x64');
|
|
27
|
+
});
|
|
28
|
+
it('resolves win32 + x64 with .exe', () => {
|
|
29
|
+
expect(resolvePlatformBinary('win32', 'x64')).toBe('mcp-server-win32-x64.exe');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe('detectExtension', () => {
|
|
33
|
+
let tmpDir;
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ext-detect-test-'));
|
|
36
|
+
});
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
39
|
+
});
|
|
40
|
+
it('returns null when no extension found', () => {
|
|
41
|
+
const result = detectExtension('nonexistent-prefix-', tmpDir);
|
|
42
|
+
expect(result).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
it('finds latest version', () => {
|
|
45
|
+
fs.mkdirSync(path.join(tmpDir, 'highagency.pencildev-0.6.30'));
|
|
46
|
+
fs.mkdirSync(path.join(tmpDir, 'highagency.pencildev-0.6.35'));
|
|
47
|
+
const result = detectExtension('highagency.pencildev-', tmpDir);
|
|
48
|
+
expect(result).not.toBeNull();
|
|
49
|
+
expect(result.version).toBe('0.6.35');
|
|
50
|
+
expect(result.extensionPath).toContain('0.6.35');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe('detectMcp', () => {
|
|
54
|
+
it('npm detection — always returns available', () => {
|
|
55
|
+
const def = makeDef({
|
|
56
|
+
id: 'slack',
|
|
57
|
+
detection: { method: 'npm', npm: { package: '@modelcontextprotocol/server-slack' } },
|
|
58
|
+
});
|
|
59
|
+
const result = detectMcp(def);
|
|
60
|
+
expect(result.status).toBe('available');
|
|
61
|
+
expect(result.detail).toContain('npm');
|
|
62
|
+
});
|
|
63
|
+
it('cli detection — returns unavailable when command not found', () => {
|
|
64
|
+
const def = makeDef({
|
|
65
|
+
id: 'test-cli',
|
|
66
|
+
detection: { method: 'cli', cli: { command: 'nonexistent-binary-xyz', version_flag: '--version' } },
|
|
67
|
+
});
|
|
68
|
+
const result = detectMcp(def);
|
|
69
|
+
expect(result.status).toBe('unavailable');
|
|
70
|
+
});
|
|
71
|
+
it('hybrid detection — falls back to npm when CLI not found', () => {
|
|
72
|
+
const def = makeDef({
|
|
73
|
+
id: 'test-hybrid',
|
|
74
|
+
detection: {
|
|
75
|
+
method: 'hybrid',
|
|
76
|
+
cli: { command: 'nonexistent-binary-xyz', version_flag: '--version' },
|
|
77
|
+
npm: { package: 'some-package' },
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
const result = detectMcp(def);
|
|
81
|
+
expect(result.status).toBe('available');
|
|
82
|
+
expect(result.detail).toContain('npm');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { writeMcpConfig, removeMcpConfig, readMcpJson } from '../setup.js';
|
|
6
|
+
let tmpDir;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-setup-test-'));
|
|
9
|
+
});
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
12
|
+
});
|
|
13
|
+
function makeDef(id) {
|
|
14
|
+
return {
|
|
15
|
+
id,
|
|
16
|
+
name: id,
|
|
17
|
+
description: '',
|
|
18
|
+
category: 'test',
|
|
19
|
+
detection: { method: 'npm', npm: { package: `@test/${id}` } },
|
|
20
|
+
server: { command: 'npx', args: ['-y', `@test/${id}`], env: { TOKEN: '${TOKEN}' } },
|
|
21
|
+
auth: [{ key: 'TOKEN', label: 'Token', hint: 'hint', required: true }],
|
|
22
|
+
tools: [],
|
|
23
|
+
relevant_sectors: [],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
describe('writeMcpConfig', () => {
|
|
27
|
+
it('creates .mcp.json with server entry', () => {
|
|
28
|
+
writeMcpConfig(tmpDir, makeDef('github'), { TOKEN: 'abc123' });
|
|
29
|
+
const json = readMcpJson(tmpDir);
|
|
30
|
+
expect(json.mcpServers.github).toBeDefined();
|
|
31
|
+
expect(json.mcpServers.github.command).toBe('npx');
|
|
32
|
+
expect(json.mcpServers.github.args).toContain('-y');
|
|
33
|
+
});
|
|
34
|
+
it('merges into existing .mcp.json without overwriting', () => {
|
|
35
|
+
const existing = { mcpServers: { pencil: { command: '/bin/pencil', args: [], env: {} } } };
|
|
36
|
+
fs.writeFileSync(path.join(tmpDir, '.mcp.json'), JSON.stringify(existing, null, 2));
|
|
37
|
+
writeMcpConfig(tmpDir, makeDef('github'), { TOKEN: 'abc' });
|
|
38
|
+
const json = readMcpJson(tmpDir);
|
|
39
|
+
expect(json.mcpServers.pencil).toBeDefined();
|
|
40
|
+
expect(json.mcpServers.github).toBeDefined();
|
|
41
|
+
});
|
|
42
|
+
it('adds env vars to .env file', () => {
|
|
43
|
+
writeMcpConfig(tmpDir, makeDef('github'), { TOKEN: 'mytoken' });
|
|
44
|
+
const env = fs.readFileSync(path.join(tmpDir, '.env'), 'utf-8');
|
|
45
|
+
expect(env).toContain('TOKEN=mytoken');
|
|
46
|
+
});
|
|
47
|
+
it('adds placeholders to .env.example', () => {
|
|
48
|
+
writeMcpConfig(tmpDir, makeDef('github'), { TOKEN: 'mytoken' });
|
|
49
|
+
const example = fs.readFileSync(path.join(tmpDir, '.env.example'), 'utf-8');
|
|
50
|
+
expect(example).toContain('TOKEN=');
|
|
51
|
+
expect(example).not.toContain('mytoken');
|
|
52
|
+
});
|
|
53
|
+
it('handles extension-based MCP with resolved command', () => {
|
|
54
|
+
const def = makeDef('pencil');
|
|
55
|
+
def.server.command = null;
|
|
56
|
+
writeMcpConfig(tmpDir, def, {}, '/resolved/path/to/binary');
|
|
57
|
+
const json = readMcpJson(tmpDir);
|
|
58
|
+
expect(json.mcpServers.pencil.command).toBe('/resolved/path/to/binary');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe('removeMcpConfig', () => {
|
|
62
|
+
it('removes MCP entry from .mcp.json', () => {
|
|
63
|
+
const existing = {
|
|
64
|
+
mcpServers: {
|
|
65
|
+
github: { command: 'npx', args: [], env: {} },
|
|
66
|
+
slack: { command: 'npx', args: [], env: {} },
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
fs.writeFileSync(path.join(tmpDir, '.mcp.json'), JSON.stringify(existing, null, 2));
|
|
70
|
+
removeMcpConfig(tmpDir, 'github');
|
|
71
|
+
const json = readMcpJson(tmpDir);
|
|
72
|
+
expect(json.mcpServers.github).toBeUndefined();
|
|
73
|
+
expect(json.mcpServers.slack).toBeDefined();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { validateSquadMcps, getConfiguredMcpIds } from '../validate.js';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
let tmpDir;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-validate-test-'));
|
|
9
|
+
});
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
12
|
+
});
|
|
13
|
+
describe('getConfiguredMcpIds', () => {
|
|
14
|
+
it('returns MCP IDs from .mcp.json', () => {
|
|
15
|
+
fs.writeFileSync(path.join(tmpDir, '.mcp.json'), JSON.stringify({
|
|
16
|
+
mcpServers: { github: { command: 'x', args: [], env: {} }, slack: { command: 'x', args: [], env: {} } },
|
|
17
|
+
}));
|
|
18
|
+
expect(getConfiguredMcpIds(tmpDir)).toEqual(['github', 'slack']);
|
|
19
|
+
});
|
|
20
|
+
it('returns empty array when .mcp.json missing', () => {
|
|
21
|
+
expect(getConfiguredMcpIds(tmpDir)).toEqual([]);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
describe('validateSquadMcps', () => {
|
|
25
|
+
it('passes when squad has no mcps field', () => {
|
|
26
|
+
expect(() => validateSquadMcps(undefined, ['github'])).not.toThrow();
|
|
27
|
+
});
|
|
28
|
+
it('passes when squad has empty mcps', () => {
|
|
29
|
+
expect(() => validateSquadMcps([], ['github'])).not.toThrow();
|
|
30
|
+
});
|
|
31
|
+
it('passes when all required MCPs are configured', () => {
|
|
32
|
+
expect(() => validateSquadMcps(['github', 'slack'], ['github', 'slack', 'sentry'])).not.toThrow();
|
|
33
|
+
});
|
|
34
|
+
it('throws when required MCP is not configured', () => {
|
|
35
|
+
expect(() => validateSquadMcps(['github', 'linear'], ['github']))
|
|
36
|
+
.toThrow(/linear/i);
|
|
37
|
+
});
|
|
38
|
+
it('includes setup command in error message', () => {
|
|
39
|
+
expect(() => validateSquadMcps(['linear'], []))
|
|
40
|
+
.toThrow(/expxagents mcp setup/i);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { McpCatalog, McpDefinition } from './types.js';
|
|
2
|
+
export declare function loadMcpCatalog(mcpsDir: string): McpCatalog;
|
|
3
|
+
export declare function listAllMcpIds(mcpsDir: string): string[];
|
|
4
|
+
export declare function loadMcpDefinition(mcpsDir: string, id: string): McpDefinition | null;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
export function loadMcpCatalog(mcpsDir) {
|
|
5
|
+
const catalogPath = path.join(mcpsDir, '_catalog.yaml');
|
|
6
|
+
if (!fs.existsSync(catalogPath)) {
|
|
7
|
+
return { version: '0.0.0', categories: [] };
|
|
8
|
+
}
|
|
9
|
+
try {
|
|
10
|
+
const raw = fs.readFileSync(catalogPath, 'utf-8');
|
|
11
|
+
const parsed = yaml.load(raw);
|
|
12
|
+
const catalog = parsed?.catalog;
|
|
13
|
+
return {
|
|
14
|
+
version: catalog?.version ?? '0.0.0',
|
|
15
|
+
categories: catalog?.categories ?? [],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return { version: '0.0.0', categories: [] };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function listAllMcpIds(mcpsDir) {
|
|
23
|
+
const catalog = loadMcpCatalog(mcpsDir);
|
|
24
|
+
return catalog.categories.flatMap(c => c.mcps);
|
|
25
|
+
}
|
|
26
|
+
export function loadMcpDefinition(mcpsDir, id) {
|
|
27
|
+
const defPath = path.join(mcpsDir, `${id}.mcp.yaml`);
|
|
28
|
+
if (!fs.existsSync(defPath)) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const raw = fs.readFileSync(defPath, 'utf-8');
|
|
33
|
+
const parsed = yaml.load(raw);
|
|
34
|
+
if (!parsed?.id || !parsed?.detection) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
id: parsed.id,
|
|
39
|
+
name: parsed.name ?? parsed.id,
|
|
40
|
+
description: parsed.description ?? '',
|
|
41
|
+
category: parsed.category ?? 'other',
|
|
42
|
+
icon: parsed.icon,
|
|
43
|
+
detection: parsed.detection,
|
|
44
|
+
server: parsed.server ?? { command: null, args: [], env: {} },
|
|
45
|
+
auth: parsed.auth ?? [],
|
|
46
|
+
tools: parsed.tools ?? [],
|
|
47
|
+
relevant_sectors: parsed.relevant_sectors ?? [],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { McpDefinition, McpDetectionResult } from './types.js';
|
|
2
|
+
export declare function resolvePlatformBinary(platform?: string, arch?: string): string;
|
|
3
|
+
export interface ExtensionInfo {
|
|
4
|
+
version: string;
|
|
5
|
+
extensionPath: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function detectExtension(prefix: string, extensionsDir?: string): ExtensionInfo | null;
|
|
8
|
+
export declare function detectMcp(def: McpDefinition, extensionsDir?: string): McpDetectionResult;
|
|
9
|
+
export declare function detectAllMcps(definitions: McpDefinition[], extensionsDir?: string): McpDetectionResult[];
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
export function resolvePlatformBinary(platform, arch) {
|
|
6
|
+
const p = platform ?? process.platform;
|
|
7
|
+
const a = arch ?? process.arch;
|
|
8
|
+
if (p === 'win32')
|
|
9
|
+
return 'mcp-server-win32-x64.exe';
|
|
10
|
+
return `mcp-server-${p}-${a}`;
|
|
11
|
+
}
|
|
12
|
+
export function detectExtension(prefix, extensionsDir) {
|
|
13
|
+
const dir = extensionsDir ?? path.join(os.homedir(), '.vscode', 'extensions');
|
|
14
|
+
if (!fs.existsSync(dir))
|
|
15
|
+
return null;
|
|
16
|
+
const entries = fs.readdirSync(dir)
|
|
17
|
+
.filter(e => e.startsWith(prefix))
|
|
18
|
+
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }));
|
|
19
|
+
if (entries.length === 0)
|
|
20
|
+
return null;
|
|
21
|
+
const latest = entries[entries.length - 1];
|
|
22
|
+
return {
|
|
23
|
+
version: latest.slice(prefix.length),
|
|
24
|
+
extensionPath: path.join(dir, latest),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function detectCli(command, versionFlag) {
|
|
28
|
+
try {
|
|
29
|
+
const output = execSync(`${command} ${versionFlag}`, { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
30
|
+
const match = output.match(/(\d+\.\d+[\.\d]*)/);
|
|
31
|
+
return { found: true, version: match?.[1] ?? 'unknown' };
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return { found: false };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function detectMcp(def, extensionsDir) {
|
|
38
|
+
const base = { id: def.id, name: def.name };
|
|
39
|
+
const { method } = def.detection;
|
|
40
|
+
if (method === 'cli' && def.detection.cli) {
|
|
41
|
+
const { command, version_flag } = def.detection.cli;
|
|
42
|
+
const cli = detectCli(command, version_flag);
|
|
43
|
+
if (cli.found) {
|
|
44
|
+
return { ...base, status: 'detected', detail: `${command} v${cli.version} found`, version: cli.version };
|
|
45
|
+
}
|
|
46
|
+
return { ...base, status: 'unavailable', detail: `${command} not found in PATH` };
|
|
47
|
+
}
|
|
48
|
+
if (method === 'npm' && def.detection.npm) {
|
|
49
|
+
return { ...base, status: 'available', detail: `Available via npm (${def.detection.npm.package})` };
|
|
50
|
+
}
|
|
51
|
+
if (method === 'extension' && def.detection.extension) {
|
|
52
|
+
const ext = detectExtension(def.detection.extension.prefix, extensionsDir);
|
|
53
|
+
if (ext) {
|
|
54
|
+
return { ...base, status: 'detected', detail: `VSCode extension v${ext.version}`, version: ext.version, extensionPath: ext.extensionPath };
|
|
55
|
+
}
|
|
56
|
+
return { ...base, status: 'unavailable', detail: 'VSCode extension not found' };
|
|
57
|
+
}
|
|
58
|
+
if (method === 'hybrid') {
|
|
59
|
+
if (def.detection.cli) {
|
|
60
|
+
const { command, version_flag } = def.detection.cli;
|
|
61
|
+
const cli = detectCli(command, version_flag);
|
|
62
|
+
if (cli.found) {
|
|
63
|
+
return { ...base, status: 'detected', detail: `${command} v${cli.version} found`, version: cli.version };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (def.detection.npm) {
|
|
67
|
+
return { ...base, status: 'available', detail: `Available via npm (${def.detection.npm.package})` };
|
|
68
|
+
}
|
|
69
|
+
return { ...base, status: 'unavailable', detail: 'No CLI or npm package available' };
|
|
70
|
+
}
|
|
71
|
+
return { ...base, status: 'unavailable', detail: 'Unknown detection method' };
|
|
72
|
+
}
|
|
73
|
+
export function detectAllMcps(definitions, extensionsDir) {
|
|
74
|
+
return definitions.map(def => detectMcp(def, extensionsDir));
|
|
75
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { McpDefinition, McpJson } from './types.js';
|
|
2
|
+
export declare function readMcpJson(projectDir: string): McpJson;
|
|
3
|
+
export declare function writeMcpConfig(projectDir: string, def: McpDefinition, authValues: Record<string, string>, resolvedCommand?: string): void;
|
|
4
|
+
export declare function removeMcpConfig(projectDir: string, mcpId: string): void;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
export function readMcpJson(projectDir) {
|
|
4
|
+
const mcpPath = path.join(projectDir, '.mcp.json');
|
|
5
|
+
if (!fs.existsSync(mcpPath)) {
|
|
6
|
+
return { mcpServers: {} };
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(fs.readFileSync(mcpPath, 'utf-8'));
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return { mcpServers: {} };
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function writeMcpConfig(projectDir, def, authValues, resolvedCommand) {
|
|
16
|
+
const mcpJson = readMcpJson(projectDir);
|
|
17
|
+
const command = resolvedCommand ?? def.server.command;
|
|
18
|
+
if (!command)
|
|
19
|
+
return;
|
|
20
|
+
mcpJson.mcpServers[def.id] = {
|
|
21
|
+
command,
|
|
22
|
+
args: def.server.args,
|
|
23
|
+
env: { ...def.server.env },
|
|
24
|
+
};
|
|
25
|
+
fs.writeFileSync(path.join(projectDir, '.mcp.json'), JSON.stringify(mcpJson, null, 2) + '\n');
|
|
26
|
+
// Write env vars
|
|
27
|
+
const envPath = path.join(projectDir, '.env');
|
|
28
|
+
const examplePath = path.join(projectDir, '.env.example');
|
|
29
|
+
let envContent = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf-8') : '';
|
|
30
|
+
let exampleContent = fs.existsSync(examplePath) ? fs.readFileSync(examplePath, 'utf-8') : '';
|
|
31
|
+
const mcpHeader = `\n# MCP: ${def.name}\n`;
|
|
32
|
+
let envBlock = '';
|
|
33
|
+
let exampleBlock = '';
|
|
34
|
+
for (const auth of def.auth) {
|
|
35
|
+
const value = authValues[auth.key] ?? '';
|
|
36
|
+
if (!envContent.includes(`${auth.key}=`)) {
|
|
37
|
+
envBlock += `${auth.key}=${value}\n`;
|
|
38
|
+
}
|
|
39
|
+
if (!exampleContent.includes(`${auth.key}=`)) {
|
|
40
|
+
exampleBlock += `${auth.key}=\n`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (envBlock) {
|
|
44
|
+
envContent += mcpHeader + envBlock;
|
|
45
|
+
fs.writeFileSync(envPath, envContent);
|
|
46
|
+
}
|
|
47
|
+
if (exampleBlock) {
|
|
48
|
+
exampleContent += mcpHeader + exampleBlock;
|
|
49
|
+
fs.writeFileSync(examplePath, exampleContent);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function removeMcpConfig(projectDir, mcpId) {
|
|
53
|
+
const mcpJson = readMcpJson(projectDir);
|
|
54
|
+
delete mcpJson.mcpServers[mcpId];
|
|
55
|
+
fs.writeFileSync(path.join(projectDir, '.mcp.json'), JSON.stringify(mcpJson, null, 2) + '\n');
|
|
56
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export interface McpAuthEntry {
|
|
2
|
+
key: string;
|
|
3
|
+
label: string;
|
|
4
|
+
hint: string;
|
|
5
|
+
required: boolean;
|
|
6
|
+
validate?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface McpToolEntry {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
}
|
|
12
|
+
export interface McpDetectionConfig {
|
|
13
|
+
method: 'cli' | 'npm' | 'extension' | 'hybrid';
|
|
14
|
+
cli?: {
|
|
15
|
+
command: string;
|
|
16
|
+
version_flag: string;
|
|
17
|
+
min_version?: string;
|
|
18
|
+
};
|
|
19
|
+
npm?: {
|
|
20
|
+
package: string;
|
|
21
|
+
};
|
|
22
|
+
extension?: {
|
|
23
|
+
prefix: string;
|
|
24
|
+
binary_dir: string;
|
|
25
|
+
binary_resolve: 'platform';
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export interface McpServerConfig {
|
|
29
|
+
command: string | null;
|
|
30
|
+
args: string[];
|
|
31
|
+
env: Record<string, string>;
|
|
32
|
+
}
|
|
33
|
+
export interface McpDefinition {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
description: string;
|
|
37
|
+
category: string;
|
|
38
|
+
icon?: string;
|
|
39
|
+
detection: McpDetectionConfig;
|
|
40
|
+
server: McpServerConfig;
|
|
41
|
+
auth: McpAuthEntry[];
|
|
42
|
+
tools: McpToolEntry[];
|
|
43
|
+
relevant_sectors: string[];
|
|
44
|
+
}
|
|
45
|
+
export interface McpCatalogCategory {
|
|
46
|
+
name: string;
|
|
47
|
+
mcps: string[];
|
|
48
|
+
}
|
|
49
|
+
export interface McpCatalog {
|
|
50
|
+
version: string;
|
|
51
|
+
categories: McpCatalogCategory[];
|
|
52
|
+
}
|
|
53
|
+
export interface McpDetectionResult {
|
|
54
|
+
id: string;
|
|
55
|
+
name: string;
|
|
56
|
+
status: 'detected' | 'available' | 'unavailable';
|
|
57
|
+
detail: string;
|
|
58
|
+
version?: string;
|
|
59
|
+
extensionPath?: string;
|
|
60
|
+
}
|
|
61
|
+
export interface McpJsonEntry {
|
|
62
|
+
command: string;
|
|
63
|
+
args: string[];
|
|
64
|
+
env: Record<string, string>;
|
|
65
|
+
}
|
|
66
|
+
export interface McpJson {
|
|
67
|
+
mcpServers: Record<string, McpJsonEntry>;
|
|
68
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
export function getConfiguredMcpIds(projectDir) {
|
|
4
|
+
const mcpPath = path.join(projectDir, '.mcp.json');
|
|
5
|
+
if (!fs.existsSync(mcpPath))
|
|
6
|
+
return [];
|
|
7
|
+
try {
|
|
8
|
+
const json = JSON.parse(fs.readFileSync(mcpPath, 'utf-8'));
|
|
9
|
+
return Object.keys(json.mcpServers ?? {});
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function validateSquadMcps(squadMcps, configuredMcps) {
|
|
16
|
+
if (!squadMcps || squadMcps.length === 0)
|
|
17
|
+
return;
|
|
18
|
+
const missing = squadMcps.filter(m => !configuredMcps.includes(m));
|
|
19
|
+
if (missing.length > 0) {
|
|
20
|
+
throw new Error(`Squad requires MCPs not configured: ${missing.join(', ')}.\n` +
|
|
21
|
+
`Run: expxagents mcp setup ${missing.join(' ')}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @deprecated — Use cli/src/mcp/detect.ts instead. This file re-exports for backwards compatibility.
|
|
3
|
+
*/
|
|
4
|
+
export { resolvePlatformBinary } from '../mcp/detect.js';
|
|
1
5
|
export interface PencilInfo {
|
|
2
6
|
version: string;
|
|
3
7
|
extensionPath: string;
|
|
@@ -11,16 +15,7 @@ export interface McpConfig {
|
|
|
11
15
|
};
|
|
12
16
|
};
|
|
13
17
|
}
|
|
14
|
-
/**
|
|
15
|
-
* Detect Pencil VSCode extension in the given extensions directory.
|
|
16
|
-
* Returns info about the latest installed version, or null if not found.
|
|
17
|
-
*/
|
|
18
|
+
/** @deprecated Use detectExtension from cli/src/mcp/detect.ts */
|
|
18
19
|
export declare function detectPencilExtension(extensionsDir?: string): PencilInfo | null;
|
|
19
|
-
/**
|
|
20
|
-
* Resolve the correct MCP server binary name for the current platform.
|
|
21
|
-
*/
|
|
22
|
-
export declare function resolvePlatformBinary(platform?: string, arch?: string): string;
|
|
23
|
-
/**
|
|
24
|
-
* Build the .mcp.json config object for Pencil MCP.
|
|
25
|
-
*/
|
|
20
|
+
/** @deprecated Use writeMcpConfig from cli/src/mcp/setup.ts */
|
|
26
21
|
export declare function buildMcpConfig(info: PencilInfo, binaryName: string): McpConfig;
|