# Directory Structure ``` ├── .env.example ├── .gitattributes ├── .gitignore ├── config │ ├── models.yaml │ └── system_instructions.yaml ├── config.example.ts ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── src │ ├── index.ts │ ├── providers │ │ └── openrouter.ts │ ├── stores │ │ ├── FileSystemStore.ts │ │ └── Store.ts │ └── types │ ├── conversation.ts │ ├── errors.ts │ └── server.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- ``` # Auto detect text files and perform LF normalization * text=auto ``` -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- ``` # OpenAI Configuration OPENAI_API_KEY=your_openai_key_here # DeepSeek Configuration DEEPSEEK_API_KEY=your_api_key_here # OpenRouter Configuration OPENROUTER_API_KEY=your-openrouter-api-key # Server Configuration DATA_DIR=./data/conversations LOG_LEVEL=info # debug, info, warn, error # Conversation storage path CONVERSATIONS_PATH=d:\\Projects\\Conversations ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional stylelint cache .stylelintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variable files .env .env.development.local .env.test.local .env.production.local .env.local # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache # Next.js build output .next out # Nuxt.js build / generate output .nuxt dist # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # vuepress v2.x temp and cache directory .temp .cache # Docusaurus cache and generated files .docusaurus # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # yarn v2 .yarn/cache .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz .pnp.* node_modules ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # MCP Conversation Server A Model Context Protocol (MCP) server implementation for managing conversations with OpenRouter's language models. This server provides a standardized interface for applications to interact with various language models through a unified conversation management system. ## Features - **MCP Protocol Support** - Full MCP protocol compliance - Resource management and discovery - Tool-based interaction model - Streaming response support - Error handling and recovery - **OpenRouter Integration** - Support for all OpenRouter models - Real-time streaming responses - Automatic token counting - Model context window management - Available models include: - Claude 3 Opus - Claude 3 Sonnet - Llama 2 70B - And many more from OpenRouter's catalog - **Conversation Management** - Create and manage multiple conversations - Support for system messages - Message history tracking - Token usage monitoring - Conversation filtering and search - **Streaming Support** - Real-time message streaming - Chunked response handling - Token counting - **File System Persistence** - Conversation state persistence - Configurable storage location - Automatic state management ## Installation ```bash npm install mcp-conversation-server ``` ## Configuration ### Configuration All configuration for the MCP Conversation Server is now provided via YAML. Please update the `config/models.yaml` file with your settings. For example: ```yaml # MCP Server Configuration openRouter: apiKey: "YOUR_OPENROUTER_API_KEY" # Replace with your actual OpenRouter API key. persistence: path: "./conversations" # Directory for storing conversation data. models: # Define your models here 'provider/model-name': id: 'provider/model-name' contextWindow: 123456 streaming: true temperature: 0.7 description: 'Model description' # Default model to use if none specified defaultModel: 'provider/model-name' ``` ### Server Configuration The MCP Conversation Server now loads all its configuration from the YAML file. In your application, you can load the configuration as follows: ```typescript const config = await loadModelsConfig(); // Loads openRouter, persistence, models, and defaultModel settings from 'config/models.yaml' ``` *Note: Environment variables are no longer required as all configuration is provided via the YAML file.* ## Usage ### Basic Server Setup ```typescript import { ConversationServer } from 'mcp-conversation-server'; const server = new ConversationServer(config); server.run().catch(console.error); ``` ### Available Tools The server exposes several MCP tools: 1. **create-conversation** ```typescript { provider: 'openrouter', // Provider is always 'openrouter' model: string, // OpenRouter model ID (e.g., 'anthropic/claude-3-opus-20240229') title?: string; // Optional conversation title } ``` 2. **send-message** ```typescript { conversationId: string; // Conversation ID content: string; // Message content stream?: boolean; // Enable streaming responses } ``` 3. **list-conversations** ```typescript { filter?: { model?: string; // Filter by model startDate?: string; // Filter by start date endDate?: string; // Filter by end date } } ``` ### Resources The server provides access to several resources: 1. **conversation://{id}** - Access specific conversation details - View message history - Check conversation metadata 2. **conversation://list** - List all active conversations - Filter conversations by criteria - Sort by recent activity ## Development ### Building ```bash npm run build ``` ### Running Tests ```bash npm test ``` ### Debugging The server provides several debugging features: 1. **Error Logging** - All errors are logged with stack traces - Token usage tracking - Rate limit monitoring 2. **MCP Inspector** ```bash npm run inspector ``` Use the MCP Inspector to: - Test tool execution - View resource contents - Monitor message flow - Validate protocol compliance 3. **Provider Validation** ```typescript await server.providerManager.validateProviders(); ``` Validates: - API key validity - Model availability - Rate limit status ### Troubleshooting Common issues and solutions: 1. **OpenRouter Connection Issues** - Verify your API key is valid - Check rate limits on [OpenRouter's dashboard](https://openrouter.ai/dashboard) - Ensure the model ID is correct - Monitor credit usage 2. **Message Streaming Errors** - Verify model streaming support - Check connection stability - Monitor token limits - Handle timeout settings 3. **File System Errors** - Check directory permissions - Verify path configuration - Monitor disk space - Handle concurrent access ## Contributing 1. Fork the repository 2. Create a feature branch 3. Commit your changes 4. Push to the branch 5. Create a Pull Request ## License ISC License ``` -------------------------------------------------------------------------------- /src/stores/Store.ts: -------------------------------------------------------------------------------- ```typescript import { Conversation } from '../types/conversation.js'; export interface Store { initialize(): Promise<void>; saveConversation(conversation: Conversation): Promise<void>; getConversation(id: string): Promise<Conversation | null>; listConversations(): Promise<Conversation[]>; } ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "outDir": "./build", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*"], "exclude": ["node_modules"] } ``` -------------------------------------------------------------------------------- /src/types/conversation.ts: -------------------------------------------------------------------------------- ```typescript export interface Message { role: 'system' | 'user' | 'assistant'; content: string; timestamp: string; name?: string; } export interface Conversation { id: string; model: string; title: string; messages: Message[]; created: string; updated: string; } export interface ConversationFilter { model?: string; startDate?: string; endDate?: string; } ``` -------------------------------------------------------------------------------- /src/types/errors.ts: -------------------------------------------------------------------------------- ```typescript export class McpError extends Error { constructor(public code: string, message: string) { super(message); this.name = 'McpError'; } } export class ValidationError extends Error { constructor(message: string) { super(message); this.name = 'ValidationError'; } } export class OpenRouterError extends Error { constructor(message: string) { super(message); this.name = 'OpenRouterError'; } } export class FileSystemError extends Error { constructor(message: string) { super(message); this.name = 'FileSystemError'; } } ``` -------------------------------------------------------------------------------- /config/models.yaml: -------------------------------------------------------------------------------- ```yaml # OpenRouter Models Configuration # Visit https://openrouter.ai/docs#models for the complete list of available models # MCP Server Configuration openRouter: apiKey: "<<OPEN ROUTER>>" # Replace with your actual OpenRouter API key. persistence: path: "d:/projects/conversations" # Optional: Directory for storing conversation data. models: 'google/gemini-2.0-pro-exp-02-05:free': id: 'google/gemini-2.0-pro-exp-02-05:free' contextWindow: 2000000 streaming: true temperature: 0.2 description: 'Google Gemini 2.0 Pro is a powerful and versatile language model that can handle a wide range of tasks.' 'google/gemini-2.0-flash-001': id: 'google/gemini-2.0-flash-001' contextWindow: 1000000 streaming: true temperature: 0.2 description: 'Google Gemini 2.0 Flash is a powerful and versatile language model that can handle a wide range of tasks.' # Add more models as needed following the same format # Example: # 'provider/model-name': # id: 'provider/model-name' # contextWindow: <window_size> # streaming: true/false # description: 'Model description' # Default model to use if none specified defaultModel: 'google/gemini-2.0-pro-exp-02-05:free' ``` -------------------------------------------------------------------------------- /src/types/server.ts: -------------------------------------------------------------------------------- ```typescript export interface ResourceConfig { maxSizeBytes: number; allowedTypes: string[]; chunkSize: number; } export interface ServerConfig { openRouter: { apiKey: string; }; models: { [key: string]: { id: string; contextWindow: number; streaming: boolean; description?: string; }; }; defaultModel: string; persistence: { type: 'filesystem'; path: string; }; resources: { maxSizeBytes: number; allowedTypes: string[]; chunkSize: number; }; } export interface ModelConfig { contextWindow: number; streaming: boolean; } export interface PersistenceConfig { type: 'filesystem' | 'memory'; path?: string; } export interface CreateConversationParams { provider?: string; model?: string; title?: string; } export interface Conversation { id: string; provider: string; model: string; title: string; messages: Message[]; createdAt: number; updatedAt: number; } export interface Message { role: 'user' | 'assistant'; content: string; timestamp: number; context?: { documents?: string[]; code?: string[]; }; } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "mcp-conversation-server", "version": "0.1.0", "description": "A Model Context Protocol server used to execute various applicatoin types.", "private": true, "type": "module", "bin": { "mcp-conversation-server": "./build/index.js" }, "files": [ "build" ], "scripts": { "prebuild": "rimraf build", "build": "tsc && npm run copy-config", "copy-config": "copyfiles config/**/* build/", "start": "node build/index.js", "dev": "ts-node-esm src/index.ts", "test": "jest" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "@modelcontextprotocol/sdk": "^1.0.0", "@types/dotenv": "^8.2.3", "@types/express": "^4.17.21", "@types/uuid": "^9.0.7", "dotenv": "^16.4.7", "express": "^4.18.2", "openai": "^4.83.0", "uuid": "^9.0.1", "yaml": "^2.7.0" }, "devDependencies": { "@types/jest": "^29.5.14", "@types/node": "^20.11.24", "@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/parser": "^7.0.0", "copyfiles": "^2.4.1", "eslint": "^8.56.0", "jest": "^29.7.0", "prettier": "^3.4.2", "rimraf": "^5.0.10", "ts-jest": "^29.2.5", "ts-node-dev": "^2.0.0", "typescript": "^5.3.3" } } ``` -------------------------------------------------------------------------------- /config/system_instructions.yaml: -------------------------------------------------------------------------------- ```yaml # System Instructions Configuration # Define default and model-specific system instructions default: | You are a helpful AI assistant focused on providing accurate and concise responses. Please follow these guidelines: - Be direct and to the point - Show code examples when relevant - Explain complex concepts clearly - Ask for clarification when needed models: # DeepSeek Models 'deepseek/deepseek-chat': | You are DeepSeek Chat, a helpful AI assistant with strong coding and technical capabilities. Guidelines: - Focus on practical, implementable solutions - Provide code examples with explanations - Use clear technical explanations - Follow best practices in software development - Ask for clarification on ambiguous requirements 'deepseek/deepseek-r1': | You are DeepSeek Reasoner, an AI focused on step-by-step problem solving and logical reasoning. Guidelines: - Break down complex problems into steps - Show your reasoning process clearly - Validate assumptions - Consider edge cases - Provide concrete examples # Claude Models 'anthropic/claude-3-opus-20240229': | You are Claude 3 Opus, a highly capable AI assistant with strong analytical and creative abilities. Guidelines: - Provide comprehensive, well-reasoned responses - Balance depth with clarity - Use examples to illustrate complex points - Consider multiple perspectives - Maintain high standards of accuracy 'anthropic/claude-3-sonnet-20240229': | You are Claude 3 Sonnet, focused on efficient and practical problem-solving. Guidelines: - Provide concise, actionable responses - Focus on practical solutions - Use clear examples - Be direct and efficient - Ask for clarification when needed # Llama Models 'meta-llama/llama-2-70b-chat': | You are Llama 2, an open-source AI assistant focused on helpful and accurate responses. Guidelines: - Provide clear, straightforward answers - Use examples when helpful - Stay within known capabilities - Be direct about limitations - Focus on practical solutions ``` -------------------------------------------------------------------------------- /config.example.ts: -------------------------------------------------------------------------------- ```typescript import { ServerConfig } from './src/types/server.js'; import * as path from 'path'; /** * Example configuration for the MCP Conversation Server * * This configuration includes examples for all supported providers: * - OpenAI * - DeepSeek * - OpenRouter * * Storage paths can be configured in several ways: * 1. Use environment variable: CONVERSATIONS_PATH * 2. Set absolute path in config * 3. Set relative path (relative to project root) * 4. Let it default to OS-specific app data directory */ const config: ServerConfig = { providers: { deepseek: { endpoint: 'https://api.deepseek.com/v1', apiKey: process.env.DEEPSEEK_API_KEY || '', models: { 'deepseek-chat': { id: 'deepseek-chat', contextWindow: 32768, streaming: true }, 'deepseek-reasoner': { id: 'deepseek-reasoner', contextWindow: 64000, streaming: true } }, timeouts: { completion: 300000, // 5 minutes for non-streaming stream: 120000 // 2 minutes per stream chunk } }, openai: { endpoint: 'https://api.openai.com/v1', apiKey: process.env.OPENAI_API_KEY || '', models: { 'gpt-4': { id: 'gpt-4', contextWindow: 8192, streaming: true }, 'gpt-3.5-turbo': { id: 'gpt-3.5-turbo', contextWindow: 4096, streaming: true } }, timeouts: { completion: 300000, // 5 minutes for non-streaming stream: 60000 // 1 minute per stream chunk } } }, defaultProvider: 'deepseek', defaultModel: 'deepseek-chat', persistence: { type: 'filesystem' as const, // Use environment variable or default to d:\Projects\Conversations path: process.env.CONVERSATIONS_PATH || path.normalize('d:\\Projects\\Conversations') }, resources: { maxSizeBytes: 10 * 1024 * 1024, // 10MB allowedTypes: ['.txt', '.md', '.json', '.csv'], chunkSize: 1024 // 1KB chunks } }; export default config; /** * Example usage: * * ```typescript * import { ConversationServer } from './src/index.js'; * import { config } from './config.js'; * * // Override storage path if needed * config.persistence.path = '/custom/path/to/conversations'; * * const server = new ConversationServer(config); * server.initialize().then(() => { * console.log('Server initialized, connecting...'); * server.connect().catch(err => console.error('Failed to connect:', err)); * }).catch(err => console.error('Failed to initialize:', err)); * ``` */ ``` -------------------------------------------------------------------------------- /src/stores/FileSystemStore.ts: -------------------------------------------------------------------------------- ```typescript import * as fs from 'fs/promises'; import * as path from 'path'; import { Conversation } from '../types/conversation.js'; import { Store } from './Store.js'; interface FSError extends Error { code?: string; message: string; } function isFSError(error: unknown): error is FSError { return error instanceof Error && ('code' in error || 'message' in error); } export class FileSystemStore implements Store { private dataPath: string; private initialized: boolean = false; constructor(dataPath: string) { this.dataPath = dataPath; } async initialize(): Promise<void> { if (this.initialized) { return; } try { await fs.mkdir(this.dataPath, { recursive: true }); this.initialized = true; } catch (error) { throw new Error(`Failed to initialize store: ${error instanceof Error ? error.message : String(error)}`); } } private getConversationPath(id: string): string { return path.join(this.dataPath, `${id}.json`); } async saveConversation(conversation: Conversation): Promise<void> { const filePath = this.getConversationPath(conversation.id); try { await fs.writeFile(filePath, JSON.stringify(conversation, null, 2)); } catch (error) { throw new Error(`Failed to save conversation: ${error instanceof Error ? error.message : String(error)}`); } } async getConversation(id: string): Promise<Conversation | null> { const filePath = this.getConversationPath(id); try { const data = await fs.readFile(filePath, 'utf-8'); return JSON.parse(data) as Conversation; } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') { return null; } throw new Error(`Failed to read conversation: ${error instanceof Error ? error.message : String(error)}`); } } async listConversations(): Promise<Conversation[]> { try { const files = await fs.readdir(this.dataPath); const conversations: Conversation[] = []; for (const file of files) { if (path.extname(file) === '.json') { try { const data = await fs.readFile(path.join(this.dataPath, file), 'utf-8'); conversations.push(JSON.parse(data) as Conversation); } catch (error) { console.error(`Failed to read conversation file ${file}:`, error); } } } return conversations; } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') { return []; } throw new Error(`Failed to list conversations: ${error instanceof Error ? error.message : String(error)}`); } } async deleteConversation(id: string): Promise<void> { const filePath = this.getConversationPath(id); try { await fs.unlink(filePath); } catch (error) { if (isFSError(error)) { if (error.code !== 'ENOENT') { throw new Error(`Failed to delete conversation ${id}: ${error.message}`); } } else { throw new Error(`Failed to delete conversation ${id}: Unknown error`); } } } } ``` -------------------------------------------------------------------------------- /src/providers/openrouter.ts: -------------------------------------------------------------------------------- ```typescript import OpenAI from 'openai'; import { Message } from '../types/conversation.js'; interface Model { id: string; contextWindow: number; streaming: boolean; supportsFunctions?: boolean; temperature?: number; description?: string; } interface ProviderConfig { apiKey: string; models: { [key: string]: { id: string; contextWindow: number; streaming: boolean; temperature?: number; description?: string; }; }; defaultModel: string; timeouts?: { completion?: number; stream?: number; }; } interface ProviderResponse { content: string; model: string; tokenCount?: number; metadata?: Record<string, unknown>; } interface CompletionParams { messages: Message[]; model: string; stream: boolean; timeout?: number; temperature?: number; maxTokens?: number; } interface ModelInfo extends Model { isDefault: boolean; provider: string; cost?: { prompt: number; completion: number; }; } export class OpenRouterProvider { private client: OpenAI; private _models: Model[]; private defaultModel: string; private timeouts: Required<NonNullable<ProviderConfig['timeouts']>>; readonly name = 'openrouter'; constructor(config: ProviderConfig) { if (!config.apiKey) { throw new Error('Missing openRouter.apiKey in YAML configuration'); } if (!config.defaultModel) { throw new Error('Missing defaultModel in YAML configuration'); } // Initialize OpenAI client with OpenRouter configuration this.client = new OpenAI({ apiKey: config.apiKey, baseURL: 'https://openrouter.ai/api/v1', defaultQuery: { use_cache: 'true' }, defaultHeaders: { 'HTTP-Referer': 'https://github.com/cursor-ai/mcp-conversation-server', 'X-Title': 'MCP Conversation Server', 'Content-Type': 'application/json', 'OR-SITE-LOCATION': 'https://github.com/cursor-ai/mcp-conversation-server', 'OR-ALLOW-FINE-TUNING': 'false' } }); this.timeouts = { completion: config.timeouts?.completion ?? 30000, stream: config.timeouts?.stream ?? 60000 }; this.defaultModel = config.defaultModel; // Convert configured models to internal format this._models = Object.entries(config.models).map(([id, modelConfig]) => ({ id, contextWindow: modelConfig.contextWindow, streaming: modelConfig.streaming, temperature: modelConfig.temperature, description: modelConfig.description, supportsFunctions: false })); } private getModelConfig(modelId: string): Model { const model = this._models.find(m => m.id === modelId); if (!model) { console.warn(`Model ${modelId} not found in configuration, falling back to default model ${this.defaultModel}`); const defaultModel = this._models.find(m => m.id === this.defaultModel); if (!defaultModel) { throw new Error('Default model not found in configuration'); } return defaultModel; } return model; } get models(): Model[] { return this._models; } async validateConfig(): Promise<void> { if (this._models.length === 0) { throw new Error('No models configured for OpenRouter provider'); } try { // Simple validation - just verify API connection works await this.client.chat.completions.create({ model: this._models[0].id, messages: [{ role: 'user', content: 'test' }], max_tokens: 1 // Minimum response size for validation }); } catch (error: unknown) { const message = error instanceof Error ? error.message : 'Unknown error'; throw new Error(`Failed to validate OpenRouter configuration: ${message}`); } } async createCompletion(params: CompletionParams): Promise<ProviderResponse> { try { // Get model configuration or fall back to default const modelConfig = this.getModelConfig(params.model); const response = await this.client.chat.completions.create({ model: modelConfig.id, messages: params.messages.map((msg: Message) => ({ role: msg.role, content: msg.content, name: msg.name })), temperature: params.temperature ?? modelConfig.temperature ?? 0.7, max_tokens: params.maxTokens, stream: false }); // Validate response structure if (!response || !response.choices || !Array.isArray(response.choices) || response.choices.length === 0) { throw new Error('Invalid or empty response from OpenRouter'); } const choice = response.choices[0]; if (!choice || !choice.message || typeof choice.message.content !== 'string') { throw new Error('Invalid message structure in OpenRouter response'); } return { content: choice.message.content, model: modelConfig.id, tokenCount: response.usage?.total_tokens, metadata: { provider: 'openrouter', modelName: modelConfig.id, ...response.usage && { usage: response.usage } } }; } catch (error: unknown) { if (error instanceof Error) { if (error.message.includes('timeout')) { throw new Error('OpenRouter request timed out. Please try again.'); } if (error.message.includes('rate_limit')) { throw new Error('OpenRouter rate limit exceeded. Please try again later.'); } if (error.message.includes('insufficient_quota')) { throw new Error('OpenRouter quota exceeded. Please check your credits.'); } throw new Error(`OpenRouter completion failed: ${error.message}`); } throw new Error('Unknown error occurred during OpenRouter completion'); } } async *streamCompletion(params: CompletionParams): AsyncIterableIterator<ProviderResponse> { try { // Get model configuration or fall back to default const modelConfig = this.getModelConfig(params.model); const stream = await this.client.chat.completions.create({ model: modelConfig.id, messages: params.messages.map((msg: Message) => ({ role: msg.role, content: msg.content, name: msg.name })), temperature: params.temperature ?? modelConfig.temperature ?? 0.7, max_tokens: params.maxTokens, stream: true }); for await (const chunk of stream) { // Validate chunk structure if (!chunk || !chunk.choices || !Array.isArray(chunk.choices) || chunk.choices.length === 0) { continue; } const delta = chunk.choices[0]?.delta; if (!delta || typeof delta.content !== 'string') { continue; } yield { content: delta.content, model: modelConfig.id, metadata: { provider: 'openrouter', modelName: modelConfig.id, isPartial: true } }; } } catch (error: unknown) { if (error instanceof Error) { if (error.message.includes('timeout')) { throw new Error('OpenRouter streaming request timed out. Please try again.'); } if (error.message.includes('rate_limit')) { throw new Error('OpenRouter rate limit exceeded. Please try again later.'); } if (error.message.includes('insufficient_quota')) { throw new Error('OpenRouter quota exceeded. Please check your credits.'); } throw new Error(`OpenRouter streaming completion failed: ${error.message}`); } throw new Error('Unknown error occurred during OpenRouter streaming'); } } /** * Get detailed information about all available models * @returns Array of model information including default status and pricing */ async listAvailableModels(): Promise<ModelInfo[]> { try { return this._models.map(model => { const [provider, modelName] = model.id.split('/'); return { ...model, provider: provider || 'unknown', isDefault: model.id === this.defaultModel, cost: undefined // Could be fetched from OpenRouter API if needed }; }).sort((a, b) => { // Sort with default model first, then by provider/name if (a.isDefault) return -1; if (b.isDefault) return 1; return a.id.localeCompare(b.id); }); } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; throw new Error(`Failed to list available models: ${message}`); } } /** * Get the current default model configuration * @returns The default model configuration */ getDefaultModel(): ModelInfo { const defaultModel = this._models.find(m => m.id === this.defaultModel); if (!defaultModel) { throw new Error('Default model not found in configuration'); } const [provider] = defaultModel.id.split('/'); return { ...defaultModel, isDefault: true, provider: provider || 'unknown', cost: undefined }; } } ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; import * as dotenv from 'dotenv'; import * as path from 'path'; import * as os from 'os'; import * as fs from 'fs/promises'; import { parse } from 'yaml'; import OpenAI from 'openai'; import { Message, Conversation, ConversationFilter } from './types/conversation.js'; import { ServerConfig } from './types/server.js'; import { OpenRouterError, FileSystemError } from './types/errors.js'; import { OpenRouterProvider } from './providers/openrouter.js'; // Load environment variables from .env file dotenv.config(); // Determine the appropriate app data directory based on OS function getAppDataPath(): string { switch (process.platform) { case 'win32': return process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'); case 'darwin': return path.join(os.homedir(), 'Library', 'Application Support'); default: return process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share'); } } // Create the app-specific data directory path const APP_NAME = 'mcp-conversation-server'; const defaultDataPath = path.join(getAppDataPath(), APP_NAME, 'conversations'); /** * MCP Conversation Server * * Workflow: * 1. Create a conversation: * - Use create-conversation tool * - Specify provider (e.g., 'deepseek') and model (e.g., 'deepseek-chat') * - Optionally provide a title * * 2. Send messages: * - Use send-message tool * - Provide conversationId from step 1 * - Set stream: true for real-time responses * - Messages maintain chat context automatically * * 3. Access conversation history: * - Use resources/read with conversation://{id}/history * - Full chat history with context is preserved * * Error Handling: * - All errors include detailed messages and proper error codes * - Automatic retries for transient failures * - Timeouts are configurable per operation */ // Schema definitions const ListResourcesSchema = z.object({ method: z.literal('resources/list') }); const ReadResourceSchema = z.object({ method: z.literal('resources/read'), params: z.object({ uri: z.string() }) }); const ListToolsSchema = z.object({ method: z.literal('tools/list') }); const CallToolSchema = z.object({ method: z.literal('tools/call'), params: z.object({ name: z.string(), arguments: z.record(z.unknown()) }) }); const ListPromptsSchema = z.object({ method: z.literal('prompts/list') }); const GetPromptSchema = z.object({ method: z.literal('prompts/get'), params: z.object({ name: z.string(), arguments: z.record(z.unknown()).optional() }) }); // Modify logging to use stderr for ALL non-JSON-RPC messages function logDebug(...args: any[]): void { console.error('[DEBUG]', ...args); } function logError(...args: any[]): void { console.error('[ERROR]', ...args); } // Create the MCP server instance const server = new McpServer({ name: 'conversation-server', version: '1.0.0' }); // Initialize server configuration const config: ServerConfig = { openRouter: { apiKey: process.env.OPENROUTER_API_KEY || '' }, models: {}, // Will be populated from YAML config defaultModel: '', // Will be populated from YAML config persistence: { type: 'filesystem', path: process.env.CONVERSATIONS_PATH || defaultDataPath }, resources: { maxSizeBytes: 10 * 1024 * 1024, // 10MB allowedTypes: ['.txt', '.md', '.json', '.csv', '.cs', '.ts', '.js', '.jsx', '.tsx', '.pdf'], chunkSize: 1024 // 1KB chunks } }; let openRouterProvider: OpenRouterProvider; // Load models configuration async function loadModelsConfig(): Promise<ServerConfig> { try { // Try to load from build directory first (for production) const buildConfigPath = path.join(path.dirname(process.argv[1]), 'config', 'models.yaml'); let fileContents: string; try { fileContents = await fs.readFile(buildConfigPath, 'utf8'); } catch (error) { // If not found in build directory, try source directory (for development) const sourceConfigPath = path.join(process.cwd(), 'config', 'models.yaml'); fileContents = await fs.readFile(sourceConfigPath, 'utf8'); } const config = parse(fileContents); // Validate required configuration if (!config.openRouter?.apiKey) { throw new Error('Missing openRouter.apiKey in models.yaml configuration'); } if (!config.models || Object.keys(config.models).length === 0) { throw new Error('No models configured in models.yaml configuration'); } if (!config.defaultModel) { throw new Error('Missing defaultModel in models.yaml configuration'); } // Set default persistence path if not specified if (!config.persistence?.path) { config.persistence = { path: defaultDataPath }; } return { openRouter: { apiKey: config.openRouter.apiKey }, models: config.models, defaultModel: config.defaultModel, persistence: { type: 'filesystem', path: config.persistence.path }, resources: { maxSizeBytes: 10 * 1024 * 1024, // 10MB allowedTypes: ['.txt', '.md', '.json', '.csv', '.cs', '.ts', '.js', '.jsx', '.tsx', '.pdf'], chunkSize: 1024 // 1KB chunks } }; } catch (error) { if (error instanceof Error) { throw new Error(`Failed to load models configuration: ${error.message}`); } throw new Error('Failed to load models configuration. Make sure models.yaml exists in the config directory.'); } } // Initialize and start the server async function startServer() { try { console.error('Starting MCP Conversation Server...'); // Load and validate the complete configuration from YAML const config = await loadModelsConfig(); console.error('Using data directory:', config.persistence.path); // Initialize OpenRouter provider with loaded config openRouterProvider = new OpenRouterProvider({ apiKey: config.openRouter.apiKey, models: config.models, defaultModel: config.defaultModel, timeouts: { completion: 30000, stream: 60000 } }); // Create data directory if it doesn't exist await fs.mkdir(config.persistence.path, { recursive: true }); // Validate OpenRouter connection using the provider await openRouterProvider.validateConfig(); // Set up tools after provider is initialized setupTools(); console.error('Successfully connected to OpenRouter'); console.error('Available models:', Object.keys(config.models).join(', ')); console.error('Default model:', config.defaultModel); // Set up server transport const transport = new StdioServerTransport(); await server.connect(transport); console.error('Server connected and ready'); } catch (error) { console.error('Failed to start server:', error); process.exit(1); } } // Setup server tools function setupTools() { // Add create-conversation tool server.tool( 'create-conversation', `Creates a new conversation with a specified model.`, { model: z.string().describe('The model ID to use for the conversation'), title: z.string().optional().describe('Optional title for the conversation') }, async (args: { model: string; title?: string }, _extra: any) => { const { model, title } = args; const now = new Date().toISOString(); const conversation: Conversation = { id: crypto.randomUUID(), model, title: title || `Conversation ${now}`, messages: [], created: now, updated: now }; try { const conversationPath = path.join(config.persistence.path, `${conversation.id}.json`); await fs.writeFile(conversationPath, JSON.stringify(conversation, null, 2)); return { content: [{ type: 'text', text: JSON.stringify(conversation, null, 2) }] }; } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; throw new FileSystemError(`Failed to save conversation: ${message}`); } } ); // Add send-message tool server.tool( 'send-message', `Sends a message to an existing conversation and receives a response.`, { conversationId: z.string(), content: z.string(), stream: z.boolean().optional() }, async (args: { conversationId: string; content: string; stream?: boolean }, _extra: any) => { const { conversationId, content, stream = false } = args; try { const conversationPath = path.join(config.persistence.path, `${conversationId}.json`); const conversation: Conversation = JSON.parse(await fs.readFile(conversationPath, 'utf8')); const userMessage: Message = { role: 'user', content, timestamp: new Date().toISOString() }; conversation.messages.push(userMessage); conversation.updated = new Date().toISOString(); try { if (stream) { const streamResponse = await openRouterProvider.streamCompletion({ model: conversation.model, messages: conversation.messages, stream: true }); await fs.writeFile(conversationPath, JSON.stringify(conversation, null, 2)); return { content: [{ type: 'resource', resource: { uri: `stream://${conversationId}`, text: 'Message stream started', mimeType: 'text/plain' } }] }; } else { const response = await openRouterProvider.createCompletion({ model: conversation.model, messages: conversation.messages, stream: false }); const assistantMessage: Message = { role: 'assistant', content: response.content, timestamp: new Date().toISOString() }; conversation.messages.push(assistantMessage); conversation.updated = new Date().toISOString(); await fs.writeFile(conversationPath, JSON.stringify(conversation, null, 2)); return { content: [{ type: 'text', text: JSON.stringify(assistantMessage, null, 2) }] }; } } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; throw new OpenRouterError(`OpenRouter request failed: ${message}`); } } catch (error) { if (error instanceof OpenRouterError) throw error; const message = error instanceof Error ? error.message : 'Unknown error'; throw new FileSystemError(`Failed to handle message: ${message}`); } } ); // Add list-models tool server.tool( 'list-models', `Lists all available models with their configurations and capabilities.`, {}, async (_args: {}, _extra: any) => { try { const models = await openRouterProvider.listAvailableModels(); return { content: [{ type: 'text', text: JSON.stringify({ models, defaultModel: openRouterProvider.getDefaultModel(), totalModels: models.length }, null, 2) }] }; } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; throw new Error(`Failed to list models: ${message}`); } } ); } // Start the server startServer(); ```