MCP Server Template

import { Tool } from '@modelcontextprotocol/sdk/types.js' import { log } from '../utils/helpers.js' import { ToolHandlers } from '../utils/types.js' import z from 'zod' import { execa } from 'execa' import commandExists from 'command-exists' import { ToolResult } from '../utils/TestClient.js' // Define input validation schema using zod const architectInputSchema = z.object({ input: z.string().min(1, 'Input must not be empty'), conversationId: z.string().optional() }); // Tool definition const ARCHITECT_TOOL: Tool = { name: 'architect', description: 'MCP server for the LLM Architect tool. Exposes resource "/llm-architect/chat" accepting POST requests with a prompt and optional conversationId, and interacts with the llm chat CLI to provide architectural design feedback while maintaining conversation context.', inputSchema: { type: 'object', properties: { input: { type: 'string', description: 'Input prompt to process', minLength: 1 }, conversationId: { type: 'string', description: 'Optional conversation ID for context', nullable: true } }, required: ['input'] } }; // Export tools export const ARCHITECT_TOOLS = [ARCHITECT_TOOL]; async function executeCommand(args: string[], input?: string): Promise<string> { log('Executing command', { args, input }); try { const result = await execa('llm', args, input ? { input, env: process.env } : { env: process.env }); const output = result.stdout.trim(); log('Command output', output); if (!output) { throw new Error('Command failed to return a response'); } return output; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to execute LLM command: ${errorMessage}`); } } // Helper function to get the latest conversation ID async function getLatestConversationId(): Promise<string> { log('Getting latest conversation ID'); const output = await executeCommand(['logs', '--current', '--json']); let logs; try { logs = JSON.parse(output); } catch (e) { throw new Error('Failed to parse logs JSON'); } if (!Array.isArray(logs) || logs.length === 0 || !logs[0]?.conversation_id) { throw new Error('No valid conversation ID found in logs'); } const conversationId = logs[0].conversation_id; log('Found conversation ID', conversationId); return conversationId; } // Helper function to handle conversation logic async function handleConversation(cleanedInput: string, conversationId?: string): Promise<{ conversationId: string, response: string }> { log('Handling conversation', { cleanedInput, conversationId }); if (conversationId) { const response = await executeCommand(['prompt', '--conversation', conversationId, '--no-stream'], cleanedInput); log('Continued conversation response', response); return { conversationId, response }; } else { const response = await executeCommand(['prompt', '--no-stream'], cleanedInput); const newConversationId = await getLatestConversationId(); log('New conversation created', { response, newConversationId }); return { conversationId: newConversationId, response }; } } // Main processing function for architect async function handleArchitectProcess(input: string, conversationId?: string): Promise<{ conversationId: string, response: string }> { const cleanedInput = input.replace(/\n/g, ' ').trim(); return await handleConversation(cleanedInput, conversationId); } // Export handlers export const ARCHITECT_HANDLERS: ToolHandlers = { 'architect': async (request): Promise<ToolResult> => { try { const { input, conversationId } = request.params.arguments as { input: string, conversationId?: string }; // Validate input using zod architectInputSchema.parse({ input, conversationId }); // Ensure the 'llm' command exists if (!(await commandExists('llm'))) { throw new Error('LLM command not found. Please ensure it is installed and in your PATH.'); } const result = await handleArchitectProcess(input, conversationId); return { toolResult: { content: [{ type: 'text', text: JSON.stringify({ conversationId: result.conversationId, response: result.response }) }], } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to process input: ${errorMessage}`); } } };