feat: first round of mcp standalone server
This commit is contained in:
@@ -29,7 +29,7 @@ describe('MCPAgentConfigEngine', () => {
|
||||
let engine: MCPAgentConfigEngine;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.resetAllMocks();
|
||||
engine = new MCPAgentConfigEngine({
|
||||
homeDir: '/home/testuser',
|
||||
platform: 'darwin',
|
||||
@@ -40,9 +40,10 @@ describe('MCPAgentConfigEngine', () => {
|
||||
describe('getAgents', () => {
|
||||
it('returns all supported agent definitions', () => {
|
||||
const agents = engine.getAgents();
|
||||
expect(agents).toHaveLength(4);
|
||||
expect(agents).toHaveLength(5);
|
||||
const ids = agents.map((a) => a.id);
|
||||
expect(ids).toContain('claude-code');
|
||||
expect(ids).toContain('claude-desktop');
|
||||
expect(ids).toContain('github-copilot');
|
||||
expect(ids).toContain('gemini-cli');
|
||||
expect(ids).toContain('opencode');
|
||||
@@ -336,4 +337,160 @@ describe('MCPAgentConfigEngine', () => {
|
||||
expect(written.mcpServers.bDS.url).toBe('http://127.0.0.1:9999/mcp');
|
||||
});
|
||||
});
|
||||
|
||||
describe('claude-desktop', () => {
|
||||
let desktopEngine: MCPAgentConfigEngine;
|
||||
|
||||
beforeEach(() => {
|
||||
desktopEngine = new MCPAgentConfigEngine({
|
||||
homeDir: '/home/testuser',
|
||||
platform: 'darwin',
|
||||
mcpUrl: 'http://127.0.0.1:4124/mcp',
|
||||
execPath: '/Applications/Blogging Desktop Server.app/Contents/MacOS/Blogging Desktop Server',
|
||||
scriptPath: '/Applications/Blogging Desktop Server.app/Contents/Resources/bds-mcp.cjs',
|
||||
});
|
||||
});
|
||||
|
||||
it('includes claude-desktop in getAgents()', () => {
|
||||
const agents = desktopEngine.getAgents();
|
||||
expect(agents.map((a) => a.id)).toContain('claude-desktop');
|
||||
});
|
||||
|
||||
it('returns correct config path for claude-desktop on macOS', () => {
|
||||
expect(desktopEngine.getConfigPath('claude-desktop')).toBe(
|
||||
'/home/testuser/Library/Application Support/Claude/claude_desktop_config.json',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns correct config path for claude-desktop on Windows', () => {
|
||||
const winEngine = new MCPAgentConfigEngine({
|
||||
homeDir: 'C:\\Users\\testuser',
|
||||
platform: 'win32',
|
||||
mcpUrl: 'http://127.0.0.1:4124/mcp',
|
||||
execPath: 'C:\\path\\to\\app.exe',
|
||||
scriptPath: 'C:\\path\\to\\bds-mcp.cjs',
|
||||
});
|
||||
const configPath = winEngine.getConfigPath('claude-desktop');
|
||||
// On Windows path.join uses backslashes; on macOS it uses forward slashes
|
||||
// so normalise for cross-platform CI
|
||||
const normalised = configPath.replace(/[\\/]/g, '/');
|
||||
expect(normalised).toBe(
|
||||
'C:/Users/testuser/AppData/Roaming/Claude/claude_desktop_config.json',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns correct config path for claude-desktop on Linux', () => {
|
||||
const linuxEngine = new MCPAgentConfigEngine({
|
||||
homeDir: '/home/user',
|
||||
platform: 'linux',
|
||||
mcpUrl: 'http://127.0.0.1:4124/mcp',
|
||||
});
|
||||
expect(linuxEngine.getConfigPath('claude-desktop')).toBe(
|
||||
'/home/user/.config/Claude/claude_desktop_config.json',
|
||||
);
|
||||
});
|
||||
|
||||
it('adds stdio entry with command/args/env to claude_desktop_config.json', () => {
|
||||
mockExistsSync.mockReturnValue(false);
|
||||
|
||||
const result = desktopEngine.addToConfig('claude-desktop');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const written = JSON.parse(mockWriteFileSync.mock.calls[0]![1] as string);
|
||||
expect(written.mcpServers.bDS).toEqual({
|
||||
command: '/Applications/Blogging Desktop Server.app/Contents/MacOS/Blogging Desktop Server',
|
||||
args: ['/Applications/Blogging Desktop Server.app/Contents/Resources/bds-mcp.cjs'],
|
||||
env: { ELECTRON_RUN_AS_NODE: '1' },
|
||||
});
|
||||
});
|
||||
|
||||
it('throws descriptive error if execPath/scriptPath missing for claude-desktop', () => {
|
||||
const noPathEngine = new MCPAgentConfigEngine({
|
||||
homeDir: '/home/testuser',
|
||||
platform: 'darwin',
|
||||
mcpUrl: 'http://127.0.0.1:4124/mcp',
|
||||
});
|
||||
mockExistsSync.mockReturnValue(false);
|
||||
|
||||
const result = noPathEngine.addToConfig('claude-desktop');
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('execPath');
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeFromConfig', () => {
|
||||
it('removes bDS entry from config and returns success', () => {
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReadFileSync.mockReturnValue(
|
||||
JSON.stringify({
|
||||
mcpServers: {
|
||||
bDS: { type: 'http', url: 'http://127.0.0.1:4124/mcp' },
|
||||
other: { type: 'http', url: 'http://other' },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const result = engine.removeFromConfig('claude-code');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const written = JSON.parse(mockWriteFileSync.mock.calls[0]![1] as string);
|
||||
expect(written.mcpServers.bDS).toBeUndefined();
|
||||
expect(written.mcpServers.other).toBeDefined();
|
||||
});
|
||||
|
||||
it('removes the mcpServers key entirely when bDS was the only entry', () => {
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReadFileSync.mockReturnValue(
|
||||
JSON.stringify({
|
||||
mcpServers: { bDS: { type: 'http', url: 'http://127.0.0.1:4124/mcp' } },
|
||||
}),
|
||||
);
|
||||
|
||||
engine.removeFromConfig('claude-code');
|
||||
|
||||
const written = JSON.parse(mockWriteFileSync.mock.calls[0]![1] as string);
|
||||
expect(written.mcpServers).toBeUndefined();
|
||||
});
|
||||
|
||||
it('no-ops gracefully when file does not exist', () => {
|
||||
mockExistsSync.mockReturnValue(false);
|
||||
|
||||
const result = engine.removeFromConfig('claude-code');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(mockWriteFileSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('no-ops gracefully when bDS entry is not in config', () => {
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReadFileSync.mockReturnValue(JSON.stringify({ mcpServers: { other: {} } }));
|
||||
|
||||
const result = engine.removeFromConfig('claude-code');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(mockWriteFileSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses the servers key for github-copilot', () => {
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReadFileSync.mockReturnValue(
|
||||
JSON.stringify({ servers: { bDS: { type: 'http', url: 'x' } } }),
|
||||
);
|
||||
|
||||
const result = engine.removeFromConfig('github-copilot');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const written = JSON.parse(mockWriteFileSync.mock.calls[0]![1] as string);
|
||||
expect(written.servers).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns success with configPath', () => {
|
||||
mockExistsSync.mockReturnValue(false);
|
||||
|
||||
const result = engine.removeFromConfig('claude-code');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.configPath).toBe('/home/testuser/.claude.json');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user