-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add clients command and improve test setup
- Add new clients command to list installed MCP clients - Add test environment setup script - Update Zed adapter to properly detect config files - Add comprehensive client documentation - Update package.json with new test commands Co-Authored-By: Michael Latman <[email protected]>
- Loading branch information
1 parent
f604f98
commit 7413cba
Showing
15 changed files
with
969 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
#!/bin/bash | ||
|
||
# Create Zed config directory | ||
mkdir -p ~/.config/zed | ||
|
||
# Create minimal Zed settings.json | ||
cat > ~/.config/zed/settings.json << 'EOL' | ||
{ | ||
"theme": "One Dark", | ||
"telemetry": false, | ||
"vim_mode": false, | ||
"language_servers": { | ||
"typescript": { | ||
"enabled": true | ||
} | ||
} | ||
} | ||
EOL | ||
|
||
echo "Test environment setup complete" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { jest, describe, it, expect, beforeEach } from '@jest/globals'; | ||
import { listClients } from '../../commands/clients.js'; | ||
import { Preferences } from '../../utils/preferences.js'; | ||
|
||
jest.mock('../../utils/preferences.js'); | ||
|
||
describe('listClients', () => { | ||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('should display installed clients and config paths', async () => { | ||
const mockClients = ['claude', 'zed']; | ||
(Preferences.prototype.detectInstalledClients as jest.Mock).mockResolvedValue(mockClients); | ||
Check failure on line 14 in src/__tests__/commands/clients.test.ts GitHub Actions / pr-check
|
||
|
||
const consoleSpy = jest.spyOn(console, 'log'); | ||
await listClients(); | ||
|
||
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('claude')); | ||
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('zed')); | ||
}); | ||
|
||
it('should handle no installed clients', async () => { | ||
(Preferences.prototype.detectInstalledClients as jest.Mock).mockResolvedValue([]); | ||
Check failure on line 24 in src/__tests__/commands/clients.test.ts GitHub Actions / pr-check
|
||
|
||
const consoleSpy = jest.spyOn(console, 'log'); | ||
await listClients(); | ||
|
||
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('No MCP clients detected')); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { ClientConfig, ServerConfig } from '../types/client-config.js'; | ||
import * as fs from 'fs/promises'; | ||
import * as path from 'path'; | ||
import * as os from 'os'; | ||
|
||
/** | ||
* Base adapter class for MCP client configuration | ||
*/ | ||
export abstract class ClientAdapter { | ||
protected config: ClientConfig; | ||
|
||
constructor(config: ClientConfig) { | ||
this.config = config; | ||
} | ||
|
||
/** | ||
* Get the platform-specific configuration path | ||
*/ | ||
abstract getConfigPath(): string; | ||
|
||
/** | ||
* Write server configuration to client config file | ||
*/ | ||
abstract writeConfig(config: ServerConfig): Promise<void>; | ||
|
||
/** | ||
* Validate server configuration against client requirements | ||
*/ | ||
abstract validateConfig(config: ServerConfig): Promise<boolean>; | ||
|
||
/** | ||
* Check if the client is installed by verifying config file existence | ||
*/ | ||
async isInstalled(): Promise<boolean> { | ||
try { | ||
const configPath = this.getConfigPath(); | ||
await fs.access(configPath); | ||
return true; | ||
} catch (error) { | ||
return false; | ||
} | ||
} | ||
|
||
/** | ||
* Helper method to get home directory | ||
*/ | ||
protected getHomeDir(): string { | ||
return os.homedir(); | ||
} | ||
|
||
/** | ||
* Helper method to resolve platform-specific paths | ||
*/ | ||
protected resolvePath(relativePath: string): string { | ||
return path.resolve(this.getHomeDir(), relativePath); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import { ClientAdapter } from './base-adapter.js'; | ||
import { ServerConfig, ClientConfig } from '../types/client-config.js'; | ||
import * as fs from 'fs/promises'; | ||
import * as path from 'path'; | ||
|
||
export class ClaudeAdapter extends ClientAdapter { | ||
constructor(config: ClientConfig) { | ||
super(config); | ||
} | ||
|
||
getConfigPath(): string { | ||
const platform = process.platform; | ||
if (platform === 'win32') { | ||
return this.resolvePath('AppData/Roaming/Claude/claude_desktop_config.json'); | ||
} | ||
return this.resolvePath('Library/Application Support/Claude/claude_desktop_config.json'); | ||
} | ||
|
||
async isInstalled(): Promise<boolean> { | ||
try { | ||
const platform = process.platform; | ||
const execPath = platform === 'win32' | ||
? this.resolvePath('AppData/Local/Programs/claude-desktop/Claude.exe') | ||
: '/Applications/Claude.app'; | ||
|
||
await fs.access(execPath); | ||
|
||
const configDir = path.dirname(this.getConfigPath()); | ||
await fs.access(configDir); | ||
|
||
return true; | ||
} catch (error) { | ||
return false; | ||
} | ||
} | ||
|
||
async writeConfig(config: ServerConfig): Promise<void> { | ||
const configPath = this.getConfigPath(); | ||
await fs.mkdir(path.dirname(configPath), { recursive: true }); | ||
|
||
let existingConfig = {}; | ||
try { | ||
const content = await fs.readFile(configPath, 'utf-8'); | ||
existingConfig = JSON.parse(content); | ||
} catch (error) { | ||
// File doesn't exist or is invalid, use empty config | ||
} | ||
|
||
const updatedConfig = { | ||
...existingConfig, | ||
mcpServers: { | ||
...(existingConfig as any).mcpServers, | ||
[config.name]: { | ||
runtime: config.runtime, | ||
command: config.command, | ||
args: config.args || [], | ||
env: config.env || {} | ||
} | ||
} | ||
}; | ||
|
||
await fs.writeFile(configPath, JSON.stringify(updatedConfig, null, 2)); | ||
} | ||
|
||
async validateConfig(config: ServerConfig): Promise<boolean> { | ||
return !config.transport || ['stdio', 'sse'].includes(config.transport); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import { ClientAdapter } from './base-adapter.js'; | ||
import { ServerConfig, ClientConfig } from '../types/client-config.js'; | ||
import * as fs from 'fs/promises'; | ||
import * as path from 'path'; | ||
import * as os from 'os'; | ||
import { glob } from 'glob'; | ||
|
||
export class ContinueAdapter extends ClientAdapter { | ||
constructor(config: ClientConfig) { | ||
super(config); | ||
} | ||
|
||
getConfigPath(): string { | ||
return this.resolvePath('.continue/config.json'); | ||
} | ||
|
||
async isInstalled(): Promise<boolean> { | ||
try { | ||
// Check for Continue VS Code extension | ||
const vscodePath = path.join(os.homedir(), '.vscode', 'extensions', 'continue.continue-*'); | ||
const vscodeExists = await this.checkGlobPath(vscodePath); | ||
|
||
// Check for Continue JetBrains plugin | ||
const jetbrainsPath = process.platform === 'win32' | ||
? path.join(process.env.APPDATA || '', 'JetBrains', '*', 'plugins', 'continue') | ||
: path.join(os.homedir(), 'Library', 'Application Support', 'JetBrains', '*', 'plugins', 'continue'); | ||
const jetbrainsExists = await this.checkGlobPath(jetbrainsPath); | ||
|
||
return vscodeExists || jetbrainsExists; | ||
} catch (error) { | ||
return false; | ||
} | ||
} | ||
|
||
private async checkGlobPath(globPath: string): Promise<boolean> { | ||
try { | ||
const matches = await glob(globPath); | ||
return matches.length > 0; | ||
} catch (error) { | ||
return false; | ||
} | ||
} | ||
|
||
async writeConfig(config: ServerConfig): Promise<void> { | ||
const configPath = this.getConfigPath(); | ||
await fs.mkdir(path.dirname(configPath), { recursive: true }); | ||
|
||
let existingConfig = {}; | ||
try { | ||
const content = await fs.readFile(configPath, 'utf-8'); | ||
existingConfig = JSON.parse(content); | ||
} catch (error) { | ||
// File doesn't exist or is invalid, use empty config | ||
} | ||
|
||
const updatedConfig = { | ||
...existingConfig, | ||
experimental: { | ||
...(existingConfig as any).experimental, | ||
modelContextProtocolServer: { | ||
transport: config.transport || 'stdio', | ||
command: config.command, | ||
args: config.args || [] | ||
} | ||
} | ||
}; | ||
|
||
await fs.writeFile(configPath, JSON.stringify(updatedConfig, null, 2)); | ||
} | ||
|
||
async validateConfig(config: ServerConfig): Promise<boolean> { | ||
// Continue supports stdio, sse, and websocket transports | ||
return !config.transport || ['stdio', 'sse', 'websocket'].includes(config.transport); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { ClientAdapter } from './base-adapter.js'; | ||
import { ServerConfig, ClientConfig } from '../types/client-config.js'; | ||
import * as fs from 'fs/promises'; | ||
import * as path from 'path'; | ||
import { execSync } from 'child_process'; | ||
|
||
export class FirebaseAdapter extends ClientAdapter { | ||
constructor(config: ClientConfig) { | ||
super(config); | ||
} | ||
|
||
getConfigPath(): string { | ||
return this.resolvePath('.firebase/mcp-config.json'); | ||
} | ||
|
||
async writeConfig(config: ServerConfig): Promise<void> { | ||
const configPath = this.getConfigPath(); | ||
await fs.mkdir(path.dirname(configPath), { recursive: true }); | ||
|
||
const serverConfig = { | ||
name: config.name, | ||
serverProcess: { | ||
command: config.command, | ||
args: config.args || [], | ||
env: config.env || {} | ||
}, | ||
transport: config.transport || 'stdio' | ||
}; | ||
|
||
await fs.writeFile(configPath, JSON.stringify(serverConfig, null, 2)); | ||
} | ||
|
||
async validateConfig(config: ServerConfig): Promise<boolean> { | ||
return !config.transport || ['stdio', 'sse'].includes(config.transport); | ||
} | ||
|
||
async isInstalled(): Promise<boolean> { | ||
try { | ||
execSync('firebase --version', { stdio: 'ignore' }); | ||
|
||
const configPath = path.join(process.cwd(), 'firebase.json'); | ||
await fs.access(configPath); | ||
|
||
return true; | ||
} catch (error) { | ||
return false; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.