generic-mcp-client.test.ts•7.94 kB
/**
* Generic MCP Client Integration Tests
*
* Tests common MCP client patterns across all platforms
* Tests:
* - Transport layer abstraction (stdio, HTTP)
* - Tool registration and discovery
* - Error handling and recovery
* - Connection lifecycle
*/
import { describe, it, expect, beforeEach } from "bun:test";
type TransportType = "stdio" | "http";
interface MCPClientConfig {
transport: TransportType;
host?: string;
port?: number;
command?: string;
args?: string[];
env?: Record<string, string>;
}
interface MCPServerInfo {
name: string;
version: string;
transport: TransportType;
connected: boolean;
}
interface MCPToolInfo {
name: string;
description: string;
parameters: Record<string, unknown>;
}
interface MCPExecutionResult {
status: "success" | "error";
data?: unknown;
error?: string;
}
class GenericMCPClient {
private config: MCPClientConfig;
private connected = false;
private tools: Map<string, MCPToolInfo> = new Map();
private serverInfo: MCPServerInfo | null = null;
constructor(config: MCPClientConfig) {
this.config = config;
}
async connect(): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
await Promise.resolve();
// Validate configuration
if (this.config.transport === "http") {
if (this.config.host === undefined || this.config.port === undefined) {
throw new Error("HTTP transport requires host and port");
}
} else if (this.config.transport === "stdio") {
if (this.config.command === undefined) {
throw new Error("Stdio transport requires command");
}
}
this.connected = true;
this.serverInfo = {
name: "homeassistant-mcp",
version: "1.0.0",
transport: this.config.transport,
connected: true
};
}
async disconnect(): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
await Promise.resolve();
this.connected = false;
this.tools.clear();
}
async discoverTools(): Promise<MCPToolInfo[]> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
await Promise.resolve();
if (!this.connected) {
throw new Error("Client not connected");
}
const tools: MCPToolInfo[] = [
{
name: "control",
description: "Control Home Assistant entities",
parameters: { type: "object", properties: { command: { type: "string" } } }
},
{
name: "list_devices",
description: "List all Home Assistant devices",
parameters: { type: "object", properties: {} }
},
{
name: "history",
description: "Get entity history",
parameters: { type: "object", properties: { entity_id: { type: "string" } } }
},
{
name: "notify",
description: "Send notification",
parameters: { type: "object", properties: { message: { type: "string" } } }
}
];
tools.forEach(tool => this.tools.set(tool.name, tool));
return tools;
}
async executeTool(toolName: string, params: Record<string, unknown>): Promise<MCPExecutionResult> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
await Promise.resolve();
if (!this.connected) {
return { status: "error", error: "Client not connected" };
}
const tool = this.tools.get(toolName);
if (!tool) {
return { status: "error", error: `Tool not found: ${toolName}` };
}
// Mock tool execution
return {
status: "success",
data: {
tool: toolName,
params,
executedAt: new Date().toISOString()
}
};
}
isConnected(): boolean {
return this.connected;
}
getServerInfo(): MCPServerInfo | null {
return this.serverInfo;
}
getToolCount(): number {
return this.tools.size;
}
getTool(name: string): MCPToolInfo | undefined {
return this.tools.get(name);
}
}
describe("Generic MCP Client Integration", () => {
let client: GenericMCPClient;
beforeEach(() => {
client = new GenericMCPClient({
transport: "http",
host: "localhost",
port: 7123
});
});
it("should require host/port for HTTP transport", async () => {
const invalidClient = new GenericMCPClient({
transport: "http"
});
try {
await invalidClient.connect();
expect(true).toBe(false);
} catch (error) {
expect(error instanceof Error).toBe(true);
const err = error as Error;
expect(err.message).toContain("host and port");
}
});
it("should require command for stdio transport", async () => {
const invalidClient = new GenericMCPClient({
transport: "stdio"
});
try {
await invalidClient.connect();
expect(true).toBe(false);
} catch (error) {
expect(error instanceof Error).toBe(true);
const err = error as Error;
expect(err.message).toContain("command");
}
});
it("should connect with valid HTTP configuration", async () => {
expect(client.isConnected()).toBe(false);
await client.connect();
expect(client.isConnected()).toBe(true);
});
it("should provide server info after connecting", async () => {
await client.connect();
const info = client.getServerInfo();
expect(info).toBeDefined();
expect(info?.name).toBe("homeassistant-mcp");
expect(info?.transport).toBe("http");
expect(info?.connected).toBe(true);
});
it("should disconnect properly", async () => {
await client.connect();
expect(client.isConnected()).toBe(true);
await client.disconnect();
expect(client.isConnected()).toBe(false);
});
it("should discover tools", async () => {
await client.connect();
const tools = await client.discoverTools();
expect(Array.isArray(tools)).toBe(true);
expect(tools.length).toBeGreaterThan(0);
expect(tools.some(t => t.name === "control")).toBe(true);
expect(tools.some(t => t.name === "list_devices")).toBe(true);
});
it("should fail to discover tools when disconnected", async () => {
try {
await client.discoverTools();
expect(true).toBe(false);
} catch (error) {
expect(error instanceof Error).toBe(true);
const err = error as Error;
expect(err.message).toContain("not connected");
}
});
it("should execute tools", async () => {
await client.connect();
await client.discoverTools();
const result = await client.executeTool("control", {
command: "turn_on",
entity_id: "light.bedroom"
});
expect(result.status).toBe("success");
expect(result.data).toBeDefined();
});
it("should fail to execute when disconnected", async () => {
const result = await client.executeTool("control", {});
expect(result.status).toBe("error");
expect(result.error).toContain("not connected");
});
it("should fail to execute unknown tool", async () => {
await client.connect();
await client.discoverTools();
const result = await client.executeTool("unknown_tool", {});
expect(result.status).toBe("error");
expect(result.error).toContain("not found");
});
it("should track tool count", async () => {
await client.connect();
expect(client.getToolCount()).toBe(0);
await client.discoverTools();
expect(client.getToolCount()).toBeGreaterThan(0);
});
it("should retrieve individual tool info", async () => {
await client.connect();
await client.discoverTools();
const tool = client.getTool("control");
expect(tool).toBeDefined();
expect(tool?.name).toBe("control");
expect(tool?.description).toContain("Home Assistant");
});
it("should clear tools on disconnect", async () => {
await client.connect();
await client.discoverTools();
expect(client.getToolCount()).toBeGreaterThan(0);
await client.disconnect();
expect(client.getToolCount()).toBe(0);
});
});