smalltalk-ai
Version:
A complete TypeScript framework for building LLM applications with agent support and MCP integration
330 lines • 12.3 kB
JavaScript
import { EventEmitter } from 'events';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
import { ListToolsResultSchema, CallToolResultSchema, ListResourcesResultSchema, ReadResourceResultSchema, ListPromptsResultSchema, GetPromptResultSchema } from '@modelcontextprotocol/sdk/types.js';
export class MCPClient extends EventEmitter {
clients = new Map();
serverConfigs = new Map();
isConnected = false;
constructor() {
super();
}
async connect(serverConfig) {
if (this.clients.has(serverConfig.name)) {
throw new Error(`MCP server '${serverConfig.name}' is already connected`);
}
try {
const client = await this.createClient(serverConfig);
this.clients.set(serverConfig.name, client);
this.serverConfigs.set(serverConfig.name, serverConfig);
this.emit('server_connected', {
serverName: serverConfig.name,
serverType: serverConfig.type
});
console.log(`[MCPClient] Connected to server: ${serverConfig.name}`);
}
catch (error) {
console.error(`[MCPClient] Failed to connect to server ${serverConfig.name}:`, error);
throw error;
}
}
async createClient(config) {
let transport;
if (config.type === 'stdio') {
if (!config.command) {
throw new Error(`Command is required for stdio MCP server: ${config.name}`);
}
transport = new StdioClientTransport({
command: config.command,
args: config.args || [],
env: config.env
});
}
else if (config.type === 'http') {
if (!config.url) {
throw new Error(`URL is required for HTTP MCP server: ${config.name}`);
}
transport = new SSEClientTransport(new URL(config.url));
}
else {
throw new Error(`Unsupported MCP server type: ${config.type}`);
}
const client = new Client({
name: 'smalltalk-framework',
version: '0.1.0'
}, {
capabilities: {
tools: {},
resources: {},
prompts: {}
}
});
await client.connect(transport);
return client;
}
async disconnect(serverName) {
if (serverName) {
const client = this.clients.get(serverName);
if (client) {
await client.close();
this.clients.delete(serverName);
this.serverConfigs.delete(serverName);
this.emit('server_disconnected', { serverName });
console.log(`[MCPClient] Disconnected from server: ${serverName}`);
}
}
else {
// Disconnect all servers
for (const [name, client] of this.clients) {
try {
await client.close();
this.emit('server_disconnected', { serverName: name });
console.log(`[MCPClient] Disconnected from server: ${name}`);
}
catch (error) {
console.error(`[MCPClient] Error disconnecting from ${name}:`, error);
}
}
this.clients.clear();
this.serverConfigs.clear();
}
this.isConnected = this.clients.size > 0;
}
async getAvailableTools() {
const allTools = [];
for (const [serverName, client] of this.clients) {
try {
const request = {
method: 'tools/list',
params: {}
};
const response = await client.request(request, ListToolsResultSchema);
if (response.tools) {
for (const tool of response.tools) {
const toolDef = {
name: `${serverName}:${tool.name}`,
description: tool.description || '',
parameters: tool.inputSchema || {},
handler: async (params) => {
return await this.executeTool(serverName, tool.name, params);
}
};
allTools.push(toolDef);
}
}
}
catch (error) {
console.error(`[MCPClient] Failed to list tools from ${serverName}:`, error);
}
}
return allTools;
}
async executeTool(serverName, toolName, parameters) {
const client = this.clients.get(serverName);
if (!client) {
throw new Error(`MCP server '${serverName}' is not connected`);
}
try {
const request = {
method: 'tools/call',
params: {
name: toolName,
arguments: parameters
}
};
const response = await client.request(request, CallToolResultSchema);
this.emit('tool_executed', {
serverName,
toolName,
parameters,
result: response.content
});
return response.content;
}
catch (error) {
console.error(`[MCPClient] Tool execution failed (${serverName}:${toolName}):`, error);
throw error;
}
}
async getAvailableResources() {
const allResources = [];
for (const [serverName, client] of this.clients) {
try {
const request = {
method: 'resources/list',
params: {}
};
const response = await client.request(request, ListResourcesResultSchema);
if (response.resources) {
for (const resource of response.resources) {
allResources.push({
uri: resource.uri,
name: resource.name,
description: resource.description,
mimeType: resource.mimeType
});
}
}
}
catch (error) {
console.error(`[MCPClient] Failed to list resources from ${serverName}:`, error);
}
}
return allResources;
}
async readResource(uri) {
// Find which server can handle this resource
for (const [serverName, client] of this.clients) {
try {
const request = {
method: 'resources/read',
params: { uri }
};
const response = await client.request(request, ReadResourceResultSchema);
if (response.contents && response.contents.length > 0) {
const content = response.contents[0];
if (content.text) {
this.emit('resource_read', { serverName, uri, content: content.text });
return content.text;
}
else if (content.blob) {
// Handle binary content
this.emit('resource_read', { serverName, uri, content: 'Binary content' });
return 'Binary content (not displayed)';
}
}
}
catch (error) {
// Continue to next server if this one can't handle the resource
continue;
}
}
throw new Error(`No MCP server can read resource: ${uri}`);
}
async getAvailablePrompts() {
const allPrompts = [];
for (const [serverName, client] of this.clients) {
try {
const request = {
method: 'prompts/list',
params: {}
};
const response = await client.request(request, ListPromptsResultSchema);
if (response.prompts) {
for (const prompt of response.prompts) {
allPrompts.push({
name: `${serverName}:${prompt.name}`,
description: prompt.description,
arguments: prompt.arguments
});
}
}
}
catch (error) {
console.error(`[MCPClient] Failed to list prompts from ${serverName}:`, error);
}
}
return allPrompts;
}
async getPrompt(serverName, promptName, arguments_) {
const client = this.clients.get(serverName);
if (!client) {
throw new Error(`MCP server '${serverName}' is not connected`);
}
try {
const request = {
method: 'prompts/get',
params: {
name: promptName,
arguments: arguments_
}
};
const response = await client.request(request, GetPromptResultSchema);
if (response.messages && response.messages.length > 0) {
const message = response.messages[0];
if (message.content.type === 'text') {
const template = {
name: `${serverName}:${promptName}`,
template: message.content.text,
variables: Object.keys(arguments_ || {}),
description: response.description
};
this.emit('prompt_retrieved', {
serverName,
promptName,
template
});
return template;
}
}
}
catch (error) {
console.error(`[MCPClient] Failed to get prompt (${serverName}:${promptName}):`, error);
throw error;
}
return undefined;
}
getConnectedServers() {
return Array.from(this.clients.keys());
}
isServerConnected(serverName) {
return this.clients.has(serverName);
}
getServerConfig(serverName) {
return this.serverConfigs.get(serverName);
}
async testConnection(serverName) {
const client = this.clients.get(serverName);
if (!client) {
return false;
}
try {
// Try to list tools as a simple health check
const request = {
method: 'tools/list',
params: {}
};
await client.request(request, ListToolsResultSchema);
return true;
}
catch (error) {
console.error(`[MCPClient] Connection test failed for ${serverName}:`, error);
return false;
}
}
getStats() {
return {
connectedServers: this.clients.size,
serverNames: this.getConnectedServers(),
isConnected: this.isConnected
};
}
async reconnect(serverName) {
const config = this.serverConfigs.get(serverName);
if (!config) {
throw new Error(`No configuration found for server: ${serverName}`);
}
// Disconnect if already connected
if (this.clients.has(serverName)) {
await this.disconnect(serverName);
}
// Reconnect
await this.connect(config);
}
async reconnectAll() {
const configs = Array.from(this.serverConfigs.values());
// Disconnect all
await this.disconnect();
// Reconnect all
for (const config of configs) {
try {
await this.connect(config);
}
catch (error) {
console.error(`[MCPClient] Failed to reconnect to ${config.name}:`, error);
}
}
}
}
//# sourceMappingURL=MCPClient.js.map