#!/usr/bin/env node
/**
* MCP Learning Client - Frontend/Client Side Implementation
*
* This demonstrates how to build a client that connects to MCP servers.
* You'll learn:
* 1. How to establish connections to MCP servers
* 2. How to discover available capabilities
* 3. How to call tools and handle responses
* 4. How to read resources
* 5. How to use prompts
* 6. Error handling and best practices
*/
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { spawn } from 'child_process';
import * as readline from 'readline';
// ============================================================================
// LESSON 1: Understanding MCP Client Structure
// ============================================================================
/**
* MCP clients connect to MCP servers and interact with their capabilities:
* 1. Establish transport connection (stdio, HTTP, etc.)
* 2. Initialize the client with server capabilities
* 3. Discover and call tools, read resources, use prompts
* 4. Handle responses and errors
*/
class MCPLearningClient {
private client: Client;
private transport!: StdioClientTransport;
private rl: readline.Interface;
private serverCapabilities: any = null;
constructor() {
this.client = new Client(
{
name: 'mcp-learning-client',
version: '1.0.0',
},
{
capabilities: {
roots: {
listChanged: true,
},
sampling: {},
},
}
);
this.rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
this.setupEventHandlers();
}
// ============================================================================
// LESSON 2: Establishing Connection to MCP Server
// ============================================================================
async connectToServer(serverPath: string): Promise<void> {
try {
console.log('π Connecting to MCP Learning Server...');
// Create transport that will spawn the server process
this.transport = new StdioClientTransport({
command: 'node',
args: [serverPath],
});
// Connect the client to the server
const result = await this.client.connect(this.transport);
this.serverCapabilities = result;
console.log('β
Connected successfully!');
console.log('π Server capabilities:', JSON.stringify(result, null, 2));
} catch (error) {
console.error('β Failed to connect to server:', error);
throw error;
}
}
// ============================================================================
// LESSON 3: Discovering Server Capabilities
// ============================================================================
async discoverCapabilities(): Promise<void> {
console.log('\nπ Discovering server capabilities...\n');
try {
// Check if server capabilities are available
if (!this.serverCapabilities || !this.serverCapabilities.capabilities) {
console.log('β οΈ Server capabilities not available, attempting to discover anyway...');
}
// Discover available tools
console.log('π οΈ AVAILABLE TOOLS:');
const toolsResult = await this.client.listTools();
toolsResult.tools.forEach((tool, index) => {
console.log(`\n${index + 1}. ${tool.name}`);
console.log(` Description: ${tool.description}`);
console.log(` Parameters: ${JSON.stringify(tool.inputSchema.properties, null, 4)}`);
});
// Discover available resources
console.log('\n\nπ AVAILABLE RESOURCES:');
const resourcesResult = await this.client.listResources();
resourcesResult.resources.forEach((resource, index) => {
console.log(`\n${index + 1}. ${resource.name}`);
console.log(` URI: ${resource.uri}`);
console.log(` Description: ${resource.description}`);
console.log(` Type: ${resource.mimeType}`);
});
// Discover available prompts
console.log('\n\n㪠AVAILABLE PROMPTS:');
const promptsResult = await this.client.listPrompts();
promptsResult.prompts.forEach((prompt, index) => {
console.log(`\n${index + 1}. ${prompt.name}`);
console.log(` Description: ${prompt.description}`);
if (prompt.arguments) {
console.log(` Arguments:`, prompt.arguments.map(arg =>
`${arg.name}${arg.required ? '*' : ''}: ${arg.description}`
).join(', '));
}
});
} catch (error) {
console.error('β Failed to discover capabilities:', error);
}
}
// ============================================================================
// LESSON 4: Interactive Learning Sessions
// ============================================================================
async startInteractiveSession(): Promise<void> {
console.log('\nπ Welcome to the MCP Learning Interactive Session!');
console.log('Type "help" for available commands, "quit" to exit.\n');
while (true) {
try {
const input = await this.askQuestion('mcp-learning> ');
const [command, ...args] = input.trim().split(' ');
if (command === 'quit' || command === 'exit') {
break;
}
await this.handleCommand(command, args);
} catch (error) {
console.error('β Error:', error);
}
}
}
private async handleCommand(command: string, args: string[]): Promise<void> {
switch (command) {
case 'help':
this.showHelp();
break;
case 'tools':
await this.listTools();
break;
case 'resources':
await this.listResources();
break;
case 'prompts':
await this.listPrompts();
break;
case 'call':
await this.callTool(args);
break;
case 'read':
await this.readResource(args);
break;
case 'prompt':
await this.usePrompt(args);
break;
case 'demo':
await this.runDemo(args[0] || 'beginner');
break;
case 'clear':
console.clear();
break;
default:
console.log('β Unknown command. Type "help" for available commands.');
}
}
private showHelp(): void {
console.log(`
π Available Commands:
Basic Commands:
help - Show this help message
tools - List all available tools
resources - List all available resources
prompts - List all available prompts
clear - Clear the screen
quit/exit - Exit the client
Interaction Commands:
call <tool> [args] - Call a tool with JSON arguments
read <resource> - Read a resource by name or URI
prompt <name> [args] - Use a prompt template
Learning Commands:
demo [level] - Run a guided demo (beginner/intermediate/advanced)
Examples:
call hello_world {"name": "Alice"}
call calculator {"operation": "add", "a": 5, "b": 3}
read mcp-concepts
prompt mcp-tutorial {"level": "beginner"}
demo intermediate
`);
}
// ============================================================================
// LESSON 5: Tool Interaction Examples
// ============================================================================
private async listTools(): Promise<void> {
try {
const result = await this.client.listTools();
console.log('\nπ οΈ Available Tools:\n');
result.tools.forEach((tool, index) => {
console.log(`${index + 1}. ${tool.name}`);
console.log(` ${tool.description}`);
console.log(` Usage: call ${tool.name} ${JSON.stringify(this.getExampleArgs(tool.name))}`);
console.log('');
});
} catch (error) {
console.error('β Failed to list tools:', error);
}
}
private async callTool(args: string[]): Promise<void> {
if (args.length < 1) {
console.log('β Usage: call <tool_name> [json_arguments]');
return;
}
const toolName = args[0];
let toolArgs = {};
// Parse JSON arguments if provided
if (args.length > 1) {
try {
toolArgs = JSON.parse(args.slice(1).join(' '));
} catch (error) {
console.error('β Invalid JSON arguments. Example: {"name": "value"}');
return;
}
}
try {
console.log(`π§ Calling tool: ${toolName}`);
console.log(`π Arguments: ${JSON.stringify(toolArgs, null, 2)}`);
const startTime = Date.now();
const result = await this.client.callTool({
name: toolName,
arguments: toolArgs,
});
const duration = Date.now() - startTime;
console.log(`β
Tool completed in ${duration}ms`);
console.log('π Response:');
(result.content as any[]).forEach((content: any, index: number) => {
console.log(`\n--- Content ${index + 1} (${content.type}) ---`);
if (content.type === 'text') {
console.log(content.text);
} else {
console.log(JSON.stringify(content, null, 2));
}
});
} catch (error: any) {
console.error(`β Tool call failed: ${error.message}`);
if (error.code) {
console.error(` Error code: ${error.code}`);
}
}
}
private getExampleArgs(toolName: string): any {
const examples: { [key: string]: any } = {
hello_world: { name: "World" },
calculator: { operation: "add", a: 5, b: 3 },
counter: { action: "increment", amount: 1 },
data_processor: { data: [1, 2, 3, 4, 5], operation: "sum" },
task_manager: { action: "list" },
error_demo: { error_type: "none" },
};
return examples[toolName] || {};
}
// ============================================================================
// LESSON 6: Resource Reading Examples
// ============================================================================
private async listResources(): Promise<void> {
try {
const result = await this.client.listResources();
console.log('\nπ Available Resources:\n');
result.resources.forEach((resource, index) => {
console.log(`${index + 1}. ${resource.name}`);
console.log(` URI: ${resource.uri}`);
console.log(` Description: ${resource.description}`);
console.log(` Type: ${resource.mimeType}`);
console.log(` Usage: read ${resource.name.replace(/\s+/g, '-').toLowerCase()}`);
console.log('');
});
} catch (error) {
console.error('β Failed to list resources:', error);
}
}
private async readResource(args: string[]): Promise<void> {
if (args.length < 1) {
console.log('β Usage: read <resource_name_or_uri>');
return;
}
const resourceIdentifier = args.join(' ');
// Map friendly names to URIs
const resourceMap: { [key: string]: string } = {
'mcp-concepts': 'learning://mcp-concepts',
'server-state': 'learning://server-state',
'best-practices': 'learning://best-practices',
};
const uri = resourceMap[resourceIdentifier] || resourceIdentifier;
try {
console.log(`π Reading resource: ${uri}`);
const result = await this.client.readResource({ uri });
console.log('β
Resource loaded successfully');
result.contents.forEach((content, index) => {
console.log(`\n--- Content ${index + 1} (${content.mimeType}) ---`);
console.log(content.text);
});
} catch (error: any) {
console.error(`β Failed to read resource: ${error.message}`);
}
}
// ============================================================================
// LESSON 7: Prompt Usage Examples
// ============================================================================
private async listPrompts(): Promise<void> {
try {
const result = await this.client.listPrompts();
console.log('\n㪠Available Prompts:\n');
result.prompts.forEach((prompt, index) => {
console.log(`${index + 1}. ${prompt.name}`);
console.log(` ${prompt.description}`);
if (prompt.arguments && prompt.arguments.length > 0) {
console.log(` Arguments: ${prompt.arguments.map(arg =>
`${arg.name}${arg.required ? '*' : ''}`
).join(', ')}`);
console.log(` Usage: prompt ${prompt.name} ${JSON.stringify(this.getExamplePromptArgs(prompt.name))}`);
} else {
console.log(` Usage: prompt ${prompt.name}`);
}
console.log('');
});
} catch (error) {
console.error('β Failed to list prompts:', error);
}
}
private async usePrompt(args: string[]): Promise<void> {
if (args.length < 1) {
console.log('β Usage: prompt <prompt_name> [json_arguments]');
return;
}
const promptName = args[0];
let promptArgs = {};
if (args.length > 1) {
try {
promptArgs = JSON.parse(args.slice(1).join(' '));
} catch (error) {
console.error('β Invalid JSON arguments. Example: {"level": "beginner"}');
return;
}
}
try {
console.log(`π¬ Using prompt: ${promptName}`);
console.log(`π Arguments: ${JSON.stringify(promptArgs, null, 2)}`);
const result = await this.client.getPrompt({
name: promptName,
arguments: promptArgs,
});
console.log('β
Prompt loaded successfully');
console.log(`π Description: ${result.description}`);
console.log('\nπ Messages:');
result.messages.forEach((message, index) => {
console.log(`\n--- Message ${index + 1} (${message.role}) ---`);
console.log(message.content.text);
});
} catch (error: any) {
console.error(`β Failed to use prompt: ${error.message}`);
}
}
private getExamplePromptArgs(promptName: string): any {
const examples: { [key: string]: any } = {
'mcp-tutorial': { level: "beginner" },
'debug-session': { issue: "Tool not working properly" },
};
return examples[promptName] || {};
}
// ============================================================================
// LESSON 8: Guided Demo Sessions
// ============================================================================
private async runDemo(level: string): Promise<void> {
console.log(`\nπ― Starting ${level} level demo...\n`);
switch (level) {
case 'beginner':
await this.runBeginnerDemo();
break;
case 'intermediate':
await this.runIntermediateDemo();
break;
case 'advanced':
await this.runAdvancedDemo();
break;
default:
console.log('β Available levels: beginner, intermediate, advanced');
}
}
private async runBeginnerDemo(): Promise<void> {
console.log('π Beginner Demo: Understanding Basic MCP Concepts\n');
// Step 1: Simple tool call
console.log('Step 1: Calling a simple tool');
await this.callTool(['hello_world', '{"name": "MCP Learner"}']);
await this.wait(2000);
// Step 2: Tool with parameters
console.log('\nStep 2: Using a tool with parameters');
await this.callTool(['calculator', '{"operation": "add", "a": 10, "b": 15}']);
await this.wait(2000);
// Step 3: Reading a resource
console.log('\nStep 3: Reading educational content');
await this.readResource(['mcp-concepts']);
await this.wait(2000);
console.log('\nβ
Beginner demo complete! Try "demo intermediate" next.');
}
private async runIntermediateDemo(): Promise<void> {
console.log('π§ Intermediate Demo: State Management and Data Processing\n');
// Step 1: Counter with state
console.log('Step 1: Working with stateful tools');
await this.callTool(['counter', '{"action": "get"}']);
await this.callTool(['counter', '{"action": "increment", "amount": 5}']);
await this.callTool(['counter', '{"action": "get"}']);
await this.wait(2000);
// Step 2: Data processing
console.log('\nStep 2: Processing arrays of data');
await this.callTool(['data_processor', '{"data": [1, 5, 3, 9, 2, 7], "operation": "sort"}']);
await this.callTool(['data_processor', '{"data": [1, 5, 3, 9, 2, 7], "operation": "average"}']);
await this.wait(2000);
// Step 3: Check server state
console.log('\nStep 3: Examining server state');
await this.readResource(['server-state']);
console.log('\nβ
Intermediate demo complete! Try "demo advanced" for complex operations.');
}
private async runAdvancedDemo(): Promise<void> {
console.log('π Advanced Demo: CRUD Operations and Error Handling\n');
// Step 1: Create tasks
console.log('Step 1: Creating resources with CRUD operations');
await this.callTool(['task_manager', '{"action": "create", "task": {"title": "Learn MCP", "description": "Complete MCP tutorial", "priority": "high"}}']);
await this.callTool(['task_manager', '{"action": "create", "task": {"title": "Build MCP Server", "description": "Create my own server", "priority": "medium"}}']);
await this.wait(2000);
// Step 2: List and read tasks
console.log('\nStep 2: Listing and reading resources');
const listResult = await this.client.callTool({
name: 'task_manager',
arguments: { action: 'list' }
});
// Extract a task ID for demonstration
const response = JSON.parse((listResult.content as any[])[0].text);
if (response.tasks && response.tasks.length > 0) {
const taskId = response.tasks[0].id;
await this.callTool(['task_manager', `{"action": "read", "id": "${taskId}"}`]);
}
await this.wait(2000);
// Step 3: Error handling demonstration
console.log('\nStep 3: Understanding error handling');
await this.callTool(['error_demo', '{"error_type": "none"}']);
await this.callTool(['error_demo', '{"error_type": "validation"}']);
await this.wait(2000);
// Step 4: Best practices resource
console.log('\nStep 4: Learning best practices');
await this.readResource(['best-practices']);
console.log('\nβ
Advanced demo complete! You now understand MCP fundamentals.');
}
// ============================================================================
// LESSON 9: Event Handling and Cleanup
// ============================================================================
private setupEventHandlers(): void {
// Handle client events
this.client.onclose = () => {
console.log('π Connection to server closed');
};
this.client.onerror = (error) => {
console.error('β Client error:', error);
};
// Handle process cleanup
process.on('SIGINT', async () => {
console.log('\nπ Shutting down client...');
await this.cleanup();
process.exit(0);
});
}
private async cleanup(): Promise<void> {
try {
if (this.client) {
await this.client.close();
}
if (this.rl) {
this.rl.close();
}
} catch (error) {
console.error('Error during cleanup:', error);
}
}
// ============================================================================
// LESSON 10: Utility Methods
// ============================================================================
private askQuestion(question: string): Promise<string> {
return new Promise((resolve) => {
this.rl.question(question, resolve);
});
}
private wait(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
// ============================================================================
// Main Application Entry Point
// ============================================================================
async run(): Promise<void> {
try {
// Check command line arguments
const serverPath = process.argv[2];
if (!serverPath) {
console.error('β Usage: node mcp-learning-client.js <path-to-server.js>');
console.error(' Example: node dist/client.js dist/server.js');
process.exit(1);
}
// Connect to the server
await this.connectToServer(serverPath);
// Discover what the server can do
await this.discoverCapabilities();
// Start interactive session
await this.startInteractiveSession();
} catch (error) {
console.error('β Application error:', error);
} finally {
await this.cleanup();
}
}
}
// ============================================================================
// Application Startup
// ============================================================================
console.log('π MCP Learning Client Starting...');
console.log('π This client demonstrates how to interact with MCP servers');
console.log('π Connecting to MCP Learning Server...\n');
const client = new MCPLearningClient();
client.run().catch((error) => {
console.error('π₯ Failed to start MCP Learning Client:', error);
process.exit(1);
});