# 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: -------------------------------------------------------------------------------- ``` 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | ``` -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- ``` 1 | # OpenAI Configuration 2 | OPENAI_API_KEY=your_openai_key_here 3 | 4 | # DeepSeek Configuration 5 | DEEPSEEK_API_KEY=your_api_key_here 6 | 7 | # OpenRouter Configuration 8 | OPENROUTER_API_KEY=your-openrouter-api-key 9 | 10 | # Server Configuration 11 | DATA_DIR=./data/conversations 12 | LOG_LEVEL=info # debug, info, warn, error 13 | 14 | # Conversation storage path 15 | CONVERSATIONS_PATH=d:\\Projects\\Conversations ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | node_modules 132 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # MCP Conversation Server 2 | 3 | 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. 4 | 5 | ## Features 6 | 7 | - **MCP Protocol Support** 8 | - Full MCP protocol compliance 9 | - Resource management and discovery 10 | - Tool-based interaction model 11 | - Streaming response support 12 | - Error handling and recovery 13 | 14 | - **OpenRouter Integration** 15 | - Support for all OpenRouter models 16 | - Real-time streaming responses 17 | - Automatic token counting 18 | - Model context window management 19 | - Available models include: 20 | - Claude 3 Opus 21 | - Claude 3 Sonnet 22 | - Llama 2 70B 23 | - And many more from OpenRouter's catalog 24 | 25 | - **Conversation Management** 26 | - Create and manage multiple conversations 27 | - Support for system messages 28 | - Message history tracking 29 | - Token usage monitoring 30 | - Conversation filtering and search 31 | 32 | - **Streaming Support** 33 | - Real-time message streaming 34 | - Chunked response handling 35 | - Token counting 36 | 37 | - **File System Persistence** 38 | - Conversation state persistence 39 | - Configurable storage location 40 | - Automatic state management 41 | 42 | ## Installation 43 | 44 | ```bash 45 | npm install mcp-conversation-server 46 | ``` 47 | 48 | ## Configuration 49 | 50 | ### Configuration 51 | 52 | All configuration for the MCP Conversation Server is now provided via YAML. Please update the `config/models.yaml` file with your settings. For example: 53 | 54 | ```yaml 55 | # MCP Server Configuration 56 | openRouter: 57 | apiKey: "YOUR_OPENROUTER_API_KEY" # Replace with your actual OpenRouter API key. 58 | 59 | persistence: 60 | path: "./conversations" # Directory for storing conversation data. 61 | 62 | models: 63 | # Define your models here 64 | 'provider/model-name': 65 | id: 'provider/model-name' 66 | contextWindow: 123456 67 | streaming: true 68 | temperature: 0.7 69 | description: 'Model description' 70 | 71 | # Default model to use if none specified 72 | defaultModel: 'provider/model-name' 73 | ``` 74 | 75 | ### Server Configuration 76 | 77 | The MCP Conversation Server now loads all its configuration from the YAML file. In your application, you can load the configuration as follows: 78 | 79 | ```typescript 80 | const config = await loadModelsConfig(); // Loads openRouter, persistence, models, and defaultModel settings from 'config/models.yaml' 81 | ``` 82 | 83 | *Note: Environment variables are no longer required as all configuration is provided via the YAML file.* 84 | 85 | ## Usage 86 | 87 | ### Basic Server Setup 88 | 89 | ```typescript 90 | import { ConversationServer } from 'mcp-conversation-server'; 91 | 92 | const server = new ConversationServer(config); 93 | server.run().catch(console.error); 94 | ``` 95 | 96 | ### Available Tools 97 | 98 | The server exposes several MCP tools: 99 | 100 | 1. **create-conversation** 101 | 102 | ```typescript 103 | { 104 | provider: 'openrouter', // Provider is always 'openrouter' 105 | model: string, // OpenRouter model ID (e.g., 'anthropic/claude-3-opus-20240229') 106 | title?: string; // Optional conversation title 107 | } 108 | ``` 109 | 110 | 2. **send-message** 111 | 112 | ```typescript 113 | { 114 | conversationId: string; // Conversation ID 115 | content: string; // Message content 116 | stream?: boolean; // Enable streaming responses 117 | } 118 | ``` 119 | 120 | 3. **list-conversations** 121 | 122 | ```typescript 123 | { 124 | filter?: { 125 | model?: string; // Filter by model 126 | startDate?: string; // Filter by start date 127 | endDate?: string; // Filter by end date 128 | } 129 | } 130 | ``` 131 | 132 | ### Resources 133 | 134 | The server provides access to several resources: 135 | 136 | 1. **conversation://{id}** 137 | - Access specific conversation details 138 | - View message history 139 | - Check conversation metadata 140 | 141 | 2. **conversation://list** 142 | - List all active conversations 143 | - Filter conversations by criteria 144 | - Sort by recent activity 145 | 146 | ## Development 147 | 148 | ### Building 149 | 150 | ```bash 151 | npm run build 152 | ``` 153 | 154 | ### Running Tests 155 | 156 | ```bash 157 | npm test 158 | ``` 159 | 160 | ### Debugging 161 | 162 | The server provides several debugging features: 163 | 164 | 1. **Error Logging** 165 | - All errors are logged with stack traces 166 | - Token usage tracking 167 | - Rate limit monitoring 168 | 169 | 2. **MCP Inspector** 170 | 171 | ```bash 172 | npm run inspector 173 | ``` 174 | 175 | Use the MCP Inspector to: 176 | - Test tool execution 177 | - View resource contents 178 | - Monitor message flow 179 | - Validate protocol compliance 180 | 181 | 3. **Provider Validation** 182 | 183 | ```typescript 184 | await server.providerManager.validateProviders(); 185 | ``` 186 | 187 | Validates: 188 | - API key validity 189 | - Model availability 190 | - Rate limit status 191 | 192 | ### Troubleshooting 193 | 194 | Common issues and solutions: 195 | 196 | 1. **OpenRouter Connection Issues** 197 | - Verify your API key is valid 198 | - Check rate limits on [OpenRouter's dashboard](https://openrouter.ai/dashboard) 199 | - Ensure the model ID is correct 200 | - Monitor credit usage 201 | 202 | 2. **Message Streaming Errors** 203 | - Verify model streaming support 204 | - Check connection stability 205 | - Monitor token limits 206 | - Handle timeout settings 207 | 208 | 3. **File System Errors** 209 | - Check directory permissions 210 | - Verify path configuration 211 | - Monitor disk space 212 | - Handle concurrent access 213 | 214 | ## Contributing 215 | 216 | 1. Fork the repository 217 | 2. Create a feature branch 218 | 3. Commit your changes 219 | 4. Push to the branch 220 | 5. Create a Pull Request 221 | 222 | ## License 223 | 224 | ISC License 225 | ``` -------------------------------------------------------------------------------- /src/stores/Store.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Conversation } from '../types/conversation.js'; 2 | 3 | export interface Store { 4 | initialize(): Promise<void>; 5 | saveConversation(conversation: Conversation): Promise<void>; 6 | getConversation(id: string): Promise<Conversation | null>; 7 | listConversations(): Promise<Conversation[]>; 8 | } ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules"] 15 | } 16 | ``` -------------------------------------------------------------------------------- /src/types/conversation.ts: -------------------------------------------------------------------------------- ```typescript 1 | export interface Message { 2 | role: 'system' | 'user' | 'assistant'; 3 | content: string; 4 | timestamp: string; 5 | name?: string; 6 | } 7 | 8 | export interface Conversation { 9 | id: string; 10 | model: string; 11 | title: string; 12 | messages: Message[]; 13 | created: string; 14 | updated: string; 15 | } 16 | 17 | export interface ConversationFilter { 18 | model?: string; 19 | startDate?: string; 20 | endDate?: string; 21 | } ``` -------------------------------------------------------------------------------- /src/types/errors.ts: -------------------------------------------------------------------------------- ```typescript 1 | export class McpError extends Error { 2 | constructor(public code: string, message: string) { 3 | super(message); 4 | this.name = 'McpError'; 5 | } 6 | } 7 | 8 | export class ValidationError extends Error { 9 | constructor(message: string) { 10 | super(message); 11 | this.name = 'ValidationError'; 12 | } 13 | } 14 | 15 | export class OpenRouterError extends Error { 16 | constructor(message: string) { 17 | super(message); 18 | this.name = 'OpenRouterError'; 19 | } 20 | } 21 | 22 | export class FileSystemError extends Error { 23 | constructor(message: string) { 24 | super(message); 25 | this.name = 'FileSystemError'; 26 | } 27 | } ``` -------------------------------------------------------------------------------- /config/models.yaml: -------------------------------------------------------------------------------- ```yaml 1 | # OpenRouter Models Configuration 2 | # Visit https://openrouter.ai/docs#models for the complete list of available models 3 | 4 | # MCP Server Configuration 5 | openRouter: 6 | apiKey: "<<OPEN ROUTER>>" # Replace with your actual OpenRouter API key. 7 | 8 | persistence: 9 | path: "d:/projects/conversations" # Optional: Directory for storing conversation data. 10 | 11 | models: 12 | 13 | 'google/gemini-2.0-pro-exp-02-05:free': 14 | id: 'google/gemini-2.0-pro-exp-02-05:free' 15 | contextWindow: 2000000 16 | streaming: true 17 | temperature: 0.2 18 | description: 'Google Gemini 2.0 Pro is a powerful and versatile language model that can handle a wide range of tasks.' 19 | 20 | 21 | 'google/gemini-2.0-flash-001': 22 | id: 'google/gemini-2.0-flash-001' 23 | contextWindow: 1000000 24 | streaming: true 25 | temperature: 0.2 26 | description: 'Google Gemini 2.0 Flash is a powerful and versatile language model that can handle a wide range of tasks.' 27 | 28 | 29 | # Add more models as needed following the same format 30 | # Example: 31 | # 'provider/model-name': 32 | # id: 'provider/model-name' 33 | # contextWindow: <window_size> 34 | # streaming: true/false 35 | # description: 'Model description' 36 | 37 | # Default model to use if none specified 38 | defaultModel: 'google/gemini-2.0-pro-exp-02-05:free' ``` -------------------------------------------------------------------------------- /src/types/server.ts: -------------------------------------------------------------------------------- ```typescript 1 | export interface ResourceConfig { 2 | maxSizeBytes: number; 3 | allowedTypes: string[]; 4 | chunkSize: number; 5 | } 6 | 7 | export interface ServerConfig { 8 | openRouter: { 9 | apiKey: string; 10 | }; 11 | models: { 12 | [key: string]: { 13 | id: string; 14 | contextWindow: number; 15 | streaming: boolean; 16 | description?: string; 17 | }; 18 | }; 19 | defaultModel: string; 20 | persistence: { 21 | type: 'filesystem'; 22 | path: string; 23 | }; 24 | resources: { 25 | maxSizeBytes: number; 26 | allowedTypes: string[]; 27 | chunkSize: number; 28 | }; 29 | } 30 | 31 | export interface ModelConfig { 32 | contextWindow: number; 33 | streaming: boolean; 34 | } 35 | 36 | export interface PersistenceConfig { 37 | type: 'filesystem' | 'memory'; 38 | path?: string; 39 | } 40 | 41 | export interface CreateConversationParams { 42 | provider?: string; 43 | model?: string; 44 | title?: string; 45 | } 46 | 47 | export interface Conversation { 48 | id: string; 49 | provider: string; 50 | model: string; 51 | title: string; 52 | messages: Message[]; 53 | createdAt: number; 54 | updatedAt: number; 55 | } 56 | 57 | export interface Message { 58 | role: 'user' | 'assistant'; 59 | content: string; 60 | timestamp: number; 61 | context?: { 62 | documents?: string[]; 63 | code?: string[]; 64 | }; 65 | } 66 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "mcp-conversation-server", 3 | "version": "0.1.0", 4 | "description": "A Model Context Protocol server used to execute various applicatoin types.", 5 | "private": true, 6 | "type": "module", 7 | "bin": { 8 | "mcp-conversation-server": "./build/index.js" 9 | }, 10 | "files": [ 11 | "build" 12 | ], 13 | "scripts": { 14 | "prebuild": "rimraf build", 15 | "build": "tsc && npm run copy-config", 16 | "copy-config": "copyfiles config/**/* build/", 17 | "start": "node build/index.js", 18 | "dev": "ts-node-esm src/index.ts", 19 | "test": "jest" 20 | }, 21 | "keywords": [], 22 | "author": "", 23 | "license": "ISC", 24 | "dependencies": { 25 | "@modelcontextprotocol/sdk": "^1.0.0", 26 | "@types/dotenv": "^8.2.3", 27 | "@types/express": "^4.17.21", 28 | "@types/uuid": "^9.0.7", 29 | "dotenv": "^16.4.7", 30 | "express": "^4.18.2", 31 | "openai": "^4.83.0", 32 | "uuid": "^9.0.1", 33 | "yaml": "^2.7.0" 34 | }, 35 | "devDependencies": { 36 | "@types/jest": "^29.5.14", 37 | "@types/node": "^20.11.24", 38 | "@typescript-eslint/eslint-plugin": "^7.0.0", 39 | "@typescript-eslint/parser": "^7.0.0", 40 | "copyfiles": "^2.4.1", 41 | "eslint": "^8.56.0", 42 | "jest": "^29.7.0", 43 | "prettier": "^3.4.2", 44 | "rimraf": "^5.0.10", 45 | "ts-jest": "^29.2.5", 46 | "ts-node-dev": "^2.0.0", 47 | "typescript": "^5.3.3" 48 | } 49 | } 50 | ``` -------------------------------------------------------------------------------- /config/system_instructions.yaml: -------------------------------------------------------------------------------- ```yaml 1 | # System Instructions Configuration 2 | # Define default and model-specific system instructions 3 | 4 | default: | 5 | You are a helpful AI assistant focused on providing accurate and concise responses. 6 | Please follow these guidelines: 7 | - Be direct and to the point 8 | - Show code examples when relevant 9 | - Explain complex concepts clearly 10 | - Ask for clarification when needed 11 | 12 | models: 13 | # DeepSeek Models 14 | 'deepseek/deepseek-chat': | 15 | You are DeepSeek Chat, a helpful AI assistant with strong coding and technical capabilities. 16 | Guidelines: 17 | - Focus on practical, implementable solutions 18 | - Provide code examples with explanations 19 | - Use clear technical explanations 20 | - Follow best practices in software development 21 | - Ask for clarification on ambiguous requirements 22 | 23 | 'deepseek/deepseek-r1': | 24 | You are DeepSeek Reasoner, an AI focused on step-by-step problem solving and logical reasoning. 25 | Guidelines: 26 | - Break down complex problems into steps 27 | - Show your reasoning process clearly 28 | - Validate assumptions 29 | - Consider edge cases 30 | - Provide concrete examples 31 | 32 | # Claude Models 33 | 'anthropic/claude-3-opus-20240229': | 34 | You are Claude 3 Opus, a highly capable AI assistant with strong analytical and creative abilities. 35 | Guidelines: 36 | - Provide comprehensive, well-reasoned responses 37 | - Balance depth with clarity 38 | - Use examples to illustrate complex points 39 | - Consider multiple perspectives 40 | - Maintain high standards of accuracy 41 | 42 | 'anthropic/claude-3-sonnet-20240229': | 43 | You are Claude 3 Sonnet, focused on efficient and practical problem-solving. 44 | Guidelines: 45 | - Provide concise, actionable responses 46 | - Focus on practical solutions 47 | - Use clear examples 48 | - Be direct and efficient 49 | - Ask for clarification when needed 50 | 51 | # Llama Models 52 | 'meta-llama/llama-2-70b-chat': | 53 | You are Llama 2, an open-source AI assistant focused on helpful and accurate responses. 54 | Guidelines: 55 | - Provide clear, straightforward answers 56 | - Use examples when helpful 57 | - Stay within known capabilities 58 | - Be direct about limitations 59 | - Focus on practical solutions ``` -------------------------------------------------------------------------------- /config.example.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ServerConfig } from './src/types/server.js'; 2 | import * as path from 'path'; 3 | 4 | /** 5 | * Example configuration for the MCP Conversation Server 6 | * 7 | * This configuration includes examples for all supported providers: 8 | * - OpenAI 9 | * - DeepSeek 10 | * - OpenRouter 11 | * 12 | * Storage paths can be configured in several ways: 13 | * 1. Use environment variable: CONVERSATIONS_PATH 14 | * 2. Set absolute path in config 15 | * 3. Set relative path (relative to project root) 16 | * 4. Let it default to OS-specific app data directory 17 | */ 18 | 19 | const config: ServerConfig = { 20 | providers: { 21 | deepseek: { 22 | endpoint: 'https://api.deepseek.com/v1', 23 | apiKey: process.env.DEEPSEEK_API_KEY || '', 24 | models: { 25 | 'deepseek-chat': { 26 | id: 'deepseek-chat', 27 | contextWindow: 32768, 28 | streaming: true 29 | }, 30 | 'deepseek-reasoner': { 31 | id: 'deepseek-reasoner', 32 | contextWindow: 64000, 33 | streaming: true 34 | } 35 | }, 36 | timeouts: { 37 | completion: 300000, // 5 minutes for non-streaming 38 | stream: 120000 // 2 minutes per stream chunk 39 | } 40 | }, 41 | openai: { 42 | endpoint: 'https://api.openai.com/v1', 43 | apiKey: process.env.OPENAI_API_KEY || '', 44 | models: { 45 | 'gpt-4': { 46 | id: 'gpt-4', 47 | contextWindow: 8192, 48 | streaming: true 49 | }, 50 | 'gpt-3.5-turbo': { 51 | id: 'gpt-3.5-turbo', 52 | contextWindow: 4096, 53 | streaming: true 54 | } 55 | }, 56 | timeouts: { 57 | completion: 300000, // 5 minutes for non-streaming 58 | stream: 60000 // 1 minute per stream chunk 59 | } 60 | } 61 | }, 62 | defaultProvider: 'deepseek', 63 | defaultModel: 'deepseek-chat', 64 | persistence: { 65 | type: 'filesystem' as const, 66 | // Use environment variable or default to d:\Projects\Conversations 67 | path: process.env.CONVERSATIONS_PATH || path.normalize('d:\\Projects\\Conversations') 68 | }, 69 | resources: { 70 | maxSizeBytes: 10 * 1024 * 1024, // 10MB 71 | allowedTypes: ['.txt', '.md', '.json', '.csv'], 72 | chunkSize: 1024 // 1KB chunks 73 | } 74 | }; 75 | 76 | export default config; 77 | 78 | /** 79 | * Example usage: 80 | * 81 | * ```typescript 82 | * import { ConversationServer } from './src/index.js'; 83 | * import { config } from './config.js'; 84 | * 85 | * // Override storage path if needed 86 | * config.persistence.path = '/custom/path/to/conversations'; 87 | * 88 | * const server = new ConversationServer(config); 89 | * server.initialize().then(() => { 90 | * console.log('Server initialized, connecting...'); 91 | * server.connect().catch(err => console.error('Failed to connect:', err)); 92 | * }).catch(err => console.error('Failed to initialize:', err)); 93 | * ``` 94 | */ ``` -------------------------------------------------------------------------------- /src/stores/FileSystemStore.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as fs from 'fs/promises'; 2 | import * as path from 'path'; 3 | import { Conversation } from '../types/conversation.js'; 4 | import { Store } from './Store.js'; 5 | 6 | interface FSError extends Error { 7 | code?: string; 8 | message: string; 9 | } 10 | 11 | function isFSError(error: unknown): error is FSError { 12 | return error instanceof Error && ('code' in error || 'message' in error); 13 | } 14 | 15 | export class FileSystemStore implements Store { 16 | private dataPath: string; 17 | private initialized: boolean = false; 18 | 19 | constructor(dataPath: string) { 20 | this.dataPath = dataPath; 21 | } 22 | 23 | async initialize(): Promise<void> { 24 | if (this.initialized) { 25 | return; 26 | } 27 | 28 | try { 29 | await fs.mkdir(this.dataPath, { recursive: true }); 30 | this.initialized = true; 31 | } catch (error) { 32 | throw new Error(`Failed to initialize store: ${error instanceof Error ? error.message : String(error)}`); 33 | } 34 | } 35 | 36 | private getConversationPath(id: string): string { 37 | return path.join(this.dataPath, `${id}.json`); 38 | } 39 | 40 | async saveConversation(conversation: Conversation): Promise<void> { 41 | const filePath = this.getConversationPath(conversation.id); 42 | try { 43 | await fs.writeFile(filePath, JSON.stringify(conversation, null, 2)); 44 | } catch (error) { 45 | throw new Error(`Failed to save conversation: ${error instanceof Error ? error.message : String(error)}`); 46 | } 47 | } 48 | 49 | async getConversation(id: string): Promise<Conversation | null> { 50 | const filePath = this.getConversationPath(id); 51 | try { 52 | const data = await fs.readFile(filePath, 'utf-8'); 53 | return JSON.parse(data) as Conversation; 54 | } catch (error) { 55 | if ((error as NodeJS.ErrnoException).code === 'ENOENT') { 56 | return null; 57 | } 58 | throw new Error(`Failed to read conversation: ${error instanceof Error ? error.message : String(error)}`); 59 | } 60 | } 61 | 62 | async listConversations(): Promise<Conversation[]> { 63 | try { 64 | const files = await fs.readdir(this.dataPath); 65 | const conversations: Conversation[] = []; 66 | 67 | for (const file of files) { 68 | if (path.extname(file) === '.json') { 69 | try { 70 | const data = await fs.readFile(path.join(this.dataPath, file), 'utf-8'); 71 | conversations.push(JSON.parse(data) as Conversation); 72 | } catch (error) { 73 | console.error(`Failed to read conversation file ${file}:`, error); 74 | } 75 | } 76 | } 77 | 78 | return conversations; 79 | } catch (error) { 80 | if ((error as NodeJS.ErrnoException).code === 'ENOENT') { 81 | return []; 82 | } 83 | throw new Error(`Failed to list conversations: ${error instanceof Error ? error.message : String(error)}`); 84 | } 85 | } 86 | 87 | async deleteConversation(id: string): Promise<void> { 88 | const filePath = this.getConversationPath(id); 89 | try { 90 | await fs.unlink(filePath); 91 | } catch (error) { 92 | if (isFSError(error)) { 93 | if (error.code !== 'ENOENT') { 94 | throw new Error(`Failed to delete conversation ${id}: ${error.message}`); 95 | } 96 | } else { 97 | throw new Error(`Failed to delete conversation ${id}: Unknown error`); 98 | } 99 | } 100 | } 101 | } 102 | ``` -------------------------------------------------------------------------------- /src/providers/openrouter.ts: -------------------------------------------------------------------------------- ```typescript 1 | import OpenAI from 'openai'; 2 | import { Message } from '../types/conversation.js'; 3 | 4 | interface Model { 5 | id: string; 6 | contextWindow: number; 7 | streaming: boolean; 8 | supportsFunctions?: boolean; 9 | temperature?: number; 10 | description?: string; 11 | } 12 | 13 | interface ProviderConfig { 14 | apiKey: string; 15 | models: { 16 | [key: string]: { 17 | id: string; 18 | contextWindow: number; 19 | streaming: boolean; 20 | temperature?: number; 21 | description?: string; 22 | }; 23 | }; 24 | defaultModel: string; 25 | timeouts?: { 26 | completion?: number; 27 | stream?: number; 28 | }; 29 | } 30 | 31 | interface ProviderResponse { 32 | content: string; 33 | model: string; 34 | tokenCount?: number; 35 | metadata?: Record<string, unknown>; 36 | } 37 | 38 | interface CompletionParams { 39 | messages: Message[]; 40 | model: string; 41 | stream: boolean; 42 | timeout?: number; 43 | temperature?: number; 44 | maxTokens?: number; 45 | } 46 | 47 | interface ModelInfo extends Model { 48 | isDefault: boolean; 49 | provider: string; 50 | cost?: { 51 | prompt: number; 52 | completion: number; 53 | }; 54 | } 55 | 56 | export class OpenRouterProvider { 57 | private client: OpenAI; 58 | private _models: Model[]; 59 | private defaultModel: string; 60 | private timeouts: Required<NonNullable<ProviderConfig['timeouts']>>; 61 | readonly name = 'openrouter'; 62 | 63 | constructor(config: ProviderConfig) { 64 | if (!config.apiKey) { 65 | throw new Error('Missing openRouter.apiKey in YAML configuration'); 66 | } 67 | 68 | if (!config.defaultModel) { 69 | throw new Error('Missing defaultModel in YAML configuration'); 70 | } 71 | 72 | // Initialize OpenAI client with OpenRouter configuration 73 | this.client = new OpenAI({ 74 | apiKey: config.apiKey, 75 | baseURL: 'https://openrouter.ai/api/v1', 76 | defaultQuery: { use_cache: 'true' }, 77 | defaultHeaders: { 78 | 'HTTP-Referer': 'https://github.com/cursor-ai/mcp-conversation-server', 79 | 'X-Title': 'MCP Conversation Server', 80 | 'Content-Type': 'application/json', 81 | 'OR-SITE-LOCATION': 'https://github.com/cursor-ai/mcp-conversation-server', 82 | 'OR-ALLOW-FINE-TUNING': 'false' 83 | } 84 | }); 85 | 86 | this.timeouts = { 87 | completion: config.timeouts?.completion ?? 30000, 88 | stream: config.timeouts?.stream ?? 60000 89 | }; 90 | 91 | this.defaultModel = config.defaultModel; 92 | 93 | // Convert configured models to internal format 94 | this._models = Object.entries(config.models).map(([id, modelConfig]) => ({ 95 | id, 96 | contextWindow: modelConfig.contextWindow, 97 | streaming: modelConfig.streaming, 98 | temperature: modelConfig.temperature, 99 | description: modelConfig.description, 100 | supportsFunctions: false 101 | })); 102 | } 103 | 104 | private getModelConfig(modelId: string): Model { 105 | const model = this._models.find(m => m.id === modelId); 106 | if (!model) { 107 | console.warn(`Model ${modelId} not found in configuration, falling back to default model ${this.defaultModel}`); 108 | const defaultModel = this._models.find(m => m.id === this.defaultModel); 109 | if (!defaultModel) { 110 | throw new Error('Default model not found in configuration'); 111 | } 112 | return defaultModel; 113 | } 114 | return model; 115 | } 116 | 117 | get models(): Model[] { 118 | return this._models; 119 | } 120 | 121 | async validateConfig(): Promise<void> { 122 | if (this._models.length === 0) { 123 | throw new Error('No models configured for OpenRouter provider'); 124 | } 125 | 126 | try { 127 | // Simple validation - just verify API connection works 128 | await this.client.chat.completions.create({ 129 | model: this._models[0].id, 130 | messages: [{ role: 'user', content: 'test' }], 131 | max_tokens: 1 // Minimum response size for validation 132 | }); 133 | } catch (error: unknown) { 134 | const message = error instanceof Error ? error.message : 'Unknown error'; 135 | throw new Error(`Failed to validate OpenRouter configuration: ${message}`); 136 | } 137 | } 138 | 139 | async createCompletion(params: CompletionParams): Promise<ProviderResponse> { 140 | try { 141 | // Get model configuration or fall back to default 142 | const modelConfig = this.getModelConfig(params.model); 143 | 144 | const response = await this.client.chat.completions.create({ 145 | model: modelConfig.id, 146 | messages: params.messages.map((msg: Message) => ({ 147 | role: msg.role, 148 | content: msg.content, 149 | name: msg.name 150 | })), 151 | temperature: params.temperature ?? modelConfig.temperature ?? 0.7, 152 | max_tokens: params.maxTokens, 153 | stream: false 154 | }); 155 | 156 | // Validate response structure 157 | if (!response || !response.choices || !Array.isArray(response.choices) || response.choices.length === 0) { 158 | throw new Error('Invalid or empty response from OpenRouter'); 159 | } 160 | 161 | const choice = response.choices[0]; 162 | if (!choice || !choice.message || typeof choice.message.content !== 'string') { 163 | throw new Error('Invalid message structure in OpenRouter response'); 164 | } 165 | 166 | return { 167 | content: choice.message.content, 168 | model: modelConfig.id, 169 | tokenCount: response.usage?.total_tokens, 170 | metadata: { 171 | provider: 'openrouter', 172 | modelName: modelConfig.id, 173 | ...response.usage && { usage: response.usage } 174 | } 175 | }; 176 | } catch (error: unknown) { 177 | if (error instanceof Error) { 178 | if (error.message.includes('timeout')) { 179 | throw new Error('OpenRouter request timed out. Please try again.'); 180 | } 181 | if (error.message.includes('rate_limit')) { 182 | throw new Error('OpenRouter rate limit exceeded. Please try again later.'); 183 | } 184 | if (error.message.includes('insufficient_quota')) { 185 | throw new Error('OpenRouter quota exceeded. Please check your credits.'); 186 | } 187 | throw new Error(`OpenRouter completion failed: ${error.message}`); 188 | } 189 | throw new Error('Unknown error occurred during OpenRouter completion'); 190 | } 191 | } 192 | 193 | async *streamCompletion(params: CompletionParams): AsyncIterableIterator<ProviderResponse> { 194 | try { 195 | // Get model configuration or fall back to default 196 | const modelConfig = this.getModelConfig(params.model); 197 | 198 | const stream = await this.client.chat.completions.create({ 199 | model: modelConfig.id, 200 | messages: params.messages.map((msg: Message) => ({ 201 | role: msg.role, 202 | content: msg.content, 203 | name: msg.name 204 | })), 205 | temperature: params.temperature ?? modelConfig.temperature ?? 0.7, 206 | max_tokens: params.maxTokens, 207 | stream: true 208 | }); 209 | 210 | for await (const chunk of stream) { 211 | // Validate chunk structure 212 | if (!chunk || !chunk.choices || !Array.isArray(chunk.choices) || chunk.choices.length === 0) { 213 | continue; 214 | } 215 | 216 | const delta = chunk.choices[0]?.delta; 217 | if (!delta || typeof delta.content !== 'string') { 218 | continue; 219 | } 220 | 221 | yield { 222 | content: delta.content, 223 | model: modelConfig.id, 224 | metadata: { 225 | provider: 'openrouter', 226 | modelName: modelConfig.id, 227 | isPartial: true 228 | } 229 | }; 230 | } 231 | } catch (error: unknown) { 232 | if (error instanceof Error) { 233 | if (error.message.includes('timeout')) { 234 | throw new Error('OpenRouter streaming request timed out. Please try again.'); 235 | } 236 | if (error.message.includes('rate_limit')) { 237 | throw new Error('OpenRouter rate limit exceeded. Please try again later.'); 238 | } 239 | if (error.message.includes('insufficient_quota')) { 240 | throw new Error('OpenRouter quota exceeded. Please check your credits.'); 241 | } 242 | throw new Error(`OpenRouter streaming completion failed: ${error.message}`); 243 | } 244 | throw new Error('Unknown error occurred during OpenRouter streaming'); 245 | } 246 | } 247 | 248 | /** 249 | * Get detailed information about all available models 250 | * @returns Array of model information including default status and pricing 251 | */ 252 | async listAvailableModels(): Promise<ModelInfo[]> { 253 | try { 254 | return this._models.map(model => { 255 | const [provider, modelName] = model.id.split('/'); 256 | return { 257 | ...model, 258 | provider: provider || 'unknown', 259 | isDefault: model.id === this.defaultModel, 260 | cost: undefined // Could be fetched from OpenRouter API if needed 261 | }; 262 | }).sort((a, b) => { 263 | // Sort with default model first, then by provider/name 264 | if (a.isDefault) return -1; 265 | if (b.isDefault) return 1; 266 | return a.id.localeCompare(b.id); 267 | }); 268 | } catch (error) { 269 | const message = error instanceof Error ? error.message : 'Unknown error'; 270 | throw new Error(`Failed to list available models: ${message}`); 271 | } 272 | } 273 | 274 | /** 275 | * Get the current default model configuration 276 | * @returns The default model configuration 277 | */ 278 | getDefaultModel(): ModelInfo { 279 | const defaultModel = this._models.find(m => m.id === this.defaultModel); 280 | if (!defaultModel) { 281 | throw new Error('Default model not found in configuration'); 282 | } 283 | const [provider] = defaultModel.id.split('/'); 284 | return { 285 | ...defaultModel, 286 | isDefault: true, 287 | provider: provider || 'unknown', 288 | cost: undefined 289 | }; 290 | } 291 | } ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 3 | import { z } from 'zod'; 4 | import * as dotenv from 'dotenv'; 5 | import * as path from 'path'; 6 | import * as os from 'os'; 7 | import * as fs from 'fs/promises'; 8 | import { parse } from 'yaml'; 9 | import OpenAI from 'openai'; 10 | import { Message, Conversation, ConversationFilter } from './types/conversation.js'; 11 | import { ServerConfig } from './types/server.js'; 12 | import { OpenRouterError, FileSystemError } from './types/errors.js'; 13 | import { OpenRouterProvider } from './providers/openrouter.js'; 14 | 15 | // Load environment variables from .env file 16 | dotenv.config(); 17 | 18 | // Determine the appropriate app data directory based on OS 19 | function getAppDataPath(): string { 20 | switch (process.platform) { 21 | case 'win32': 22 | return process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'); 23 | case 'darwin': 24 | return path.join(os.homedir(), 'Library', 'Application Support'); 25 | default: 26 | return process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share'); 27 | } 28 | } 29 | 30 | // Create the app-specific data directory path 31 | const APP_NAME = 'mcp-conversation-server'; 32 | const defaultDataPath = path.join(getAppDataPath(), APP_NAME, 'conversations'); 33 | 34 | /** 35 | * MCP Conversation Server 36 | * 37 | * Workflow: 38 | * 1. Create a conversation: 39 | * - Use create-conversation tool 40 | * - Specify provider (e.g., 'deepseek') and model (e.g., 'deepseek-chat') 41 | * - Optionally provide a title 42 | * 43 | * 2. Send messages: 44 | * - Use send-message tool 45 | * - Provide conversationId from step 1 46 | * - Set stream: true for real-time responses 47 | * - Messages maintain chat context automatically 48 | * 49 | * 3. Access conversation history: 50 | * - Use resources/read with conversation://{id}/history 51 | * - Full chat history with context is preserved 52 | * 53 | * Error Handling: 54 | * - All errors include detailed messages and proper error codes 55 | * - Automatic retries for transient failures 56 | * - Timeouts are configurable per operation 57 | */ 58 | 59 | // Schema definitions 60 | const ListResourcesSchema = z.object({ 61 | method: z.literal('resources/list') 62 | }); 63 | 64 | const ReadResourceSchema = z.object({ 65 | method: z.literal('resources/read'), 66 | params: z.object({ 67 | uri: z.string() 68 | }) 69 | }); 70 | 71 | const ListToolsSchema = z.object({ 72 | method: z.literal('tools/list') 73 | }); 74 | 75 | const CallToolSchema = z.object({ 76 | method: z.literal('tools/call'), 77 | params: z.object({ 78 | name: z.string(), 79 | arguments: z.record(z.unknown()) 80 | }) 81 | }); 82 | 83 | const ListPromptsSchema = z.object({ 84 | method: z.literal('prompts/list') 85 | }); 86 | 87 | const GetPromptSchema = z.object({ 88 | method: z.literal('prompts/get'), 89 | params: z.object({ 90 | name: z.string(), 91 | arguments: z.record(z.unknown()).optional() 92 | }) 93 | }); 94 | 95 | // Modify logging to use stderr for ALL non-JSON-RPC messages 96 | function logDebug(...args: any[]): void { 97 | console.error('[DEBUG]', ...args); 98 | } 99 | 100 | function logError(...args: any[]): void { 101 | console.error('[ERROR]', ...args); 102 | } 103 | 104 | // Create the MCP server instance 105 | const server = new McpServer({ 106 | name: 'conversation-server', 107 | version: '1.0.0' 108 | }); 109 | 110 | // Initialize server configuration 111 | const config: ServerConfig = { 112 | openRouter: { 113 | apiKey: process.env.OPENROUTER_API_KEY || '' 114 | }, 115 | models: {}, // Will be populated from YAML config 116 | defaultModel: '', // Will be populated from YAML config 117 | persistence: { 118 | type: 'filesystem', 119 | path: process.env.CONVERSATIONS_PATH || defaultDataPath 120 | }, 121 | resources: { 122 | maxSizeBytes: 10 * 1024 * 1024, // 10MB 123 | allowedTypes: ['.txt', '.md', '.json', '.csv', '.cs', '.ts', '.js', '.jsx', '.tsx', '.pdf'], 124 | chunkSize: 1024 // 1KB chunks 125 | } 126 | }; 127 | 128 | let openRouterProvider: OpenRouterProvider; 129 | 130 | // Load models configuration 131 | async function loadModelsConfig(): Promise<ServerConfig> { 132 | try { 133 | // Try to load from build directory first (for production) 134 | const buildConfigPath = path.join(path.dirname(process.argv[1]), 'config', 'models.yaml'); 135 | let fileContents: string; 136 | 137 | try { 138 | fileContents = await fs.readFile(buildConfigPath, 'utf8'); 139 | } catch (error) { 140 | // If not found in build directory, try source directory (for development) 141 | const sourceConfigPath = path.join(process.cwd(), 'config', 'models.yaml'); 142 | fileContents = await fs.readFile(sourceConfigPath, 'utf8'); 143 | } 144 | 145 | const config = parse(fileContents); 146 | 147 | // Validate required configuration 148 | if (!config.openRouter?.apiKey) { 149 | throw new Error('Missing openRouter.apiKey in models.yaml configuration'); 150 | } 151 | 152 | if (!config.models || Object.keys(config.models).length === 0) { 153 | throw new Error('No models configured in models.yaml configuration'); 154 | } 155 | 156 | if (!config.defaultModel) { 157 | throw new Error('Missing defaultModel in models.yaml configuration'); 158 | } 159 | 160 | // Set default persistence path if not specified 161 | if (!config.persistence?.path) { 162 | config.persistence = { 163 | path: defaultDataPath 164 | }; 165 | } 166 | 167 | return { 168 | openRouter: { 169 | apiKey: config.openRouter.apiKey 170 | }, 171 | models: config.models, 172 | defaultModel: config.defaultModel, 173 | persistence: { 174 | type: 'filesystem', 175 | path: config.persistence.path 176 | }, 177 | resources: { 178 | maxSizeBytes: 10 * 1024 * 1024, // 10MB 179 | allowedTypes: ['.txt', '.md', '.json', '.csv', '.cs', '.ts', '.js', '.jsx', '.tsx', '.pdf'], 180 | chunkSize: 1024 // 1KB chunks 181 | } 182 | }; 183 | } catch (error) { 184 | if (error instanceof Error) { 185 | throw new Error(`Failed to load models configuration: ${error.message}`); 186 | } 187 | throw new Error('Failed to load models configuration. Make sure models.yaml exists in the config directory.'); 188 | } 189 | } 190 | 191 | // Initialize and start the server 192 | async function startServer() { 193 | try { 194 | console.error('Starting MCP Conversation Server...'); 195 | 196 | // Load and validate the complete configuration from YAML 197 | const config = await loadModelsConfig(); 198 | 199 | console.error('Using data directory:', config.persistence.path); 200 | 201 | // Initialize OpenRouter provider with loaded config 202 | openRouterProvider = new OpenRouterProvider({ 203 | apiKey: config.openRouter.apiKey, 204 | models: config.models, 205 | defaultModel: config.defaultModel, 206 | timeouts: { 207 | completion: 30000, 208 | stream: 60000 209 | } 210 | }); 211 | 212 | // Create data directory if it doesn't exist 213 | await fs.mkdir(config.persistence.path, { recursive: true }); 214 | 215 | // Validate OpenRouter connection using the provider 216 | await openRouterProvider.validateConfig(); 217 | 218 | // Set up tools after provider is initialized 219 | setupTools(); 220 | 221 | console.error('Successfully connected to OpenRouter'); 222 | console.error('Available models:', Object.keys(config.models).join(', ')); 223 | console.error('Default model:', config.defaultModel); 224 | 225 | // Set up server transport 226 | const transport = new StdioServerTransport(); 227 | await server.connect(transport); 228 | 229 | console.error('Server connected and ready'); 230 | } catch (error) { 231 | console.error('Failed to start server:', error); 232 | process.exit(1); 233 | } 234 | } 235 | 236 | // Setup server tools 237 | function setupTools() { 238 | // Add create-conversation tool 239 | server.tool( 240 | 'create-conversation', 241 | `Creates a new conversation with a specified model.`, 242 | { 243 | model: z.string().describe('The model ID to use for the conversation'), 244 | title: z.string().optional().describe('Optional title for the conversation') 245 | }, 246 | async (args: { model: string; title?: string }, _extra: any) => { 247 | const { model, title } = args; 248 | const now = new Date().toISOString(); 249 | const conversation: Conversation = { 250 | id: crypto.randomUUID(), 251 | model, 252 | title: title || `Conversation ${now}`, 253 | messages: [], 254 | created: now, 255 | updated: now 256 | }; 257 | 258 | try { 259 | const conversationPath = path.join(config.persistence.path, `${conversation.id}.json`); 260 | await fs.writeFile(conversationPath, JSON.stringify(conversation, null, 2)); 261 | return { 262 | content: [{ 263 | type: 'text', 264 | text: JSON.stringify(conversation, null, 2) 265 | }] 266 | }; 267 | } catch (error) { 268 | const message = error instanceof Error ? error.message : 'Unknown error'; 269 | throw new FileSystemError(`Failed to save conversation: ${message}`); 270 | } 271 | } 272 | ); 273 | 274 | // Add send-message tool 275 | server.tool( 276 | 'send-message', 277 | `Sends a message to an existing conversation and receives a response.`, 278 | { 279 | conversationId: z.string(), 280 | content: z.string(), 281 | stream: z.boolean().optional() 282 | }, 283 | async (args: { conversationId: string; content: string; stream?: boolean }, _extra: any) => { 284 | const { conversationId, content, stream = false } = args; 285 | 286 | try { 287 | const conversationPath = path.join(config.persistence.path, `${conversationId}.json`); 288 | const conversation: Conversation = JSON.parse(await fs.readFile(conversationPath, 'utf8')); 289 | 290 | const userMessage: Message = { 291 | role: 'user', 292 | content, 293 | timestamp: new Date().toISOString() 294 | }; 295 | conversation.messages.push(userMessage); 296 | conversation.updated = new Date().toISOString(); 297 | 298 | try { 299 | if (stream) { 300 | const streamResponse = await openRouterProvider.streamCompletion({ 301 | model: conversation.model, 302 | messages: conversation.messages, 303 | stream: true 304 | }); 305 | 306 | await fs.writeFile(conversationPath, JSON.stringify(conversation, null, 2)); 307 | 308 | return { 309 | content: [{ 310 | type: 'resource', 311 | resource: { 312 | uri: `stream://${conversationId}`, 313 | text: 'Message stream started', 314 | mimeType: 'text/plain' 315 | } 316 | }] 317 | }; 318 | } else { 319 | const response = await openRouterProvider.createCompletion({ 320 | model: conversation.model, 321 | messages: conversation.messages, 322 | stream: false 323 | }); 324 | 325 | const assistantMessage: Message = { 326 | role: 'assistant', 327 | content: response.content, 328 | timestamp: new Date().toISOString() 329 | }; 330 | conversation.messages.push(assistantMessage); 331 | conversation.updated = new Date().toISOString(); 332 | 333 | await fs.writeFile(conversationPath, JSON.stringify(conversation, null, 2)); 334 | 335 | return { 336 | content: [{ 337 | type: 'text', 338 | text: JSON.stringify(assistantMessage, null, 2) 339 | }] 340 | }; 341 | } 342 | } catch (error) { 343 | const message = error instanceof Error ? error.message : 'Unknown error'; 344 | throw new OpenRouterError(`OpenRouter request failed: ${message}`); 345 | } 346 | } catch (error) { 347 | if (error instanceof OpenRouterError) throw error; 348 | const message = error instanceof Error ? error.message : 'Unknown error'; 349 | throw new FileSystemError(`Failed to handle message: ${message}`); 350 | } 351 | } 352 | ); 353 | 354 | // Add list-models tool 355 | server.tool( 356 | 'list-models', 357 | `Lists all available models with their configurations and capabilities.`, 358 | {}, 359 | async (_args: {}, _extra: any) => { 360 | try { 361 | const models = await openRouterProvider.listAvailableModels(); 362 | return { 363 | content: [{ 364 | type: 'text', 365 | text: JSON.stringify({ 366 | models, 367 | defaultModel: openRouterProvider.getDefaultModel(), 368 | totalModels: models.length 369 | }, null, 2) 370 | }] 371 | }; 372 | } catch (error) { 373 | const message = error instanceof Error ? error.message : 'Unknown error'; 374 | throw new Error(`Failed to list models: ${message}`); 375 | } 376 | } 377 | ); 378 | } 379 | 380 | // Start the server 381 | startServer(); ```