-
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 multi-client support to mcp-get
- Add client configuration abstractions and adapters - Implement automatic client detection and selection - Add support for multiple client configuration - Update documentation with multi-client support details - Add comprehensive test suite for client adapters - Improve transport validation and error handling Co-Authored-By: Michael Latman <[email protected]>
- Loading branch information
1 parent
9287434
commit 8ebec03
Showing
14 changed files
with
399 additions
and
188 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
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 |
---|---|---|
@@ -1,16 +1,22 @@ | ||
/** @type {import('ts-jest').JestConfigWithTsJest} */ | ||
export default { | ||
preset: 'ts-jest/presets/default-esm', | ||
preset: 'ts-jest', | ||
testEnvironment: 'node', | ||
roots: ['<rootDir>/src'], | ||
testMatch: ['**/__tests__/**/*.test.ts'], | ||
moduleFileExtensions: ['ts', 'js', 'json', 'node'], | ||
setupFiles: ['<rootDir>/src/__tests__/setup.ts'], | ||
extensionsToTreatAsEsm: ['.ts'], | ||
setupFilesAfterEnv: ['<rootDir>/src/__tests__/setup.ts'], | ||
transform: { | ||
'^.+\\.tsx?$': ['ts-jest', { useESM: true }] | ||
'^.+\\.tsx?$': [ | ||
'ts-jest', | ||
{ | ||
useESM: true, | ||
isolatedModules: true | ||
} | ||
] | ||
}, | ||
moduleNameMapper: { | ||
'^(\\.{1,2}/.*)\\.js$': '$1' | ||
} | ||
}, | ||
extensionsToTreatAsEsm: ['.ts'] | ||
}; |
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
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
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
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
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,119 @@ | ||
import { jest } from '@jest/globals'; | ||
import * as fs from 'fs'; | ||
import * as path from 'path'; | ||
import * as os from 'os'; | ||
import { ClaudeAdapter } from '../../clients/claude-adapter.js'; | ||
import { ZedAdapter } from '../../clients/zed-adapter.js'; | ||
import { ContinueAdapter } from '../../clients/continue-adapter.js'; | ||
import { FirebaseAdapter } from '../../clients/firebase-adapter.js'; | ||
import { ClientConfig, ServerConfig } from '../../types/client-config.js'; | ||
|
||
jest.mock('fs'); | ||
|
||
describe('Multi-client Integration', () => { | ||
const mockConfig: ClientConfig = { type: 'claude' }; | ||
const serverConfig: ServerConfig = { | ||
name: 'test-server', | ||
runtime: 'node', | ||
command: 'node server.js', | ||
args: ['--port', '3000'], | ||
env: { NODE_ENV: 'production' }, | ||
transport: 'stdio' | ||
}; | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
(fs.existsSync as jest.Mock).mockReturnValue(true); | ||
(fs.mkdirSync as jest.Mock).mockImplementation(() => {}); | ||
(fs.readFileSync as jest.Mock).mockReturnValue('{}'); | ||
(fs.writeFileSync as jest.Mock).mockImplementation(() => {}); | ||
}); | ||
|
||
it('should handle multiple client installations', async () => { | ||
const claude = new ClaudeAdapter(mockConfig); | ||
const zed = new ZedAdapter(mockConfig); | ||
const cont = new ContinueAdapter(mockConfig); | ||
const firebase = new FirebaseAdapter(mockConfig); | ||
|
||
const installations = await Promise.all([ | ||
claude.isInstalled(), | ||
zed.isInstalled(), | ||
cont.isInstalled(), | ||
firebase.isInstalled() | ||
]); | ||
|
||
expect(installations.filter(Boolean).length).toBe(4); | ||
}); | ||
|
||
it('should write configurations to all clients', async () => { | ||
const claude = new ClaudeAdapter(mockConfig); | ||
const zed = new ZedAdapter(mockConfig); | ||
const cont = new ContinueAdapter(mockConfig); | ||
const firebase = new FirebaseAdapter(mockConfig); | ||
|
||
await Promise.all([ | ||
claude.writeConfig(serverConfig), | ||
zed.writeConfig(serverConfig), | ||
cont.writeConfig(serverConfig), | ||
firebase.writeConfig(serverConfig) | ||
]); | ||
|
||
expect(fs.writeFileSync).toHaveBeenCalledTimes(4); | ||
}); | ||
|
||
it('should handle unsupported transport methods gracefully', async () => { | ||
const claude = new ClaudeAdapter(mockConfig); | ||
const zed = new ZedAdapter(mockConfig); | ||
const cont = new ContinueAdapter(mockConfig); | ||
const firebase = new FirebaseAdapter(mockConfig); | ||
|
||
const unsupportedConfig: ServerConfig = { | ||
...serverConfig, | ||
transport: 'unsupported' as any | ||
}; | ||
|
||
const results = await Promise.all([ | ||
claude.validateConfig(unsupportedConfig), | ||
zed.validateConfig(unsupportedConfig), | ||
cont.validateConfig(unsupportedConfig), | ||
firebase.validateConfig(unsupportedConfig) | ||
]); | ||
|
||
expect(results.every(result => result === false)).toBe(true); | ||
}); | ||
|
||
it('should maintain separate configurations for each client', async () => { | ||
const claude = new ClaudeAdapter(mockConfig); | ||
const zed = new ZedAdapter(mockConfig); | ||
const cont = new ContinueAdapter(mockConfig); | ||
const firebase = new FirebaseAdapter(mockConfig); | ||
|
||
await Promise.all([ | ||
claude.writeConfig(serverConfig), | ||
zed.writeConfig(serverConfig), | ||
cont.writeConfig(serverConfig), | ||
firebase.writeConfig(serverConfig) | ||
]); | ||
|
||
const writeFileSync = fs.writeFileSync as jest.Mock; | ||
const calls = writeFileSync.mock.calls; | ||
|
||
const configs = calls.map(call => JSON.parse(call[1] as string)); | ||
|
||
expect(configs[0]).toHaveProperty('servers'); | ||
expect(configs[1]).toHaveProperty('mcp.servers'); | ||
expect(configs[2]).toHaveProperty('servers'); | ||
expect(configs[3]).toHaveProperty('mcp.servers'); | ||
}); | ||
|
||
it('should handle file system errors gracefully', async () => { | ||
const claude = new ClaudeAdapter(mockConfig); | ||
(fs.writeFileSync as jest.Mock).mockImplementation(() => { | ||
throw new Error('Mock file system error'); | ||
}); | ||
|
||
await claude.writeConfig(serverConfig).catch(error => { | ||
expect(error.message).toBe('Mock file system error'); | ||
}); | ||
}); | ||
}); |
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 |
---|---|---|
@@ -1,33 +1,54 @@ | ||
import { jest, beforeEach } from '@jest/globals'; | ||
import * as os from 'os'; | ||
import * as path from 'path'; | ||
import * as fs from 'fs'; | ||
import type * as osType from 'os'; | ||
import type * as fsType from 'fs'; | ||
import type * as fsPromisesType from 'fs/promises'; | ||
import type * as globType from 'glob'; | ||
import type { PathLike } from 'fs'; | ||
|
||
// Type the glob function | ||
type GlobFunction = (pattern: string, options?: any) => Promise<string[]>; | ||
|
||
// Mock glob module | ||
jest.mock('glob', () => ({ | ||
glob: jest.fn().mockImplementation(async () => []) as jest.MockedFunction<GlobFunction>, | ||
})); | ||
|
||
// Mock os.platform() to control testing environment | ||
jest.mock('os', () => { | ||
const actual = jest.requireActual('os') as typeof os; | ||
const actual = jest.requireActual<typeof osType>('os'); | ||
return { | ||
...actual, | ||
platform: jest.fn(), | ||
homedir: jest.fn(), | ||
platform: jest.fn().mockImplementation(() => 'darwin') as jest.MockedFunction<typeof actual.platform>, | ||
homedir: jest.fn().mockImplementation(() => '/Users/testuser') as jest.MockedFunction<typeof actual.homedir>, | ||
arch: actual.arch, | ||
cpus: actual.cpus, | ||
type: actual.type, | ||
}; | ||
}); | ||
|
||
// Mock fs operations | ||
jest.mock('fs', () => { | ||
const actual = jest.requireActual('fs') as typeof fs; | ||
const actual = jest.requireActual<typeof fsType>('fs'); | ||
return { | ||
existsSync: jest.fn().mockImplementation(() => false) as jest.MockedFunction<typeof actual.existsSync>, | ||
readFileSync: jest.fn().mockImplementation(() => '{}') as jest.MockedFunction<typeof actual.readFileSync>, | ||
writeFileSync: jest.fn().mockImplementation(() => undefined) as jest.MockedFunction<typeof actual.writeFileSync>, | ||
mkdirSync: jest.fn().mockImplementation(() => undefined) as jest.MockedFunction<typeof actual.mkdirSync>, | ||
constants: actual.constants, | ||
Stats: actual.Stats, | ||
}; | ||
}); | ||
|
||
// Mock fs/promises operations | ||
jest.mock('fs/promises', () => { | ||
const actual = jest.requireActual<typeof fsPromisesType>('fs/promises'); | ||
return { | ||
...actual, | ||
existsSync: jest.fn(), | ||
readFileSync: jest.fn(), | ||
writeFileSync: jest.fn(), | ||
mkdirSync: jest.fn(), | ||
mkdir: jest.fn().mockImplementation(async () => undefined) as jest.MockedFunction<typeof actual.mkdir>, | ||
writeFile: jest.fn().mockImplementation(async () => undefined) as jest.MockedFunction<typeof actual.writeFile>, | ||
readFile: jest.fn().mockImplementation(async () => '{}') as jest.MockedFunction<typeof actual.readFile>, | ||
}; | ||
}); | ||
|
||
// Reset all mocks before each test | ||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
(os.platform as jest.Mock).mockReturnValue('darwin'); // Default to macOS | ||
(os.homedir as jest.Mock).mockReturnValue('/Users/testuser'); | ||
}); |
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.