/**
* Integration tests for Claude Code Connector MCP Server
*
* These tests spawn the actual MCP server and send JSON-RPC requests
* to verify end-to-end functionality.
*/
import { spawn, ChildProcess } from 'child_process';
import { join } from 'path';
import { mkdir, rm, writeFile, readFile } from 'fs/promises';
import { existsSync } from 'fs';
import * as path from 'path';
// Server path - use path.resolve for compatibility
const SERVER_PATH = path.resolve(process.cwd(), 'dist', 'index.js');
// Base test directory - each test gets a unique subdirectory
const TEST_BASE_DIR = '/tmp/mcp-integration-tests';
const PROJECTS_JSON_PATH = join(process.env.HOME || '', '.claude', 'projects.json');
// Helper to send JSON-RPC request to server
async function sendRequest(
child: ChildProcess,
method: string,
params: Record<string, any>,
id: number = 1
): Promise<any> {
return new Promise((resolve, reject) => {
const request = {
jsonrpc: '2.0',
id,
method,
params
};
let responseData = '';
const onData = (data: Buffer) => {
responseData += data.toString();
try {
const parsed = JSON.parse(responseData);
child.stdout?.off('data', onData);
resolve(parsed);
} catch {
// Wait for more data
}
};
child.stdout?.on('data', onData);
setTimeout(() => {
child.stdout?.off('data', onData);
reject(new Error('Request timeout'));
}, 5000);
child.stdin?.write(JSON.stringify(request) + '\n');
});
}
// Helper to spawn server
function spawnServer(): ChildProcess {
return spawn('node', [SERVER_PATH], {
stdio: ['pipe', 'pipe', 'pipe']
});
}
// Helper to clean test projects from projects.json
async function cleanTestProjects(): Promise<void> {
if (existsSync(PROJECTS_JSON_PATH)) {
try {
const data = JSON.parse(await readFile(PROJECTS_JSON_PATH, 'utf-8'));
data.projects = data.projects.filter((p: any) => !p.id.startsWith('integration-test'));
await writeFile(PROJECTS_JSON_PATH, JSON.stringify(data, null, 2), 'utf-8');
} catch {
// Ignore cleanup errors
}
}
}
// Generate unique test directory
let testCounter = 0;
function getUniqueTestDir(): string {
testCounter++;
return join(TEST_BASE_DIR, `test-${Date.now()}-${testCounter}`);
}
describe('MCP Server Integration Tests', () => {
let server: ChildProcess;
beforeAll(async () => {
// Clean up any leftover test projects
await cleanTestProjects();
// Create base test directory
await mkdir(TEST_BASE_DIR, { recursive: true });
});
afterAll(async () => {
// Clean up test data
await cleanTestProjects();
// Clean up test directory
await rm(TEST_BASE_DIR, { recursive: true, force: true });
});
beforeEach(() => {
server = spawnServer();
});
afterEach(() => {
if (server && !server.killed) {
server.kill();
}
});
describe('register_project', () => {
it('should register a new project successfully', async () => {
const testDir = getUniqueTestDir();
await mkdir(testDir, { recursive: true });
const response = await sendRequest(server, 'tools/call', {
name: 'register_project',
arguments: {
name: 'Integration Test Project',
rootPath: testDir,
id: 'integration-test-reg-1'
}
});
expect(response.result).toBeDefined();
const content = JSON.parse(response.result.content[0].text);
expect(content.success).toBe(true);
expect(content.projectId).toBe('integration-test-reg-1');
});
it('should reject non-existent path', async () => {
const response = await sendRequest(server, 'tools/call', {
name: 'register_project',
arguments: {
name: 'Bad Project',
rootPath: '/nonexistent/path/12345'
}
});
expect(response.result.isError).toBe(true);
const content = JSON.parse(response.result.content[0].text);
expect(content.error).toBe('INVALID_PATH');
});
it('should reject missing required fields', async () => {
const response = await sendRequest(server, 'tools/call', {
name: 'register_project',
arguments: {
name: 'No Path Project'
// Missing rootPath
}
});
expect(response.result.isError).toBe(true);
const content = JSON.parse(response.result.content[0].text);
expect(content.error).toBe('INVALID_ARGUMENTS');
});
});
describe('list_projects', () => {
it('should list registered projects', async () => {
const testDir = getUniqueTestDir();
await mkdir(testDir, { recursive: true });
// First register a project in this server session
await sendRequest(server, 'tools/call', {
name: 'register_project',
arguments: {
name: 'List Test Project',
rootPath: testDir,
id: 'integration-test-list'
}
});
// Query in same server session
const response = await sendRequest(server, 'tools/call', {
name: 'list_projects',
arguments: { includeInactive: true }
});
expect(response.result).toBeDefined();
const content = JSON.parse(response.result.content[0].text);
expect(content.projects).toBeDefined();
expect(Array.isArray(content.projects)).toBe(true);
expect(content.projects.length).toBeGreaterThan(0);
});
it('should filter inactive projects by default', async () => {
const response = await sendRequest(server, 'tools/call', {
name: 'list_projects',
arguments: {}
});
expect(response.result).toBeDefined();
const content = JSON.parse(response.result.content[0].text);
// All returned projects should be active
content.projects.forEach((p: any) => {
expect(p.active).toBe(true);
});
});
});
describe('write_to_project and read_from_project', () => {
it('should write a file to project', async () => {
const testDir = getUniqueTestDir();
await mkdir(testDir, { recursive: true });
// Register project in same session
await sendRequest(server, 'tools/call', {
name: 'register_project',
arguments: {
name: 'Write Test',
rootPath: testDir,
id: 'integration-test-write'
}
});
const response = await sendRequest(server, 'tools/call', {
name: 'write_to_project',
arguments: {
projectId: 'integration-test-write',
filePath: 'test-file.txt',
content: 'Hello Integration Test!'
}
});
expect(response.result).toBeDefined();
expect(response.result.isError).toBeFalsy();
const content = JSON.parse(response.result.content[0].text);
expect(content.success).toBe(true);
expect(content.action).toBe('created');
});
it('should read a file from project', async () => {
const testDir = getUniqueTestDir();
await mkdir(testDir, { recursive: true });
// Register project in same session
await sendRequest(server, 'tools/call', {
name: 'register_project',
arguments: {
name: 'Read Test',
rootPath: testDir,
id: 'integration-test-read'
}
});
// Write a file
await sendRequest(server, 'tools/call', {
name: 'write_to_project',
arguments: {
projectId: 'integration-test-read',
filePath: 'read-test.txt',
content: 'Content to read back'
}
});
// Read it back
const response = await sendRequest(server, 'tools/call', {
name: 'read_from_project',
arguments: {
projectId: 'integration-test-read',
filePath: 'read-test.txt'
}
});
expect(response.result).toBeDefined();
const content = JSON.parse(response.result.content[0].text);
expect(content.content).toBe('Content to read back');
});
it('should reject directory traversal attempts', async () => {
const testDir = getUniqueTestDir();
await mkdir(testDir, { recursive: true });
// Register project in same session
await sendRequest(server, 'tools/call', {
name: 'register_project',
arguments: {
name: 'Traversal Test',
rootPath: testDir,
id: 'integration-test-traversal'
}
});
const response = await sendRequest(server, 'tools/call', {
name: 'write_to_project',
arguments: {
projectId: 'integration-test-traversal',
filePath: '../../../etc/passwd',
content: 'malicious'
}
});
expect(response.result.isError).toBe(true);
const content = JSON.parse(response.result.content[0].text);
expect(content.error).toBe('INVALID_PATH');
});
it('should reject writing to non-existent project', async () => {
const response = await sendRequest(server, 'tools/call', {
name: 'write_to_project',
arguments: {
projectId: 'nonexistent-project-id',
filePath: 'test.txt',
content: 'test'
}
});
expect(response.result.isError).toBe(true);
const content = JSON.parse(response.result.content[0].text);
expect(content.error).toBe('PROJECT_NOT_FOUND');
});
it('should create nested directories when writing', async () => {
const testDir = getUniqueTestDir();
await mkdir(testDir, { recursive: true });
// Register project in same session
await sendRequest(server, 'tools/call', {
name: 'register_project',
arguments: {
name: 'Nested Test',
rootPath: testDir,
id: 'integration-test-nested'
}
});
const response = await sendRequest(server, 'tools/call', {
name: 'write_to_project',
arguments: {
projectId: 'integration-test-nested',
filePath: 'deep/nested/path/file.txt',
content: 'Nested content',
createDirs: true
}
});
expect(response.result).toBeDefined();
const content = JSON.parse(response.result.content[0].text);
expect(content.success).toBe(true);
});
});
describe('resources', () => {
it('should list project files resource', async () => {
const testDir = getUniqueTestDir();
await mkdir(testDir, { recursive: true });
// Register a project first
await sendRequest(server, 'tools/call', {
name: 'register_project',
arguments: {
name: 'Resource Test',
rootPath: testDir,
id: 'integration-test-resource'
}
});
// List resources
const response = await sendRequest(server, 'resources/list', {});
expect(response.result).toBeDefined();
expect(response.result.resources).toBeDefined();
// Should include our test project
const hasTestResource = response.result.resources.some(
(r: any) => r.uri.includes('integration-test')
);
expect(hasTestResource).toBe(true);
});
it('should read project files resource', async () => {
const testDir = getUniqueTestDir();
await mkdir(testDir, { recursive: true });
// Register project and create a file
await sendRequest(server, 'tools/call', {
name: 'register_project',
arguments: {
name: 'Resource Read Test',
rootPath: testDir,
id: 'integration-test-resource-read'
}
});
await sendRequest(server, 'tools/call', {
name: 'write_to_project',
arguments: {
projectId: 'integration-test-resource-read',
filePath: 'resource-test.txt',
content: 'For resource test'
}
});
const response = await sendRequest(server, 'resources/read', {
uri: 'claude-code://integration-test-resource-read/files'
});
expect(response.result).toBeDefined();
expect(response.result.contents).toBeDefined();
const content = JSON.parse(response.result.contents[0].text);
expect(content.tree).toBeDefined();
expect(Array.isArray(content.tree)).toBe(true);
});
});
});