v3/@claude-flow/cli/docs/MCP_CLIENT_GUIDE.md
The MCP Client (mcp-client.ts) provides a thin wrapper for CLI commands to call MCP tools, implementing ADR-005: MCP-First API Design where CLI acts as a thin wrapper around MCP tools.
┌─────────────────┐
│ CLI Command │ ← User interaction & display only
└────────┬────────┘
│ callMCPTool()
▼
┌─────────────────┐
│ MCP Client │ ← Tool registry & routing
└────────┬────────┘
│ tool.handler()
▼
┌─────────────────┐
│ MCP Tool │ ← Business logic lives here
│ Handler │
└─────────────────┘
import { callMCPTool, MCPClientError } from '../mcp-client.js';
try {
const result = await callMCPTool('agent/spawn', {
agentType: 'coder',
priority: 'normal',
config: { timeout: 300 }
});
// Handle success - display output
output.printSuccess(`Agent ${result.agentId} spawned`);
return { success: true, data: result };
} catch (error) {
if (error instanceof MCPClientError) {
output.printError(`Failed: ${error.message}`);
}
return { success: false, exitCode: 1 };
}
| Tool Name | Description | Input Parameters |
|---|---|---|
agent/spawn | Spawn a new agent | agentType, id?, config?, priority?, metadata? |
agent/list | List all agents | status?, agentType?, limit?, offset? |
agent/status | Get agent status | agentId, includeMetrics?, includeHistory? |
agent/terminate | Terminate an agent | agentId, graceful?, reason? |
| Tool Name | Description | Input Parameters |
|---|---|---|
swarm/init | Initialize swarm | topology, maxAgents?, config?, metadata? |
swarm/status | Get swarm status | includeAgents?, includeMetrics?, includeTopology? |
swarm/scale | Scale swarm | targetAgents, scaleStrategy?, agentTypes?, reason? |
| Tool Name | Description | Input Parameters |
|---|---|---|
memory/store | Store memory | content, type?, category?, tags?, importance?, ttl? |
memory/search | Search memories | query, searchType?, type?, category?, tags?, limit?, minRelevance? |
memory/list | List memories | type?, category?, tags?, sortBy?, sortOrder?, limit?, offset? |
| Tool Name | Description | Input Parameters |
|---|---|---|
config/load | Load configuration | path?, scope?, merge?, includeDefaults? |
config/save | Save configuration | config, path?, scope?, merge?, createBackup? |
config/validate | Validate config | config, strict?, fixIssues? |
callMCPTool<T>(toolName, input, context?): Promise<T>Call an MCP tool by name and return typed result.
Parameters:
toolName: MCP tool name (e.g., 'agent/spawn')input: Tool input parameters (validated by tool's schema)context?: Optional context objectReturns: Promise resolving to tool result
Throws: MCPClientError if tool not found or execution fails
Example:
const result = await callMCPTool<{ agentId: string }>('agent/spawn', {
agentType: 'coder',
priority: 'normal'
});
console.log(`Spawned agent: ${result.agentId}`);
getToolMetadata(toolName): ToolMetadata | undefinedGet tool metadata without executing it.
Example:
const metadata = getToolMetadata('agent/spawn');
if (metadata) {
console.log(`Description: ${metadata.description}`);
console.log(`Category: ${metadata.category}`);
console.log(`Schema:`, metadata.inputSchema);
}
listMCPTools(category?): ToolMetadata[]List all available MCP tools, optionally filtered by category.
Example:
// List all tools
const allTools = listMCPTools();
// List only agent tools
const agentTools = listMCPTools('agent');
hasTool(toolName): booleanCheck if an MCP tool exists.
Example:
if (hasTool('agent/spawn')) {
console.log('Agent spawn tool is available');
}
validateToolInput(toolName, input): { valid: boolean; errors?: string[] }Validate input against tool schema before calling.
Example:
const validation = validateToolInput('agent/spawn', {
agentType: 'coder'
// missing required field
});
if (!validation.valid) {
console.error('Validation errors:', validation.errors);
}
getToolCategories(): string[]Get all unique tool categories.
Example:
const categories = getToolCategories();
console.log('Available categories:', categories);
// Output: ['agent', 'swarm', 'memory', 'config']
MCPClientErrorCustom error class for MCP tool failures.
Properties:
message: Error messagetoolName: Name of the tool that failedcause?: Original error if availableExample:
try {
await callMCPTool('agent/spawn', { ... });
} catch (error) {
if (error instanceof MCPClientError) {
console.error(`Tool '${error.toolName}' failed: ${error.message}`);
if (error.cause) {
console.error('Caused by:', error.cause);
}
}
}
All CLI commands should follow this pattern:
import type { Command, CommandContext, CommandResult } from '../types.js';
import { output } from '../output.js';
import { select, confirm, input } from '../prompt.js';
import { callMCPTool, MCPClientError } from '../mcp-client.js';
const myCommand: Command = {
name: 'my-command',
description: 'Command description',
options: [ /* command options */ ],
action: async (ctx: CommandContext): Promise<CommandResult> => {
// STEP 1: Gather input (interactive prompts if needed)
let param = ctx.flags.param as string;
if (!param && ctx.interactive) {
param = await input({
message: 'Enter parameter:',
validate: (v) => v.length > 0 || 'Required'
});
}
// STEP 2: Validate required inputs
if (!param) {
output.printError('Parameter is required');
return { success: false, exitCode: 1 };
}
// STEP 3: Call MCP tool (business logic)
try {
const result = await callMCPTool<ResultType>('tool/name', {
param,
// ... other inputs
});
// STEP 4: Format and display output
if (ctx.flags.format === 'json') {
output.printJson(result);
} else {
output.printTable({
columns: [ /* ... */ ],
data: [ /* format result for display */ ]
});
}
output.printSuccess('Operation successful');
return { success: true, data: result };
} catch (error) {
// STEP 5: Handle errors
if (error instanceof MCPClientError) {
output.printError(`Failed: ${error.message}`);
} else {
output.printError(`Unexpected error: ${String(error)}`);
}
return { success: false, exitCode: 1 };
}
}
};
✅ Interactive prompts (select, confirm, input) ✅ Flag/argument parsing ✅ Input validation (basic checks) ✅ Output formatting (tables, boxes, colors) ✅ Progress indicators ✅ Success/error messages ✅ JSON output formatting
✅ Data validation (schema validation) ✅ Business rules enforcement ✅ Resource management (agents, swarms, memory) ✅ State changes ✅ Database operations ✅ External API calls ✅ Calculations and transformations
// Spawn an agent
const spawnCommand: Command = {
name: 'spawn',
action: async (ctx: CommandContext) => {
const agentType = ctx.flags.type as string;
try {
const result = await callMCPTool('agent/spawn', {
agentType,
priority: 'normal'
});
output.printSuccess(`Spawned agent: ${result.agentId}`);
return { success: true, data: result };
} catch (error) {
if (error instanceof MCPClientError) {
output.printError(error.message);
}
return { success: false, exitCode: 1 };
}
}
};
// List agents with filters
const listCommand: Command = {
name: 'list',
action: async (ctx: CommandContext) => {
try {
const result = await callMCPTool<{
agents: Agent[];
total: number;
}>('agent/list', {
status: ctx.flags.status || 'all',
agentType: ctx.flags.type,
limit: 100
});
// Display results
output.printTable({
columns: [
{ key: 'id', header: 'ID', width: 20 },
{ key: 'type', header: 'Type', width: 15 },
{ key: 'status', header: 'Status', width: 10 }
],
data: result.agents
});
output.printInfo(`Total: ${result.total} agents`);
return { success: true, data: result };
} catch (error) {
if (error instanceof MCPClientError) {
output.printError(error.message);
}
return { success: false, exitCode: 1 };
}
}
};
// Store memory with interactive input
const storeCommand: Command = {
name: 'store',
action: async (ctx: CommandContext) => {
// Get input interactively if not provided
let content = ctx.flags.content as string;
if (!content && ctx.interactive) {
content = await input({
message: 'Enter content to store:',
validate: (v) => v.length > 0 || 'Content required'
});
}
if (!content) {
output.printError('Content is required');
return { success: false, exitCode: 1 };
}
// Select memory type interactively
let type = ctx.flags.type as string;
if (!type && ctx.interactive) {
type = await select({
message: 'Select memory type:',
options: [
{ value: 'episodic', label: 'Episodic' },
{ value: 'semantic', label: 'Semantic' },
{ value: 'procedural', label: 'Procedural' }
]
});
}
try {
const result = await callMCPTool('memory/store', {
content,
type: type || 'episodic',
tags: ctx.flags.tags?.split(',') || [],
importance: ctx.flags.importance
});
output.printSuccess(`Stored memory: ${result.id}`);
return { success: true, data: result };
} catch (error) {
if (error instanceof MCPClientError) {
output.printError(error.message);
}
return { success: false, exitCode: 1 };
}
}
};
import { callMCPTool, MCPClientError, hasTool } from '../mcp-client.js';
describe('MCP Client', () => {
it('should call agent/spawn tool', async () => {
const result = await callMCPTool('agent/spawn', {
agentType: 'coder'
});
expect(result).toHaveProperty('agentId');
expect(result).toHaveProperty('agentType', 'coder');
});
it('should throw MCPClientError for unknown tool', async () => {
await expect(
callMCPTool('unknown/tool', {})
).rejects.toThrow(MCPClientError);
});
it('should check if tool exists', () => {
expect(hasTool('agent/spawn')).toBe(true);
expect(hasTool('unknown/tool')).toBe(false);
});
});
import { execute } from '../cli.js';
describe('Agent spawn command', () => {
it('should spawn agent via MCP tool', async () => {
const result = await execute(['agent', 'spawn', '--type', 'coder']);
expect(result.success).toBe(true);
expect(result.data).toHaveProperty('agentId');
});
});
Always provide type parameters to callMCPTool:
// ✅ Good: Type-safe
const result = await callMCPTool<{ agentId: string }>('agent/spawn', { ... });
console.log(result.agentId); // TypeScript knows this exists
// ❌ Bad: No type safety
const result = await callMCPTool('agent/spawn', { ... });
console.log(result.agentId); // No type checking
Always handle MCPClientError:
// ✅ Good: Specific error handling
try {
const result = await callMCPTool(...);
} catch (error) {
if (error instanceof MCPClientError) {
output.printError(`Tool failed: ${error.message}`);
} else {
output.printError(`Unexpected error: ${String(error)}`);
}
return { success: false, exitCode: 1 };
}
// ❌ Bad: Generic error handling
try {
const result = await callMCPTool(...);
} catch (error) {
console.error(error); // User sees raw error
}
Validate inputs before calling tools:
// ✅ Good: Validate first
if (!agentId) {
output.printError('Agent ID is required');
return { success: false, exitCode: 1 };
}
const result = await callMCPTool('agent/status', { agentId });
// ❌ Bad: Let tool fail
const result = await callMCPTool('agent/status', { agentId }); // Might be undefined
Keep display logic in CLI, not in tool results:
// ✅ Good: CLI formats output
const result = await callMCPTool('agent/list', { ... });
const displayData = result.agents.map(agent => ({
id: agent.id,
type: agent.agentType,
created: new Date(agent.createdAt).toLocaleString() // Format in CLI
}));
output.printTable({ data: displayData });
// ❌ Bad: Expect pre-formatted data from tool
const result = await callMCPTool('agent/list', { ... });
output.printTable({ data: result.formattedAgents }); // Tool shouldn't format
Use feature detection for optional capabilities:
// Check if tool supports feature
const metadata = getToolMetadata('agent/status');
const supportsMetrics = metadata?.inputSchema.properties?.includeMetrics;
const result = await callMCPTool('agent/status', {
agentId,
includeMetrics: supportsMetrics ? true : undefined
});
Problem: MCPClientError: MCP tool not found: xyz/abc
Solutions:
mcp-client.tsProblem: TypeScript errors when calling callMCPTool
Solutions:
callMCPTool<ResultType>(...)Problem: Tool execution fails with validation error
Solutions:
validateToolInput() before callingWhen adding new CLI commands:
callMCPTool and MCPClientErrorMCPClientErrorThe MCP Client provides a clean, type-safe way for CLI commands to call MCP tools while maintaining proper separation of concerns:
This architecture ensures maintainability, testability, and consistency across all interfaces to the claude-flow system.