├── .gitignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── package.json
├── pnpm-lock.yaml
├── src
├── index.ts
├── model-cache.ts
├── openrouter-api.ts
├── tool-handlers.ts
├── tool-handlers
│ ├── chat-completion.ts
│ ├── get-model-info.ts
│ ├── search-models.ts
│ └── validate-model.ts
└── types.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | /ref/
4 | /docs/
5 | /project.txt
6 | .cline*
7 |
8 | # dependencies
9 | /node_modules
10 | /.pnp
11 | .pnp.*
12 | .yarn/*
13 | !.yarn/patches
14 | !.yarn/plugins
15 | !.yarn/releases
16 | !.yarn/versions
17 |
18 | # testing
19 | /coverage
20 |
21 | # docs
22 | project.*
23 |
24 | # production
25 | /build
26 | /dist
27 |
28 | # misc
29 | .DS_Store
30 | *.pem
31 |
32 | # debug
33 | npm-debug.log*
34 | yarn-debug.log*
35 | yarn-error.log*
36 |
37 | # env files (can opt-in for committing if needed)
38 | .env
39 |
40 | # typescript
41 | *.tsbuildinfo
42 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 | All notable changes to this project will be documented in this file.
3 |
4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6 |
7 | ## [2.2.0] - 2025-03-27
8 | ### Added
9 | - Unified ToolResult response format for all tool handlers
10 | - Structured error messages with "Error: " prefix consistency
11 | - Comprehensive error handling documentation in README
12 |
13 | ### Changed
14 | - Updated all tool handlers to use new response format
15 | - Improved error messages with contextual information
16 | - Refactored core error handling infrastructure
17 |
18 | ### Fixed
19 | - Removed residual JSON-RPC error code references
20 | - Standardized success/error response structures
21 |
22 |
23 | ## [2.1.0] - 2025-02-10
24 | ### Added
25 | - Conversation context support
26 | - MCP server badge
27 | ### Refactor
28 | - Modular structure
29 | ### Chore
30 | - Bump version to 2.1.0
31 | - Update dependencies and TypeScript config
32 | - Update .gitignore remove project specific files
33 | ### Documentation
34 | - Improve README with comprehensive documentation and badges
35 |
36 | ## [2.0.3] - 2024-03-22
37 | ### Changed
38 | - Updated dependencies to latest versions:
39 | - axios to ^1.7.9
40 | - openai to ^4.77.0
41 | - typescript to ^5.7.2
42 | - Updated .gitignore file list
43 | - Fixed package.json bin configuration format
44 | ### Added
45 | - Package overrides to fix punycode deprecation warning:
46 | - Added uri-js-replace to replace deprecated uri-js
47 | - Updated whatwg-url to v14.1.0
48 | ### Fixed
49 | - Updated repository URLs to correct GitHub repository
50 | - Fixed Node.js punycode deprecation warning (DEP0040)
51 |
52 | ## [2.0.2] - 2024-03-21
53 | ### Changed
54 | - Simplified binary name to 'openrouterai'
55 |
56 | ## [2.0.1] - 2024-03-21
57 | ### Added
58 | - Complete npm package configuration
59 | - Binary support for CLI installation
60 | - Repository and documentation links
61 | - Node.js engine requirement specification
62 | - PrepublishOnly script for build safety
63 |
64 | ## [2.0.0] - 2024-03-20
65 | ### Breaking Changes
66 | - Remove list_models tool in favor of enhanced search_models
67 | - Remove set_default_model and clear_default_model in favor of config-based default model
68 | - Move default model to MCP configuration via OPENROUTER_DEFAULT_MODEL environment variable
69 |
70 | ### Added
71 | - Comprehensive model filtering capabilities
72 | - Direct OpenRouter /models endpoint integration
73 | - Accurate model data (pricing, context length, capabilities)
74 | - Rate limiting with exponential backoff
75 | - Model capability validation
76 | - Cache invalidation strategy
77 | - Enhanced error handling with detailed feedback
78 |
79 | ### Changed
80 | - Rename StateManager to ModelCache for better clarity
81 | - Update error messages to reference MCP configuration
82 | - Switch from OpenAI SDK models.list() to direct OpenRouter API calls
83 | - Update package name to @mcpservers/openrouterai
84 | - Update documentation to follow MCP server standards
85 |
86 | ## [1.0.0] - 2024-03-15
87 | ### Added
88 | - Initial OpenRouter MCP server implementation
89 | - Basic model management features and state handling
90 | - Core API integration
91 | - Project documentation and configuration files
92 |
93 | ### Changed
94 | - Update license to Apache 2.0
95 |
96 | [2.2.0]: https://github.com/mcpservers/openrouterai/compare/v2.1.0...v2.2.0
97 | [2.0.2]: https://github.com/mcpservers/openrouterai/compare/v2.0.1...v2.0.2
98 | [2.0.1]: https://github.com/mcpservers/openrouterai/compare/v2.0.0...v2.0.1
99 | [2.0.0]: https://github.com/mcpservers/openrouterai/compare/v1.0.0...v2.0.0
100 | [1.0.0]: https://github.com/mcpservers/openrouterai/releases/tag/v1.0.0
101 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to OpenRouter MCP Server
2 |
3 | ## Development Setup
4 |
5 | 1. Clone the repository:
6 | ```bash
7 | git clone https://github.com/heltonteixeira/openrouterai.git
8 | cd openrouterai
9 | ```
10 |
11 | 2. Install dependencies:
12 | ```bash
13 | npm install
14 | ```
15 |
16 | 3. Build the project:
17 | ```bash
18 | npm run build
19 | ```
20 |
21 | ## Features
22 |
23 | - Chat completion support for all OpenRouter.ai models
24 | - Advanced model search and filtering:
25 | - Search by name, description, or provider
26 | - Filter by context length range
27 | - Filter by maximum price per token
28 | - Filter by model capabilities
29 | - Configurable result limits
30 | - Robust API handling:
31 | - Rate limiting with automatic retry
32 | - Exponential backoff for failed requests
33 | - Request caching with automatic expiration
34 | - Performance optimizations:
35 | - Model information caching (1-hour expiry)
36 | - Efficient model capability tracking
37 | - Error handling and reporting:
38 | - Detailed error messages with applied filters
39 | - Rate limit handling
40 | - API error recovery
41 |
42 | ## Model Information
43 |
44 | The server provides comprehensive model information:
45 |
46 | - **Pricing Data**: Accurate cost per token for both prompt and completion
47 | - **Context Length**: Model-specific maximum context window
48 | - **Capabilities**: Support for:
49 | - Function calling
50 | - Tool use
51 | - Vision/image processing
52 | - JSON mode
53 | - **Provider Details**: Maximum completion tokens and context lengths
54 |
55 | ## Rate Limiting
56 |
57 | The server implements intelligent rate limit handling:
58 |
59 | - Tracks remaining requests through response headers
60 | - Automatically waits when rate limits are reached
61 | - Implements exponential backoff for failed requests
62 | - Provides clear error messages for rate limit issues
63 |
64 | ## Caching
65 |
66 | Model information is cached for optimal performance:
67 |
68 | - 1-hour cache duration for model data
69 | - Automatic cache invalidation
70 | - Memory-efficient storage
71 |
72 | ## Error Handling
73 |
74 | Robust error handling for all operations:
75 |
76 | - Detailed error responses with applied filters
77 | - Rate limit detection and recovery
78 | - API error reporting with details
79 | - Model validation failures
80 | - Cache-related issues
81 | - Network timeouts and retries
82 |
83 | ## Tool Implementation Examples
84 |
85 | ### chat_completion
86 | ```typescript
87 | const response = await mcpClient.useTool("openrouterai", "chat_completion", {
88 | model: "anthropic/claude-3-opus-20240229", // Optional if default model is set in config
89 | messages: [
90 | { role: "user", content: "Hello!" }
91 | ],
92 | temperature: 0.7
93 | });
94 | ```
95 |
96 | ### search_models
97 | ```typescript
98 | const models = await mcpClient.useTool("openrouterai", "search_models", {
99 | query: "claude", // Optional: Search in name/description
100 | provider: "anthropic", // Optional: Filter by provider
101 | minContextLength: 10000, // Optional: Minimum context length
102 | maxContextLength: 100000, // Optional: Maximum context length
103 | maxPromptPrice: 0.01, // Optional: Max price per 1K tokens for prompts
104 | maxCompletionPrice: 0.02, // Optional: Max price per 1K tokens for completions
105 | capabilities: { // Optional: Required capabilities
106 | functions: true,
107 | tools: true,
108 | vision: false,
109 | json_mode: true
110 | },
111 | limit: 10 // Optional: Maximum results (default: 10, max: 50)
112 | });
113 | ```
114 |
115 | ### get_model_info
116 | ```typescript
117 | const info = await mcpClient.useTool("openrouterai", "get_model_info", {
118 | model: "anthropic/claude-3-opus-20240229"
119 | });
120 | ```
121 |
122 | ### validate_model
123 | ```typescript
124 | const validation = await mcpClient.useTool("openrouterai", "validate_model", {
125 | model: "anthropic/claude-3-opus-20240229"
126 | });
127 | ```
128 |
129 | ## License
130 |
131 | Apache License 2.0 - see LICENSE file for details.
132 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2024 Cline Bot Inc.
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # OpenRouter MCP Server
2 |
3 | [](https://github.com/heltonteixeira/openrouterai)
4 | [](CHANGELOG.md)
5 | [](https://www.typescriptlang.org/)
6 | [](LICENSE)
7 |
8 | A Model Context Protocol (MCP) server providing seamless integration with OpenRouter.ai's diverse model ecosystem. Access various AI models through a unified, type-safe interface with built-in caching, rate limiting, and error handling.
9 |
10 |
11 |
12 | ## Features
13 |
14 | - **Model Access**
15 | - Direct access to all OpenRouter.ai models
16 | - Automatic model validation and capability checking
17 | - Default model configuration support
18 |
19 | - **Performance Optimization**
20 | - Smart model information caching (1-hour expiry)
21 | - Automatic rate limit management
22 | - Exponential backoff for failed requests
23 |
24 | - **Unified Response Format**
25 | - Consistent `ToolResult` structure for all responses
26 | - Clear error identification with `isError` flag
27 | - Structured error messages with context
28 | ## Installation
29 |
30 | ```bash
31 | pnpm install @mcpservers/openrouterai
32 | ```
33 |
34 | ## Configuration
35 |
36 | ### Prerequisites
37 |
38 | 1. Get your OpenRouter API key from [OpenRouter Keys](https://openrouter.ai/keys)
39 | 2. Choose a default model (optional)
40 |
41 | ### Environment Variables
42 | ```env
43 | OPENROUTER_API_KEY=your-api-key-here
44 | OPENROUTER_DEFAULT_MODEL=optional-default-model
45 | ```
46 |
47 | ### Setup
48 |
49 | Add to your MCP settings configuration file (`cline_mcp_settings.json` or `claude_desktop_config.json`):
50 |
51 | ```json
52 | {
53 | "mcpServers": {
54 | "openrouterai": {
55 | "command": "npx",
56 | "args": ["@mcpservers/openrouterai"],
57 | "env": {
58 | "OPENROUTER_API_KEY": "your-api-key-here",
59 | "OPENROUTER_DEFAULT_MODEL": "optional-default-model"
60 | }
61 | }
62 | }
63 | }
64 | ```
65 |
66 | ## Response Format
67 |
68 | All tools return responses in a standardized structure:
69 |
70 | ```typescript
71 | interface ToolResult {
72 | isError: boolean;
73 | content: Array<{
74 | type: "text";
75 | text: string; // JSON string or error message
76 | }>;
77 | }
78 | ```
79 |
80 | **Success Example:**
81 | ```json
82 | {
83 | "isError": false,
84 | "content": [{
85 | "type": "text",
86 | "text": "{\"id\": \"gen-123\", ...}"
87 | }]
88 | }
89 | ```
90 |
91 | **Error Example:**
92 | ```json
93 | {
94 | "isError": true,
95 | "content": [{
96 | "type": "text",
97 | "text": "Error: Model validation failed - 'invalid-model' not found"
98 | }]
99 | }
100 | ```
101 |
102 | ## Available Tools
103 |
104 | ### chat_completion
105 |
106 | Send messages to OpenRouter.ai models:
107 |
108 | ```typescript
109 | interface ChatCompletionRequest {
110 | model?: string;
111 | messages: Array<{role: "user"|"system"|"assistant", content: string}>;
112 | temperature?: number; // 0-2
113 | }
114 |
115 | // Response: ToolResult with chat completion data or error
116 | ```
117 |
118 | ### search_models
119 |
120 | Search and filter available models:
121 |
122 | ```typescript
123 | interface ModelSearchRequest {
124 | query?: string;
125 | provider?: string;
126 | minContextLength?: number;
127 | capabilities?: {
128 | functions?: boolean;
129 | vision?: boolean;
130 | };
131 | }
132 |
133 | // Response: ToolResult with model list or error
134 | ```
135 |
136 | ### get_model_info
137 |
138 | Get detailed information about a specific model:
139 |
140 | ```typescript
141 | {
142 | model: string; // Model identifier
143 | }
144 | ```
145 |
146 | ### validate_model
147 |
148 | Check if a model ID is valid:
149 |
150 | ```typescript
151 | interface ModelValidationRequest {
152 | model: string;
153 | }
154 |
155 | // Response:
156 | // Success: { isError: false, valid: true }
157 | // Error: { isError: true, error: "Model not found" }
158 | ```
159 |
160 | ## Error Handling
161 |
162 | The server provides structured errors with contextual information:
163 |
164 | ```typescript
165 | // Error response structure
166 | {
167 | isError: true,
168 | content: [{
169 | type: "text",
170 | text: "Error: [Category] - Detailed message"
171 | }]
172 | }
173 | ```
174 |
175 | **Common Error Categories:**
176 | - `Validation Error`: Invalid input parameters
177 | - `API Error`: OpenRouter API communication issues
178 | - `Rate Limit`: Request throttling detection
179 | - `Internal Error`: Server-side processing failures
180 |
181 | **Handling Responses:**
182 | ```typescript
183 | async function handleResponse(result: ToolResult) {
184 | if (result.isError) {
185 | const errorMessage = result.content[0].text;
186 | if (errorMessage.startsWith('Error: Rate Limit')) {
187 | // Handle rate limiting
188 | }
189 | // Other error handling
190 | } else {
191 | const data = JSON.parse(result.content[0].text);
192 | // Process successful response
193 | }
194 | }
195 | ```
196 |
197 | ## Development
198 |
199 | See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed information about:
200 | - Development setup
201 | - Project structure
202 | - Feature implementation
203 | - Error handling guidelines
204 | - Tool usage examples
205 |
206 | ```bash
207 | # Install dependencies
208 | pnpm install
209 |
210 | # Build project
211 | pnpm run build
212 |
213 | # Run tests
214 | pnpm test
215 | ```
216 |
217 | ## Changelog
218 | See [CHANGELOG.md](./CHANGELOG.md) for recent updates including:
219 | - Unified response format implementation
220 | - Enhanced error handling system
221 | - Type-safe interface improvements
222 |
223 | ## License
224 |
225 | This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mcpservers/openrouterai",
3 | "version": "2.2.0",
4 | "type": "module",
5 | "main": "dist/index.js",
6 | "bin": {
7 | "openrouterai": "dist/index.js"
8 | },
9 | "files": [
10 | "dist",
11 | "README.md",
12 | "LICENSE"
13 | ],
14 | "scripts": {
15 | "build": "tsc && shx chmod +x dist/*.js",
16 | "prepare": "npm run build",
17 | "watch": "tsc --watch"
18 | },
19 | "keywords": [
20 | "mcp",
21 | "openrouter",
22 | "ai",
23 | "llm",
24 | "modelcontextprotocol"
25 | ],
26 | "author": "bossying",
27 | "license": "Apache-2.0",
28 | "description": "MCP server for OpenRouter.ai integration",
29 | "repository": {
30 | "type": "git",
31 | "url": "git+https://github.com/heltonteixeira/openrouterai.git"
32 | },
33 | "bugs": {
34 | "url": "https://github.com/heltonteixeira/openrouterai/issues"
35 | },
36 | "homepage": "https://github.com/heltonteixeira/openrouterai#readme",
37 | "engines": {
38 | "node": ">=18.0.0"
39 | },
40 | "dependencies": {
41 | "@modelcontextprotocol/sdk": "1.4.1",
42 | "axios": "^1.7.9",
43 | "openai": "^4.83.0",
44 | "typescript": "^5.7.3"
45 | },
46 | "devDependencies": {
47 | "@types/node": "^22.13.1",
48 | "shx": "^0.3.4"
49 | },
50 | "overrides": {
51 | "uri-js": "npm:uri-js-replace",
52 | "whatwg-url": "^14.1.0"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/pnpm-lock.yaml:
--------------------------------------------------------------------------------
1 | lockfileVersion: '9.0'
2 |
3 | settings:
4 | autoInstallPeers: true
5 | excludeLinksFromLockfile: false
6 |
7 | importers:
8 |
9 | .:
10 | dependencies:
11 | '@modelcontextprotocol/sdk':
12 | specifier: 1.4.1
13 | version: 1.4.1
14 | axios:
15 | specifier: ^1.7.9
16 | version: 1.8.4
17 | openai:
18 | specifier: ^4.83.0
19 | version: 4.89.0(zod@3.24.2)
20 | typescript:
21 | specifier: ^5.7.3
22 | version: 5.8.2
23 | devDependencies:
24 | '@types/node':
25 | specifier: ^22.13.1
26 | version: 22.13.11
27 | shx:
28 | specifier: ^0.3.4
29 | version: 0.3.4
30 |
31 | packages:
32 |
33 | '@modelcontextprotocol/sdk@1.4.1':
34 | resolution: {integrity: sha512-wS6YC4lkUZ9QpP+/7NBTlVNiEvsnyl0xF7rRusLF+RsG0xDPc/zWR7fEEyhKnnNutGsDAZh59l/AeoWGwIb1+g==}
35 | engines: {node: '>=18'}
36 |
37 | '@types/node-fetch@2.6.12':
38 | resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==}
39 |
40 | '@types/node@18.19.81':
41 | resolution: {integrity: sha512-7KO9oZ2//ivtSsryp0LQUqq79zyGXzwq1WqfywpC9ucjY7YyltMMmxWgtRFRKCxwa7VPxVBVy4kHf5UC1E8Lug==}
42 |
43 | '@types/node@22.13.11':
44 | resolution: {integrity: sha512-iEUCUJoU0i3VnrCmgoWCXttklWcvoCIx4jzcP22fioIVSdTmjgoEvmAO/QPw6TcS9k5FrNgn4w7q5lGOd1CT5g==}
45 |
46 | abort-controller@3.0.0:
47 | resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
48 | engines: {node: '>=6.5'}
49 |
50 | agentkeepalive@4.6.0:
51 | resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==}
52 | engines: {node: '>= 8.0.0'}
53 |
54 | asynckit@0.4.0:
55 | resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
56 |
57 | axios@1.8.4:
58 | resolution: {integrity: sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==}
59 |
60 | balanced-match@1.0.2:
61 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
62 |
63 | brace-expansion@1.1.11:
64 | resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
65 |
66 | bytes@3.1.2:
67 | resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
68 | engines: {node: '>= 0.8'}
69 |
70 | call-bind-apply-helpers@1.0.2:
71 | resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
72 | engines: {node: '>= 0.4'}
73 |
74 | combined-stream@1.0.8:
75 | resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
76 | engines: {node: '>= 0.8'}
77 |
78 | concat-map@0.0.1:
79 | resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
80 |
81 | content-type@1.0.5:
82 | resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
83 | engines: {node: '>= 0.6'}
84 |
85 | delayed-stream@1.0.0:
86 | resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
87 | engines: {node: '>=0.4.0'}
88 |
89 | depd@2.0.0:
90 | resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
91 | engines: {node: '>= 0.8'}
92 |
93 | dunder-proto@1.0.1:
94 | resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
95 | engines: {node: '>= 0.4'}
96 |
97 | es-define-property@1.0.1:
98 | resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
99 | engines: {node: '>= 0.4'}
100 |
101 | es-errors@1.3.0:
102 | resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
103 | engines: {node: '>= 0.4'}
104 |
105 | es-object-atoms@1.1.1:
106 | resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
107 | engines: {node: '>= 0.4'}
108 |
109 | es-set-tostringtag@2.1.0:
110 | resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
111 | engines: {node: '>= 0.4'}
112 |
113 | event-target-shim@5.0.1:
114 | resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
115 | engines: {node: '>=6'}
116 |
117 | eventsource-parser@3.0.0:
118 | resolution: {integrity: sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==}
119 | engines: {node: '>=18.0.0'}
120 |
121 | eventsource@3.0.5:
122 | resolution: {integrity: sha512-LT/5J605bx5SNyE+ITBDiM3FxffBiq9un7Vx0EwMDM3vg8sWKx/tO2zC+LMqZ+smAM0F2hblaDZUVZF0te2pSw==}
123 | engines: {node: '>=18.0.0'}
124 |
125 | follow-redirects@1.15.9:
126 | resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
127 | engines: {node: '>=4.0'}
128 | peerDependencies:
129 | debug: '*'
130 | peerDependenciesMeta:
131 | debug:
132 | optional: true
133 |
134 | form-data-encoder@1.7.2:
135 | resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
136 |
137 | form-data@4.0.2:
138 | resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==}
139 | engines: {node: '>= 6'}
140 |
141 | formdata-node@4.4.1:
142 | resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==}
143 | engines: {node: '>= 12.20'}
144 |
145 | fs.realpath@1.0.0:
146 | resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
147 |
148 | function-bind@1.1.2:
149 | resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
150 |
151 | get-intrinsic@1.3.0:
152 | resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
153 | engines: {node: '>= 0.4'}
154 |
155 | get-proto@1.0.1:
156 | resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
157 | engines: {node: '>= 0.4'}
158 |
159 | glob@7.2.3:
160 | resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
161 | deprecated: Glob versions prior to v9 are no longer supported
162 |
163 | gopd@1.2.0:
164 | resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
165 | engines: {node: '>= 0.4'}
166 |
167 | has-symbols@1.1.0:
168 | resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
169 | engines: {node: '>= 0.4'}
170 |
171 | has-tostringtag@1.0.2:
172 | resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
173 | engines: {node: '>= 0.4'}
174 |
175 | hasown@2.0.2:
176 | resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
177 | engines: {node: '>= 0.4'}
178 |
179 | http-errors@2.0.0:
180 | resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
181 | engines: {node: '>= 0.8'}
182 |
183 | humanize-ms@1.2.1:
184 | resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
185 |
186 | iconv-lite@0.6.3:
187 | resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
188 | engines: {node: '>=0.10.0'}
189 |
190 | inflight@1.0.6:
191 | resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
192 | deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
193 |
194 | inherits@2.0.4:
195 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
196 |
197 | interpret@1.4.0:
198 | resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==}
199 | engines: {node: '>= 0.10'}
200 |
201 | is-core-module@2.16.1:
202 | resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
203 | engines: {node: '>= 0.4'}
204 |
205 | math-intrinsics@1.1.0:
206 | resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
207 | engines: {node: '>= 0.4'}
208 |
209 | mime-db@1.52.0:
210 | resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
211 | engines: {node: '>= 0.6'}
212 |
213 | mime-types@2.1.35:
214 | resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
215 | engines: {node: '>= 0.6'}
216 |
217 | minimatch@3.1.2:
218 | resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
219 |
220 | minimist@1.2.8:
221 | resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
222 |
223 | ms@2.1.3:
224 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
225 |
226 | node-domexception@1.0.0:
227 | resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
228 | engines: {node: '>=10.5.0'}
229 |
230 | node-fetch@2.7.0:
231 | resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
232 | engines: {node: 4.x || >=6.0.0}
233 | peerDependencies:
234 | encoding: ^0.1.0
235 | peerDependenciesMeta:
236 | encoding:
237 | optional: true
238 |
239 | once@1.4.0:
240 | resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
241 |
242 | openai@4.89.0:
243 | resolution: {integrity: sha512-XNI0q2l8/Os6jmojxaID5EhyQjxZgzR2gWcpEjYWK5hGKwE7AcifxEY7UNwFDDHJQXqeiosQ0CJwQN+rvnwdjA==}
244 | hasBin: true
245 | peerDependencies:
246 | ws: ^8.18.0
247 | zod: ^3.23.8
248 | peerDependenciesMeta:
249 | ws:
250 | optional: true
251 | zod:
252 | optional: true
253 |
254 | path-is-absolute@1.0.1:
255 | resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
256 | engines: {node: '>=0.10.0'}
257 |
258 | path-parse@1.0.7:
259 | resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
260 |
261 | proxy-from-env@1.1.0:
262 | resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
263 |
264 | raw-body@3.0.0:
265 | resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==}
266 | engines: {node: '>= 0.8'}
267 |
268 | rechoir@0.6.2:
269 | resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==}
270 | engines: {node: '>= 0.10'}
271 |
272 | resolve@1.22.10:
273 | resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==}
274 | engines: {node: '>= 0.4'}
275 | hasBin: true
276 |
277 | safer-buffer@2.1.2:
278 | resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
279 |
280 | setprototypeof@1.2.0:
281 | resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
282 |
283 | shelljs@0.8.5:
284 | resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==}
285 | engines: {node: '>=4'}
286 | hasBin: true
287 |
288 | shx@0.3.4:
289 | resolution: {integrity: sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==}
290 | engines: {node: '>=6'}
291 | hasBin: true
292 |
293 | statuses@2.0.1:
294 | resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
295 | engines: {node: '>= 0.8'}
296 |
297 | supports-preserve-symlinks-flag@1.0.0:
298 | resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
299 | engines: {node: '>= 0.4'}
300 |
301 | toidentifier@1.0.1:
302 | resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
303 | engines: {node: '>=0.6'}
304 |
305 | tr46@0.0.3:
306 | resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
307 |
308 | typescript@5.8.2:
309 | resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==}
310 | engines: {node: '>=14.17'}
311 | hasBin: true
312 |
313 | undici-types@5.26.5:
314 | resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
315 |
316 | undici-types@6.20.0:
317 | resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==}
318 |
319 | unpipe@1.0.0:
320 | resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
321 | engines: {node: '>= 0.8'}
322 |
323 | web-streams-polyfill@4.0.0-beta.3:
324 | resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
325 | engines: {node: '>= 14'}
326 |
327 | webidl-conversions@3.0.1:
328 | resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
329 |
330 | whatwg-url@5.0.0:
331 | resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
332 |
333 | wrappy@1.0.2:
334 | resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
335 |
336 | zod-to-json-schema@3.24.5:
337 | resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==}
338 | peerDependencies:
339 | zod: ^3.24.1
340 |
341 | zod@3.24.2:
342 | resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==}
343 |
344 | snapshots:
345 |
346 | '@modelcontextprotocol/sdk@1.4.1':
347 | dependencies:
348 | content-type: 1.0.5
349 | eventsource: 3.0.5
350 | raw-body: 3.0.0
351 | zod: 3.24.2
352 | zod-to-json-schema: 3.24.5(zod@3.24.2)
353 |
354 | '@types/node-fetch@2.6.12':
355 | dependencies:
356 | '@types/node': 22.13.11
357 | form-data: 4.0.2
358 |
359 | '@types/node@18.19.81':
360 | dependencies:
361 | undici-types: 5.26.5
362 |
363 | '@types/node@22.13.11':
364 | dependencies:
365 | undici-types: 6.20.0
366 |
367 | abort-controller@3.0.0:
368 | dependencies:
369 | event-target-shim: 5.0.1
370 |
371 | agentkeepalive@4.6.0:
372 | dependencies:
373 | humanize-ms: 1.2.1
374 |
375 | asynckit@0.4.0: {}
376 |
377 | axios@1.8.4:
378 | dependencies:
379 | follow-redirects: 1.15.9
380 | form-data: 4.0.2
381 | proxy-from-env: 1.1.0
382 | transitivePeerDependencies:
383 | - debug
384 |
385 | balanced-match@1.0.2: {}
386 |
387 | brace-expansion@1.1.11:
388 | dependencies:
389 | balanced-match: 1.0.2
390 | concat-map: 0.0.1
391 |
392 | bytes@3.1.2: {}
393 |
394 | call-bind-apply-helpers@1.0.2:
395 | dependencies:
396 | es-errors: 1.3.0
397 | function-bind: 1.1.2
398 |
399 | combined-stream@1.0.8:
400 | dependencies:
401 | delayed-stream: 1.0.0
402 |
403 | concat-map@0.0.1: {}
404 |
405 | content-type@1.0.5: {}
406 |
407 | delayed-stream@1.0.0: {}
408 |
409 | depd@2.0.0: {}
410 |
411 | dunder-proto@1.0.1:
412 | dependencies:
413 | call-bind-apply-helpers: 1.0.2
414 | es-errors: 1.3.0
415 | gopd: 1.2.0
416 |
417 | es-define-property@1.0.1: {}
418 |
419 | es-errors@1.3.0: {}
420 |
421 | es-object-atoms@1.1.1:
422 | dependencies:
423 | es-errors: 1.3.0
424 |
425 | es-set-tostringtag@2.1.0:
426 | dependencies:
427 | es-errors: 1.3.0
428 | get-intrinsic: 1.3.0
429 | has-tostringtag: 1.0.2
430 | hasown: 2.0.2
431 |
432 | event-target-shim@5.0.1: {}
433 |
434 | eventsource-parser@3.0.0: {}
435 |
436 | eventsource@3.0.5:
437 | dependencies:
438 | eventsource-parser: 3.0.0
439 |
440 | follow-redirects@1.15.9: {}
441 |
442 | form-data-encoder@1.7.2: {}
443 |
444 | form-data@4.0.2:
445 | dependencies:
446 | asynckit: 0.4.0
447 | combined-stream: 1.0.8
448 | es-set-tostringtag: 2.1.0
449 | mime-types: 2.1.35
450 |
451 | formdata-node@4.4.1:
452 | dependencies:
453 | node-domexception: 1.0.0
454 | web-streams-polyfill: 4.0.0-beta.3
455 |
456 | fs.realpath@1.0.0: {}
457 |
458 | function-bind@1.1.2: {}
459 |
460 | get-intrinsic@1.3.0:
461 | dependencies:
462 | call-bind-apply-helpers: 1.0.2
463 | es-define-property: 1.0.1
464 | es-errors: 1.3.0
465 | es-object-atoms: 1.1.1
466 | function-bind: 1.1.2
467 | get-proto: 1.0.1
468 | gopd: 1.2.0
469 | has-symbols: 1.1.0
470 | hasown: 2.0.2
471 | math-intrinsics: 1.1.0
472 |
473 | get-proto@1.0.1:
474 | dependencies:
475 | dunder-proto: 1.0.1
476 | es-object-atoms: 1.1.1
477 |
478 | glob@7.2.3:
479 | dependencies:
480 | fs.realpath: 1.0.0
481 | inflight: 1.0.6
482 | inherits: 2.0.4
483 | minimatch: 3.1.2
484 | once: 1.4.0
485 | path-is-absolute: 1.0.1
486 |
487 | gopd@1.2.0: {}
488 |
489 | has-symbols@1.1.0: {}
490 |
491 | has-tostringtag@1.0.2:
492 | dependencies:
493 | has-symbols: 1.1.0
494 |
495 | hasown@2.0.2:
496 | dependencies:
497 | function-bind: 1.1.2
498 |
499 | http-errors@2.0.0:
500 | dependencies:
501 | depd: 2.0.0
502 | inherits: 2.0.4
503 | setprototypeof: 1.2.0
504 | statuses: 2.0.1
505 | toidentifier: 1.0.1
506 |
507 | humanize-ms@1.2.1:
508 | dependencies:
509 | ms: 2.1.3
510 |
511 | iconv-lite@0.6.3:
512 | dependencies:
513 | safer-buffer: 2.1.2
514 |
515 | inflight@1.0.6:
516 | dependencies:
517 | once: 1.4.0
518 | wrappy: 1.0.2
519 |
520 | inherits@2.0.4: {}
521 |
522 | interpret@1.4.0: {}
523 |
524 | is-core-module@2.16.1:
525 | dependencies:
526 | hasown: 2.0.2
527 |
528 | math-intrinsics@1.1.0: {}
529 |
530 | mime-db@1.52.0: {}
531 |
532 | mime-types@2.1.35:
533 | dependencies:
534 | mime-db: 1.52.0
535 |
536 | minimatch@3.1.2:
537 | dependencies:
538 | brace-expansion: 1.1.11
539 |
540 | minimist@1.2.8: {}
541 |
542 | ms@2.1.3: {}
543 |
544 | node-domexception@1.0.0: {}
545 |
546 | node-fetch@2.7.0:
547 | dependencies:
548 | whatwg-url: 5.0.0
549 |
550 | once@1.4.0:
551 | dependencies:
552 | wrappy: 1.0.2
553 |
554 | openai@4.89.0(zod@3.24.2):
555 | dependencies:
556 | '@types/node': 18.19.81
557 | '@types/node-fetch': 2.6.12
558 | abort-controller: 3.0.0
559 | agentkeepalive: 4.6.0
560 | form-data-encoder: 1.7.2
561 | formdata-node: 4.4.1
562 | node-fetch: 2.7.0
563 | optionalDependencies:
564 | zod: 3.24.2
565 | transitivePeerDependencies:
566 | - encoding
567 |
568 | path-is-absolute@1.0.1: {}
569 |
570 | path-parse@1.0.7: {}
571 |
572 | proxy-from-env@1.1.0: {}
573 |
574 | raw-body@3.0.0:
575 | dependencies:
576 | bytes: 3.1.2
577 | http-errors: 2.0.0
578 | iconv-lite: 0.6.3
579 | unpipe: 1.0.0
580 |
581 | rechoir@0.6.2:
582 | dependencies:
583 | resolve: 1.22.10
584 |
585 | resolve@1.22.10:
586 | dependencies:
587 | is-core-module: 2.16.1
588 | path-parse: 1.0.7
589 | supports-preserve-symlinks-flag: 1.0.0
590 |
591 | safer-buffer@2.1.2: {}
592 |
593 | setprototypeof@1.2.0: {}
594 |
595 | shelljs@0.8.5:
596 | dependencies:
597 | glob: 7.2.3
598 | interpret: 1.4.0
599 | rechoir: 0.6.2
600 |
601 | shx@0.3.4:
602 | dependencies:
603 | minimist: 1.2.8
604 | shelljs: 0.8.5
605 |
606 | statuses@2.0.1: {}
607 |
608 | supports-preserve-symlinks-flag@1.0.0: {}
609 |
610 | toidentifier@1.0.1: {}
611 |
612 | tr46@0.0.3: {}
613 |
614 | typescript@5.8.2: {}
615 |
616 | undici-types@5.26.5: {}
617 |
618 | undici-types@6.20.0: {}
619 |
620 | unpipe@1.0.0: {}
621 |
622 | web-streams-polyfill@4.0.0-beta.3: {}
623 |
624 | webidl-conversions@3.0.1: {}
625 |
626 | whatwg-url@5.0.0:
627 | dependencies:
628 | tr46: 0.0.3
629 | webidl-conversions: 3.0.1
630 |
631 | wrappy@1.0.2: {}
632 |
633 | zod-to-json-schema@3.24.5(zod@3.24.2):
634 | dependencies:
635 | zod: 3.24.2
636 |
637 | zod@3.24.2: {}
638 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4 |
5 | import { ToolHandlers } from './tool-handlers.js';
6 |
7 | const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY;
8 | const DEFAULT_MODEL = process.env.OPENROUTER_DEFAULT_MODEL;
9 |
10 | if (!OPENROUTER_API_KEY) {
11 | throw new Error('OPENROUTER_API_KEY environment variable is required');
12 | }
13 |
14 | class OpenRouterServer {
15 | private server: Server;
16 | private toolHandlers: ToolHandlers;
17 |
18 | constructor() {
19 | this.server = new Server(
20 | {
21 | name: 'openrouter-server',
22 | version: '2.2.0',
23 | },
24 | {
25 | capabilities: {
26 | tools: {},
27 | },
28 | }
29 | );
30 |
31 | // Initialize tool handlers
32 | this.toolHandlers = new ToolHandlers(
33 | this.server,
34 | OPENROUTER_API_KEY!,
35 | DEFAULT_MODEL
36 | );
37 |
38 | // Error handling
39 | this.server.onerror = (error) => console.error('[MCP Error]', error);
40 | process.on('SIGINT', async () => {
41 | await this.server.close();
42 | process.exit(0);
43 | });
44 | }
45 |
46 | async run() {
47 | const transport = new StdioServerTransport();
48 | await this.server.connect(transport);
49 | console.error('OpenRouter MCP server running on stdio');
50 | }
51 | }
52 |
53 | const server = new OpenRouterServer();
54 | server.run().catch(console.error);
55 |
--------------------------------------------------------------------------------
/src/model-cache.ts:
--------------------------------------------------------------------------------
1 | export interface OpenRouterModel {
2 | id: string;
3 | name: string;
4 | description?: string;
5 | context_length: number;
6 | pricing: {
7 | prompt: string;
8 | completion: string;
9 | unit: number;
10 | };
11 | top_provider?: {
12 | max_completion_tokens?: number;
13 | max_context_length?: number;
14 | };
15 | capabilities?: {
16 | functions?: boolean;
17 | tools?: boolean;
18 | vision?: boolean;
19 | json_mode?: boolean;
20 | };
21 | }
22 |
23 | export interface OpenRouterModelResponse {
24 | data: OpenRouterModel[];
25 | timestamp?: number;
26 | }
27 |
28 | export interface CachedModelResponse extends OpenRouterModelResponse {
29 | timestamp: number;
30 | }
31 |
32 | // Simple in-memory state management
33 | export class ModelCache {
34 | private static instance: ModelCache;
35 | private cachedModels: CachedModelResponse | null = null;
36 | private readonly cacheExpiry = 3600000; // 1 hour in milliseconds
37 |
38 | private constructor() {}
39 |
40 | static getInstance(): ModelCache {
41 | if (!ModelCache.instance) {
42 | ModelCache.instance = new ModelCache();
43 | }
44 | return ModelCache.instance;
45 | }
46 |
47 | private validateCache(): boolean {
48 | if (!this.cachedModels) return false;
49 | return Date.now() - this.cachedModels.timestamp <= this.cacheExpiry;
50 | }
51 |
52 | setCachedModels(models: OpenRouterModelResponse & { timestamp: number }) {
53 | this.cachedModels = models as CachedModelResponse;
54 | }
55 |
56 | getCachedModels(): CachedModelResponse | null {
57 | return this.validateCache() ? this.cachedModels : null;
58 | }
59 |
60 | clearCache() {
61 | this.cachedModels = null;
62 | }
63 |
64 | async validateModel(model: string): Promise {
65 | const models = this.getCachedModels();
66 | if (!models) return false;
67 | return models.data.some(m => m.id === model);
68 | }
69 |
70 | async getModelInfo(model: string): Promise {
71 | const models = this.getCachedModels();
72 | if (!models) return undefined;
73 | return models.data.find(m => m.id === model);
74 | }
75 | }
--------------------------------------------------------------------------------
/src/openrouter-api.ts:
--------------------------------------------------------------------------------
1 | import axios, { AxiosError, AxiosInstance } from 'axios';
2 | import { setTimeout } from 'timers/promises';
3 | import { OpenRouterModelResponse } from './model-cache.js';
4 |
5 | export interface RateLimitState {
6 | remaining: number;
7 | reset: number;
8 | total: number;
9 | }
10 |
11 | export const RETRY_DELAYS = [1000, 2000, 4000]; // Exponential backoff delays in ms
12 |
13 | export class OpenRouterAPIClient {
14 | private axiosInstance: AxiosInstance;
15 | private rateLimit: RateLimitState = {
16 | remaining: 50, // Default conservative value
17 | reset: Date.now() + 60000,
18 | total: 50
19 | };
20 |
21 | constructor(apiKey: string) {
22 | // Initialize axios instance for OpenRouter API
23 | this.axiosInstance = axios.create({
24 | baseURL: 'https://openrouter.ai/api/v1',
25 | headers: {
26 | 'Authorization': `Bearer ${apiKey}`,
27 | 'HTTP-Referer': 'https://github.com/heltonteixeira/openrouterai',
28 | 'X-Title': 'MCP OpenRouter Server'
29 | }
30 | });
31 |
32 | // Add response interceptor for rate limit headers
33 | this.axiosInstance.interceptors.response.use(
34 | (response: any) => {
35 | const remaining = parseInt(response.headers['x-ratelimit-remaining'] || '50');
36 | const reset = parseInt(response.headers['x-ratelimit-reset'] || '60');
37 | const total = parseInt(response.headers['x-ratelimit-limit'] || '50');
38 |
39 | this.rateLimit = {
40 | remaining,
41 | reset: Date.now() + (reset * 1000),
42 | total
43 | };
44 |
45 | return response;
46 | },
47 | async (error: AxiosError) => {
48 | if (error.response?.status === 429) {
49 | console.error('Rate limit exceeded, waiting for reset...');
50 | const resetAfter = parseInt(error.response.headers['retry-after'] || '60');
51 | await setTimeout(resetAfter * 1000);
52 | return this.axiosInstance.request(error.config!);
53 | }
54 | throw error;
55 | }
56 | );
57 | }
58 |
59 | async fetchModels(): Promise {
60 | // Check rate limits before making request
61 | if (this.rateLimit.remaining <= 0 && Date.now() < this.rateLimit.reset) {
62 | const waitTime = this.rateLimit.reset - Date.now();
63 | await setTimeout(waitTime);
64 | }
65 |
66 | // Retry mechanism for fetching models
67 | for (let i = 0; i <= RETRY_DELAYS.length; i++) {
68 | try {
69 | const response = await this.axiosInstance.get('/models');
70 | return {
71 | data: response.data.data,
72 | timestamp: Date.now()
73 | };
74 | } catch (error) {
75 | if (i === RETRY_DELAYS.length) throw error;
76 | await setTimeout(RETRY_DELAYS[i]);
77 | }
78 | }
79 |
80 | throw new Error('Failed to fetch models after multiple attempts');
81 | }
82 |
83 | async chatCompletion(params: {
84 | model: string,
85 | messages: any[],
86 | temperature?: number
87 | }) {
88 | return this.axiosInstance.post('/chat/completions', {
89 | model: params.model,
90 | messages: params.messages,
91 | temperature: params.temperature ?? 1
92 | });
93 | }
94 |
95 | getRateLimit(): RateLimitState {
96 | return { ...this.rateLimit };
97 | }
98 | }
--------------------------------------------------------------------------------
/src/tool-handlers.ts:
--------------------------------------------------------------------------------
1 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2 | import {
3 | CallToolRequestSchema,
4 | ErrorCode,
5 | ListToolsRequestSchema,
6 | McpError,
7 | } from '@modelcontextprotocol/sdk/types.js';
8 | import OpenAI from 'openai';
9 |
10 | import { ModelCache } from './model-cache.js';
11 | import { OpenRouterAPIClient } from './openrouter-api.js';
12 | import { ToolResult } from './types.js'; // Import the unified type
13 | import { handleChatCompletion, ChatCompletionToolRequest } from './tool-handlers/chat-completion.js';
14 | import { handleSearchModels, SearchModelsToolRequest } from './tool-handlers/search-models.js';
15 | import { handleGetModelInfo, GetModelInfoToolRequest } from './tool-handlers/get-model-info.js';
16 | import { handleValidateModel, ValidateModelToolRequest } from './tool-handlers/validate-model.js';
17 |
18 | export class ToolHandlers {
19 | private server: Server;
20 | private openai: OpenAI;
21 | private modelCache: ModelCache;
22 | private apiClient: OpenRouterAPIClient;
23 | private defaultModel?: string;
24 |
25 | constructor(
26 | server: Server,
27 | apiKey: string,
28 | defaultModel?: string
29 | ) {
30 | this.server = server;
31 | this.modelCache = ModelCache.getInstance();
32 | this.apiClient = new OpenRouterAPIClient(apiKey);
33 | this.defaultModel = defaultModel;
34 |
35 | this.openai = new OpenAI({
36 | apiKey: apiKey,
37 | baseURL: 'https://openrouter.ai/api/v1',
38 | defaultHeaders: {
39 | 'HTTP-Referer': 'https://github.com/heltonteixeira/openrouterai',
40 | 'X-Title': 'MCP OpenRouter Server',
41 | },
42 | });
43 |
44 | this.setupToolHandlers();
45 | }
46 |
47 | private setupToolHandlers() {
48 | this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
49 | tools: [
50 | {
51 | name: 'chat_completion',
52 | description: 'Send a message to OpenRouter.ai and get a response',
53 | inputSchema: {
54 | type: 'object',
55 | properties: {
56 | model: {
57 | type: 'string',
58 | description: 'The model to use (e.g., "google/gemini-2.0-flash-thinking-exp:free", "undi95/toppy-m-7b:free"). If not provided, uses the default model if set.',
59 | },
60 | messages: {
61 | type: 'array',
62 | description: 'An array of conversation messages with roles and content',
63 | minItems: 1,
64 | maxItems: 100,
65 | items: {
66 | type: 'object',
67 | properties: {
68 | role: {
69 | type: 'string',
70 | enum: ['system', 'user', 'assistant'],
71 | description: 'The role of the message sender',
72 | },
73 | content: {
74 | type: 'string',
75 | description: 'The content of the message',
76 | },
77 | },
78 | required: ['role', 'content'],
79 | } },
80 | temperature: {
81 | type: 'number',
82 | description: 'Sampling temperature (0-2)',
83 | minimum: 0,
84 | maximum: 2,
85 | },
86 | },
87 | required: ['messages'],
88 | },
89 | // Context window management details can be added as a separate property
90 | maxContextTokens: 200000
91 | },
92 | {
93 | name: 'search_models',
94 | description: 'Search and filter OpenRouter.ai models based on various criteria',
95 | inputSchema: {
96 | type: 'object',
97 | properties: {
98 | query: {
99 | type: 'string',
100 | description: 'Optional search query to filter by name, description, or provider',
101 | },
102 | provider: {
103 | type: 'string',
104 | description: 'Filter by specific provider (e.g., "anthropic", "openai", "cohere")',
105 | },
106 | minContextLength: {
107 | type: 'number',
108 | description: 'Minimum context length in tokens',
109 | },
110 | maxContextLength: {
111 | type: 'number',
112 | description: 'Maximum context length in tokens',
113 | },
114 | maxPromptPrice: {
115 | type: 'number',
116 | description: 'Maximum price per 1K tokens for prompts',
117 | },
118 | maxCompletionPrice: {
119 | type: 'number',
120 | description: 'Maximum price per 1K tokens for completions',
121 | },
122 | capabilities: {
123 | type: 'object',
124 | description: 'Filter by model capabilities',
125 | properties: {
126 | functions: {
127 | type: 'boolean',
128 | description: 'Requires function calling capability',
129 | },
130 | tools: {
131 | type: 'boolean',
132 | description: 'Requires tools capability',
133 | },
134 | vision: {
135 | type: 'boolean',
136 | description: 'Requires vision capability',
137 | },
138 | json_mode: {
139 | type: 'boolean',
140 | description: 'Requires JSON mode capability',
141 | }
142 | }
143 | },
144 | limit: {
145 | type: 'number',
146 | description: 'Maximum number of results to return (default: 10)',
147 | minimum: 1,
148 | maximum: 50
149 | }
150 | }
151 | },
152 | },
153 | {
154 | name: 'get_model_info',
155 | description: 'Get detailed information about a specific model',
156 | inputSchema: {
157 | type: 'object',
158 | properties: {
159 | model: {
160 | type: 'string',
161 | description: 'The model ID to get information for',
162 | },
163 | },
164 | required: ['model'],
165 | },
166 | },
167 | {
168 | name: 'validate_model',
169 | description: 'Check if a model ID is valid',
170 | inputSchema: {
171 | type: 'object',
172 | properties: {
173 | model: {
174 | type: 'string',
175 | description: 'The model ID to validate',
176 | },
177 | },
178 | required: ['model'],
179 | },
180 | },
181 | ],
182 | }));
183 |
184 | // Remove explicit return type annotation
185 | this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
186 | // Wrap the entire handler logic in a try...catch
187 | try {
188 | switch (request.params.name) {
189 | case 'chat_completion':
190 | // Add 'as any' to satisfy SDK type checker
191 | return handleChatCompletion({
192 | params: {
193 | arguments: request.params.arguments as unknown as ChatCompletionToolRequest
194 | }
195 | }, this.openai, this.defaultModel) as any;
196 |
197 | case 'search_models':
198 | // Add 'as any' to satisfy SDK type checker
199 | return handleSearchModels({
200 | params: {
201 | arguments: request.params.arguments as SearchModelsToolRequest
202 | }
203 | }, this.apiClient, this.modelCache) as any;
204 |
205 | case 'get_model_info':
206 | // Add 'as any' to satisfy SDK type checker
207 | return handleGetModelInfo({
208 | params: {
209 | arguments: request.params.arguments as unknown as GetModelInfoToolRequest
210 | }
211 | }, this.modelCache) as any;
212 |
213 | case 'validate_model':
214 | // Add 'as any' to satisfy SDK type checker
215 | return handleValidateModel({
216 | params: {
217 | arguments: request.params.arguments as unknown as ValidateModelToolRequest
218 | }
219 | }, this.modelCache) as any;
220 |
221 | default:
222 | // Return ToolResult for unknown tool
223 | console.warn(`Unknown tool requested: ${request.params.name}`);
224 | return {
225 | isError: true,
226 | content: [{ type: 'text', text: `Error: Tool '${request.params.name}' not found.` }],
227 | } as any; // Add 'as any'
228 | }
229 | } catch (error) {
230 | // Catch unexpected errors within the handler itself
231 | console.error('Unexpected error in CallToolRequest handler:', error);
232 | return {
233 | isError: true,
234 | content: [{ type: 'text', text: 'Error: Internal server error occurred while processing the tool call.' }],
235 | } as any; // Add 'as any'
236 | }
237 | });
238 | }
239 | }
--------------------------------------------------------------------------------
/src/tool-handlers/chat-completion.ts:
--------------------------------------------------------------------------------
1 | import OpenAI from 'openai';
2 | import { ChatCompletionMessageParam } from 'openai/resources/chat/completions.js';
3 | import { ToolResult } from '../types.js'; // Import the unified type
4 |
5 | // Maximum context tokens (matches tool-handlers.ts)
6 | const MAX_CONTEXT_TOKENS = 200000;
7 |
8 | export interface ChatCompletionToolRequest {
9 | model?: string;
10 | messages: ChatCompletionMessageParam[];
11 | temperature?: number;
12 | }
13 |
14 | // Utility function to estimate token count (simplified)
15 | function estimateTokenCount(text: string): number {
16 | // Rough approximation: 4 characters per token
17 | return Math.ceil(text.length / 4);
18 | }
19 |
20 | // Truncate messages to fit within the context window
21 | function truncateMessagesToFit(
22 | messages: ChatCompletionMessageParam[],
23 | maxTokens: number
24 | ): ChatCompletionMessageParam[] {
25 | const truncated: ChatCompletionMessageParam[] = [];
26 | let currentTokenCount = 0;
27 |
28 | // Always include system message first if present
29 | if (messages[0]?.role === 'system') {
30 | truncated.push(messages[0]);
31 | currentTokenCount += estimateTokenCount(messages[0].content as string);
32 | }
33 |
34 | // Add messages from the end, respecting the token limit
35 | for (let i = messages.length - 1; i >= 0; i--) {
36 | // Skip system message if already added
37 | if (i === 0 && messages[0]?.role === 'system') continue;
38 |
39 | const messageContent = messages[i].content;
40 | // Handle potential null/undefined content safely
41 | const contentString = typeof messageContent === 'string' ? messageContent : '';
42 | const messageTokens = estimateTokenCount(contentString);
43 |
44 | if (currentTokenCount + messageTokens > maxTokens) break;
45 |
46 | truncated.unshift(messages[i]);
47 | currentTokenCount += messageTokens;
48 | }
49 |
50 | return truncated;
51 | }
52 |
53 | // Update function signature to return Promise
54 | export async function handleChatCompletion(
55 | request: { params: { arguments: ChatCompletionToolRequest } },
56 | openai: OpenAI,
57 | defaultModel?: string
58 | ): Promise {
59 | const args = request.params.arguments;
60 |
61 | // Validate model selection
62 | const model = args.model || defaultModel;
63 | if (!model) {
64 | return {
65 | isError: true, // Ensure isError is present
66 | content: [
67 | {
68 | type: 'text',
69 | // Add "Error: " prefix
70 | text: 'Error: No model specified and no default model configured in MCP settings. Please specify a model or set OPENROUTER_DEFAULT_MODEL in the MCP configuration.',
71 | },
72 | ],
73 | };
74 | }
75 |
76 | // Validate message array
77 | if (!args.messages || args.messages.length === 0) { // Add check for undefined/null messages
78 | return {
79 | isError: true, // Ensure isError is present
80 | content: [
81 | {
82 | type: 'text',
83 | // Add "Error: " prefix
84 | text: 'Error: Messages array cannot be empty. At least one message is required.',
85 | },
86 | ],
87 | };
88 | }
89 |
90 | try {
91 | // Truncate messages to fit within context window
92 | const truncatedMessages = truncateMessagesToFit(args.messages, MAX_CONTEXT_TOKENS);
93 |
94 | const completion = await openai.chat.completions.create({
95 | model,
96 | messages: truncatedMessages,
97 | temperature: args.temperature ?? 1,
98 | });
99 |
100 | // Format response to match OpenRouter schema
101 | const response = {
102 | id: `gen-${Date.now()}`,
103 | choices: [{
104 | finish_reason: completion.choices[0].finish_reason,
105 | message: {
106 | role: completion.choices[0].message.role,
107 | content: completion.choices[0].message.content || '',
108 | tool_calls: completion.choices[0].message.tool_calls
109 | }
110 | }],
111 | created: Math.floor(Date.now() / 1000),
112 | model: model,
113 | object: 'chat.completion',
114 | usage: completion.usage || {
115 | prompt_tokens: 0,
116 | completion_tokens: 0,
117 | total_tokens: 0
118 | }
119 | };
120 |
121 | // Add isError: false to successful return
122 | return {
123 | isError: false,
124 | content: [
125 | {
126 | type: 'text',
127 | text: JSON.stringify(response, null, 2),
128 | },
129 | ],
130 | };
131 | } catch (error) {
132 | console.error('Error during chat completion:', error); // Log the error
133 | // Handle known and unknown errors, always return ToolResult
134 | if (error instanceof Error) {
135 | return {
136 | isError: true,
137 | content: [
138 | {
139 | type: 'text',
140 | // Add "Error: " prefix
141 | text: `Error: OpenRouter API error: ${error.message}`,
142 | },
143 | ],
144 | };
145 | } else {
146 | // Handle unknown errors
147 | return {
148 | isError: true,
149 | content: [
150 | {
151 | type: 'text',
152 | text: 'Error: An unknown error occurred during chat completion.',
153 | },
154 | ],
155 | };
156 | }
157 | // DO NOT throw error;
158 | }
159 | }
--------------------------------------------------------------------------------
/src/tool-handlers/get-model-info.ts:
--------------------------------------------------------------------------------
1 | import { ModelCache } from '../model-cache.js';
2 | import { ToolResult } from '../types.js'; // Import the unified type
3 |
4 | export interface GetModelInfoToolRequest {
5 | model: string;
6 | }
7 |
8 | // Update function signature to return Promise
9 | export async function handleGetModelInfo(
10 | request: { params: { arguments: GetModelInfoToolRequest } },
11 | modelCache: ModelCache
12 | ): Promise {
13 | const { model } = request.params.arguments;
14 |
15 | // Wrap core logic in try...catch
16 | try {
17 | const modelInfo = await modelCache.getModelInfo(model);
18 |
19 | if (!modelInfo) {
20 | return {
21 | isError: true, // Ensure isError is present
22 | content: [
23 | {
24 | type: 'text',
25 | // Add "Error: " prefix
26 | text: `Error: Model not found: ${model}`,
27 | },
28 | ],
29 | };
30 | }
31 |
32 | // Format successful response
33 | const response = {
34 | id: `info-${Date.now()}`,
35 | object: 'model',
36 | created: Math.floor(Date.now() / 1000),
37 | owned_by: modelInfo.id.split('/')[0],
38 | permission: [],
39 | root: modelInfo.id,
40 | parent: null,
41 | data: {
42 | id: modelInfo.id,
43 | name: modelInfo.name,
44 | description: modelInfo.description || 'No description available',
45 | context_length: modelInfo.context_length,
46 | pricing: {
47 | prompt: `$${modelInfo.pricing.prompt}/1K tokens`,
48 | completion: `$${modelInfo.pricing.completion}/1K tokens`
49 | },
50 | capabilities: {
51 | functions: modelInfo.capabilities?.functions || false,
52 | tools: modelInfo.capabilities?.tools || false,
53 | vision: modelInfo.capabilities?.vision || false,
54 | json_mode: modelInfo.capabilities?.json_mode || false
55 | }
56 | }
57 | };
58 |
59 | // Add isError: false to successful return
60 | return {
61 | isError: false,
62 | content: [
63 | {
64 | type: 'text',
65 | text: JSON.stringify(response, null, 2),
66 | },
67 | ],
68 | };
69 | } catch (error) {
70 | // Catch errors during model info retrieval
71 | console.error(`Error getting model info for ${model}:`, error);
72 | const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
73 | return {
74 | isError: true,
75 | content: [
76 | {
77 | type: 'text',
78 | text: `Error: Failed to get model info: ${errorMessage}`,
79 | },
80 | ],
81 | };
82 | }
83 | }
--------------------------------------------------------------------------------
/src/tool-handlers/search-models.ts:
--------------------------------------------------------------------------------
1 | import { ModelCache, OpenRouterModel } from '../model-cache.js';
2 | import { OpenRouterAPIClient } from '../openrouter-api.js';
3 | import { ToolResult } from '../types.js'; // Import the unified type
4 |
5 | export interface SearchModelsToolRequest {
6 | query?: string;
7 | provider?: string;
8 | minContextLength?: number;
9 | maxContextLength?: number;
10 | maxPromptPrice?: number;
11 | maxCompletionPrice?: number;
12 | capabilities?: {
13 | functions?: boolean;
14 | tools?: boolean;
15 | vision?: boolean;
16 | json_mode?: boolean;
17 | };
18 | limit?: number;
19 | }
20 |
21 | // Update function signature to return Promise
22 | export async function handleSearchModels(
23 | request: { params: { arguments: SearchModelsToolRequest } },
24 | apiClient: OpenRouterAPIClient,
25 | modelCache: ModelCache
26 | ): Promise {
27 | const args = request.params.arguments;
28 |
29 | try {
30 | // Use cached models if available
31 | let models = modelCache.getCachedModels();
32 | if (!models) {
33 | models = await apiClient.fetchModels();
34 | if (models) {
35 | modelCache.setCachedModels({ ...models, timestamp: Date.now() });
36 | }
37 | }
38 |
39 | // Simplify the "Failed to fetch models" error return
40 | if (!models) {
41 | return {
42 | isError: true, // Ensure isError is present
43 | content: [
44 | {
45 | type: 'text',
46 | // Use simple error string
47 | text: 'Error: Failed to fetch models. Please try again.',
48 | },
49 | ],
50 | };
51 | }
52 |
53 | // Apply all filters
54 | const searchResults = models.data
55 | .filter(model => {
56 | // Text search
57 | if (args.query) {
58 | const searchTerm = args.query.toLowerCase();
59 | const matchesQuery =
60 | model.id.toLowerCase().includes(searchTerm) ||
61 | (model.name && model.name.toLowerCase().includes(searchTerm)) ||
62 | (model.description && model.description.toLowerCase().includes(searchTerm));
63 | if (!matchesQuery) return false;
64 | }
65 |
66 | // Provider filter
67 | if (args.provider) {
68 | const provider = model.id.split('/')[0];
69 | if (provider !== args.provider.toLowerCase()) return false;
70 | }
71 |
72 | // Context length filters
73 | if (args.minContextLength && model.context_length < args.minContextLength) return false;
74 | if (args.maxContextLength && model.context_length > args.maxContextLength) return false;
75 |
76 | // Price filters
77 | if (args.maxPromptPrice && parseFloat(model.pricing.prompt) > args.maxPromptPrice) return false;
78 | if (args.maxCompletionPrice && parseFloat(model.pricing.completion) > args.maxCompletionPrice) return false;
79 |
80 | // Capabilities filters
81 | if (args.capabilities) {
82 | if (args.capabilities.functions && !model.capabilities?.functions) return false;
83 | if (args.capabilities.tools && !model.capabilities?.tools) return false;
84 | if (args.capabilities.vision && !model.capabilities?.vision) return false;
85 | if (args.capabilities.json_mode && !model.capabilities?.json_mode) return false;
86 | }
87 |
88 | return true;
89 | })
90 | // Apply limit
91 | .slice(0, args.limit || 10)
92 | .map(model => ({
93 | id: model.id,
94 | name: model.name,
95 | description: model.description || 'No description available',
96 | context_length: model.context_length,
97 | pricing: {
98 | prompt: `$${model.pricing.prompt}/1K tokens`,
99 | completion: `$${model.pricing.completion}/1K tokens`
100 | },
101 | capabilities: {
102 | functions: model.capabilities?.functions || false,
103 | tools: model.capabilities?.tools || false,
104 | vision: model.capabilities?.vision || false,
105 | json_mode: model.capabilities?.json_mode || false
106 | }
107 | }));
108 |
109 | const response = {
110 | id: `search-${Date.now()}`,
111 | object: 'list',
112 | data: searchResults,
113 | created: Math.floor(Date.now() / 1000),
114 | metadata: {
115 | total_models: models.data.length,
116 | filtered_count: searchResults.length,
117 | applied_filters: {
118 | query: args.query,
119 | provider: args.provider,
120 | minContextLength: args.minContextLength,
121 | maxContextLength: args.maxContextLength,
122 | maxPromptPrice: args.maxPromptPrice,
123 | maxCompletionPrice: args.maxCompletionPrice,
124 | capabilities: args.capabilities,
125 | limit: args.limit
126 | }
127 | }
128 | };
129 |
130 | // Add isError: false to successful return
131 | return {
132 | isError: false,
133 | content: [
134 | {
135 | type: 'text',
136 | text: JSON.stringify(response, null, 2),
137 | },
138 | ],
139 | };
140 | } catch (error) {
141 | console.error('Error during model search:', error); // Log the error
142 | // Handle known and unknown errors, always return ToolResult
143 | if (error instanceof Error) {
144 | return {
145 | isError: true,
146 | content: [
147 | {
148 | type: 'text',
149 | // Add "Error: " prefix
150 | text: `Error: Failed to search models: ${error.message}`,
151 | },
152 | ],
153 | };
154 | } else {
155 | // Handle unknown errors
156 | return {
157 | isError: true,
158 | content: [
159 | {
160 | type: 'text',
161 | text: 'Error: An unknown error occurred during model search.',
162 | },
163 | ],
164 | };
165 | }
166 | // DO NOT throw error;
167 | }
168 | }
--------------------------------------------------------------------------------
/src/tool-handlers/validate-model.ts:
--------------------------------------------------------------------------------
1 | import { ModelCache } from '../model-cache.js';
2 | import { ToolResult } from '../types.js'; // Import the unified type
3 |
4 | export interface ValidateModelToolRequest {
5 | model: string;
6 | }
7 |
8 | // Update function signature to return Promise
9 | export async function handleValidateModel(
10 | request: { params: { arguments: ValidateModelToolRequest } },
11 | modelCache: ModelCache
12 | ): Promise {
13 | const { model } = request.params.arguments;
14 |
15 | // Wrap core logic in try...catch
16 | try {
17 | const isValid = await modelCache.validateModel(model);
18 |
19 | // Modify return logic based on validity
20 | if (isValid) {
21 | // Return success ToolResult
22 | return {
23 | isError: false,
24 | content: [
25 | {
26 | type: 'text',
27 | // Keep simple JSON for valid response
28 | text: JSON.stringify({ model: model, valid: true }, null, 2),
29 | },
30 | ],
31 | };
32 | } else {
33 | // Return error ToolResult for invalid model
34 | return {
35 | isError: true,
36 | content: [
37 | {
38 | type: 'text',
39 | // Use simple error string
40 | text: `Error: Model not found: ${model}`,
41 | },
42 | ],
43 | };
44 | }
45 | } catch (error) {
46 | // Catch errors during model validation
47 | console.error(`Error validating model ${model}:`, error);
48 | const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
49 | return {
50 | isError: true,
51 | content: [
52 | {
53 | type: 'text',
54 | text: `Error: Failed to validate model: ${errorMessage}`,
55 | },
56 | ],
57 | };
58 | }
59 | }
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Represents a single content item in a tool response.
3 | * Currently, only text content is supported.
4 | */
5 | export interface ResponseContentItem {
6 | type: "text";
7 | text: string;
8 | }
9 |
10 | /**
11 | * Unified structure for all tool handler responses.
12 | * Follows the principles outlined in the refactoring plan.
13 | */
14 | export interface ToolResult {
15 | /** Indicates whether the tool execution resulted in an error. */
16 | isError: boolean;
17 | /** An array of content items, typically containing a single text item with the result or error message. */
18 | content: ResponseContentItem[];
19 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "NodeNext",
5 | "moduleResolution": "NodeNext",
6 | "outDir": "./dist",
7 | "rootDir": "./src",
8 | "strict": true,
9 | "esModuleInterop": true,
10 | "skipLibCheck": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "declaration": true,
13 | },
14 | "include": ["src/**/*"],
15 | "exclude": ["node_modules", "dist"]
16 | }
17 |
--------------------------------------------------------------------------------