├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src └── index.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | *.log 4 | .env* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Joe Wilson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Perplexity MCP Server 2 | 3 | A Model Context Protocol (MCP) server that provides intelligent code analysis and debugging capabilities using Perplexity AI's API. Works seamlessly with the Claude desktop client. 4 | 5 | Perplexity Server MCP server 6 | 7 | ## Features 8 | 9 | - **Intelligent Error Analysis**: Detailed breakdown of coding errors with root cause analysis 10 | - **Pattern Detection**: Automatically recognizes common error patterns and provides targeted solutions 11 | - **Comprehensive Solutions**: Step-by-step fixes with multiple implementation alternatives 12 | - **Best Practices**: Includes coding standards and error prevention tips 13 | - **Python Support**: Specialized handling of Python type errors and common coding issues 14 | 15 | ## Example Usage 16 | 17 | Ask questions like: 18 | - "Fix this TypeError in my Python code" 19 | - "What's causing this error message?" 20 | - "How do I fix this code?" 21 | 22 | Include your code snippet for targeted analysis: 23 | 24 | ```python 25 | def calculate_total(items): 26 | total = 0 27 | for item in items: 28 | total = total + item['price'] # TypeError: string + int 29 | 30 | data = [ 31 | {'name': 'Book', 'price': '10'}, 32 | {'name': 'Pen', 'price': '2'} 33 | ] 34 | 35 | result = calculate_total(data) 36 | ``` 37 | 38 | The server will provide: 39 | 1. Root cause analysis of the error 40 | 2. Step-by-step solution with code examples 41 | 3. Best practices to prevent similar issues 42 | 4. Alternative implementation approaches 43 | 44 | ## Installation 45 | 46 | ### Prerequisites 47 | - Node.js 18 or higher 48 | - A Perplexity AI API key 49 | 50 | ### Option 1: Install from npm (Recommended) 51 | 52 | ```bash 53 | # Using npm 54 | npm install -g perplexity-mcp 55 | 56 | # Or using the repository directly 57 | npm install -g git+https://github.com/yourusername/perplexity-mcp.git 58 | ``` 59 | 60 | ### Option 2: Install from Source 61 | 62 | 1. Clone the repository: 63 | ```bash 64 | git clone https://github.com/yourusername/perplexity-server.git 65 | cd perplexity-server 66 | ``` 67 | 68 | 2. Install dependencies: 69 | ```bash 70 | npm install 71 | ``` 72 | 73 | 3. Build and install globally: 74 | ```bash 75 | npm run build 76 | npm install -g . 77 | ``` 78 | 79 | ### Configure Claude Desktop 80 | 81 | 82 | Add to your Claude desktop configuration file: 83 | 84 | **MacOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` 85 | **Windows**: `%APPDATA%/Claude/claude_desktop_config.json` 86 | 87 | ```json 88 | { 89 | "mcpServers": { 90 | "perplexity": { 91 | "command": "perplexity-mcp", 92 | "args": [], 93 | "env": { 94 | "PERPLEXITY_API_KEY": "your-api-key-here" 95 | } 96 | } 97 | } 98 | } 99 | ``` 100 | 101 | Or if installed from source: 102 | 103 | ```json 104 | { 105 | "mcpServers": { 106 | "perplexity": { 107 | "command": "node", 108 | "args": ["/absolute/path/to/perplexity-server/build/index.js"], 109 | "env": { 110 | "PERPLEXITY_API_KEY": "your-api-key-here" 111 | } 112 | } 113 | } 114 | } 115 | ``` 116 | 117 | ## Security 118 | 119 | - The API key is stored securely in Claude's desktop configuration file 120 | - The key is passed to the server as an environment variable 121 | - No sensitive data is stored in the repository 122 | - The server expects the API key to be provided by Claude's environment 123 | 124 | ## Development 125 | 126 | ### Project Structure 127 | 128 | ``` 129 | perplexity-server/ 130 | ├── src/ 131 | │ └── index.ts # Main server implementation 132 | ├── package.json # Project configuration 133 | └── tsconfig.json # TypeScript configuration 134 | ``` 135 | 136 | ### Available Scripts 137 | 138 | - `npm run build`: Build the project 139 | - `npm run watch`: Watch for changes and rebuild automatically 140 | - `npm run prepare`: Prepare the package for publishing 141 | - `npm run inspector`: Run the MCP inspector for debugging 142 | 143 | ### Contributing 144 | 145 | 1. Fork the repository 146 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 147 | 3. Commit your changes (`git commit -m 'Add some amazing feature'`) 148 | 4. Push to the branch (`git push origin feature/amazing-feature`) 149 | 5. Open a Pull Request 150 | 151 | ## License 152 | 153 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 154 | 155 | ## Acknowledgments 156 | 157 | - Thanks to [Perplexity AI](https://www.perplexity.ai/) for their powerful API 158 | - Built with [Model Context Protocol](https://github.com/anthropics/model-context-protocol) 159 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "perplexity-server", 3 | "version": "0.1.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "perplexity-server", 9 | "version": "0.1.0", 10 | "dependencies": { 11 | "@modelcontextprotocol/sdk": "0.6.0", 12 | "axios": "^1.7.9" 13 | }, 14 | "bin": { 15 | "perplexity-server": "build/index.js" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^20.11.24", 19 | "typescript": "^5.3.3" 20 | } 21 | }, 22 | "node_modules/@modelcontextprotocol/sdk": { 23 | "version": "0.6.0", 24 | "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-0.6.0.tgz", 25 | "integrity": "sha512-9rsDudGhDtMbvxohPoMMyAUOmEzQsOK+XFchh6gZGqo8sx9sBuZQs+CUttXqa8RZXKDaJRCN2tUtgGof7jRkkw==", 26 | "dependencies": { 27 | "content-type": "^1.0.5", 28 | "raw-body": "^3.0.0", 29 | "zod": "^3.23.8" 30 | } 31 | }, 32 | "node_modules/@types/node": { 33 | "version": "20.17.10", 34 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.10.tgz", 35 | "integrity": "sha512-/jrvh5h6NXhEauFFexRin69nA0uHJ5gwk4iDivp/DeoEua3uwCUto6PC86IpRITBOs4+6i2I56K5x5b6WYGXHA==", 36 | "dev": true, 37 | "dependencies": { 38 | "undici-types": "~6.19.2" 39 | } 40 | }, 41 | "node_modules/asynckit": { 42 | "version": "0.4.0", 43 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 44 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" 45 | }, 46 | "node_modules/axios": { 47 | "version": "1.7.9", 48 | "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", 49 | "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", 50 | "dependencies": { 51 | "follow-redirects": "^1.15.6", 52 | "form-data": "^4.0.0", 53 | "proxy-from-env": "^1.1.0" 54 | } 55 | }, 56 | "node_modules/bytes": { 57 | "version": "3.1.2", 58 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 59 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 60 | "engines": { 61 | "node": ">= 0.8" 62 | } 63 | }, 64 | "node_modules/combined-stream": { 65 | "version": "1.0.8", 66 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 67 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 68 | "dependencies": { 69 | "delayed-stream": "~1.0.0" 70 | }, 71 | "engines": { 72 | "node": ">= 0.8" 73 | } 74 | }, 75 | "node_modules/content-type": { 76 | "version": "1.0.5", 77 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 78 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 79 | "engines": { 80 | "node": ">= 0.6" 81 | } 82 | }, 83 | "node_modules/delayed-stream": { 84 | "version": "1.0.0", 85 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 86 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 87 | "engines": { 88 | "node": ">=0.4.0" 89 | } 90 | }, 91 | "node_modules/depd": { 92 | "version": "2.0.0", 93 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 94 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 95 | "engines": { 96 | "node": ">= 0.8" 97 | } 98 | }, 99 | "node_modules/follow-redirects": { 100 | "version": "1.15.9", 101 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", 102 | "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", 103 | "funding": [ 104 | { 105 | "type": "individual", 106 | "url": "https://github.com/sponsors/RubenVerborgh" 107 | } 108 | ], 109 | "engines": { 110 | "node": ">=4.0" 111 | }, 112 | "peerDependenciesMeta": { 113 | "debug": { 114 | "optional": true 115 | } 116 | } 117 | }, 118 | "node_modules/form-data": { 119 | "version": "4.0.1", 120 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", 121 | "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", 122 | "dependencies": { 123 | "asynckit": "^0.4.0", 124 | "combined-stream": "^1.0.8", 125 | "mime-types": "^2.1.12" 126 | }, 127 | "engines": { 128 | "node": ">= 6" 129 | } 130 | }, 131 | "node_modules/http-errors": { 132 | "version": "2.0.0", 133 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 134 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 135 | "dependencies": { 136 | "depd": "2.0.0", 137 | "inherits": "2.0.4", 138 | "setprototypeof": "1.2.0", 139 | "statuses": "2.0.1", 140 | "toidentifier": "1.0.1" 141 | }, 142 | "engines": { 143 | "node": ">= 0.8" 144 | } 145 | }, 146 | "node_modules/iconv-lite": { 147 | "version": "0.6.3", 148 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 149 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 150 | "dependencies": { 151 | "safer-buffer": ">= 2.1.2 < 3.0.0" 152 | }, 153 | "engines": { 154 | "node": ">=0.10.0" 155 | } 156 | }, 157 | "node_modules/inherits": { 158 | "version": "2.0.4", 159 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 160 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 161 | }, 162 | "node_modules/mime-db": { 163 | "version": "1.52.0", 164 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 165 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 166 | "engines": { 167 | "node": ">= 0.6" 168 | } 169 | }, 170 | "node_modules/mime-types": { 171 | "version": "2.1.35", 172 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 173 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 174 | "dependencies": { 175 | "mime-db": "1.52.0" 176 | }, 177 | "engines": { 178 | "node": ">= 0.6" 179 | } 180 | }, 181 | "node_modules/proxy-from-env": { 182 | "version": "1.1.0", 183 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 184 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" 185 | }, 186 | "node_modules/raw-body": { 187 | "version": "3.0.0", 188 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", 189 | "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", 190 | "dependencies": { 191 | "bytes": "3.1.2", 192 | "http-errors": "2.0.0", 193 | "iconv-lite": "0.6.3", 194 | "unpipe": "1.0.0" 195 | }, 196 | "engines": { 197 | "node": ">= 0.8" 198 | } 199 | }, 200 | "node_modules/safer-buffer": { 201 | "version": "2.1.2", 202 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 203 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 204 | }, 205 | "node_modules/setprototypeof": { 206 | "version": "1.2.0", 207 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 208 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 209 | }, 210 | "node_modules/statuses": { 211 | "version": "2.0.1", 212 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 213 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 214 | "engines": { 215 | "node": ">= 0.8" 216 | } 217 | }, 218 | "node_modules/toidentifier": { 219 | "version": "1.0.1", 220 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 221 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 222 | "engines": { 223 | "node": ">=0.6" 224 | } 225 | }, 226 | "node_modules/typescript": { 227 | "version": "5.7.2", 228 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", 229 | "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", 230 | "dev": true, 231 | "bin": { 232 | "tsc": "bin/tsc", 233 | "tsserver": "bin/tsserver" 234 | }, 235 | "engines": { 236 | "node": ">=14.17" 237 | } 238 | }, 239 | "node_modules/undici-types": { 240 | "version": "6.19.8", 241 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", 242 | "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", 243 | "dev": true 244 | }, 245 | "node_modules/unpipe": { 246 | "version": "1.0.0", 247 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 248 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 249 | "engines": { 250 | "node": ">= 0.8" 251 | } 252 | }, 253 | "node_modules/zod": { 254 | "version": "3.24.1", 255 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", 256 | "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", 257 | "funding": { 258 | "url": "https://github.com/sponsors/colinhacks" 259 | } 260 | } 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "perplexity-mcp", 3 | "version": "0.1.0", 4 | "description": "MCP server that integrates with Perplexity AI for intelligent coding assistance", 5 | "type": "module", 6 | "bin": { 7 | "perplexity-mcp": "./build/index.js" 8 | }, 9 | "files": [ 10 | "build" 11 | ], 12 | "scripts": { 13 | "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"", 14 | "prepare": "npm run build", 15 | "watch": "tsc --watch", 16 | "inspector": "npx @modelcontextprotocol/inspector build/index.js" 17 | }, 18 | "dependencies": { 19 | "@modelcontextprotocol/sdk": "0.6.0", 20 | "axios": "^1.7.9" 21 | }, 22 | "devDependencies": { 23 | "@types/node": "^20.11.24", 24 | "typescript": "^5.3.3" 25 | }, 26 | "keywords": [ 27 | "mcp", 28 | "perplexity", 29 | "ai", 30 | "coding", 31 | "assistance", 32 | "claude", 33 | "anthropic" 34 | ], 35 | "author": "Joe Wilson", 36 | "license": "MIT", 37 | "repository": { 38 | "type": "git", 39 | "url": "git+https://github.com/yourusername/perplexity-mcp.git" 40 | }, 41 | "bugs": { 42 | "url": "https://github.com/yourusername/perplexity-mcp/issues" 43 | }, 44 | "homepage": "https://github.com/yourusername/perplexity-mcp#readme", 45 | "engines": { 46 | "node": ">=18" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /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 | import { 5 | CallToolRequestSchema, 6 | ErrorCode, 7 | ListToolsRequestSchema, 8 | McpError, 9 | } from '@modelcontextprotocol/sdk/types.js'; 10 | import axios from 'axios'; 11 | 12 | const API_KEY = process.env.PERPLEXITY_API_KEY; 13 | if (!API_KEY) { 14 | throw new Error('PERPLEXITY_API_KEY environment variable is required'); 15 | } 16 | 17 | interface PerplexityResponse { 18 | id: string; 19 | model: string; 20 | created: number; 21 | choices: Array<{ 22 | index: number; 23 | finish_reason: string; 24 | message: { 25 | role: string; 26 | content: string; 27 | }; 28 | }>; 29 | } 30 | 31 | interface ServerConfig { 32 | name: string; 33 | version: string; 34 | capabilities: { 35 | tools: Record; 36 | }; 37 | } 38 | 39 | interface AxiosConfig { 40 | baseURL: string; 41 | headers: { 42 | Authorization: string; 43 | 'Content-Type': string; 44 | }; 45 | } 46 | 47 | interface CodeAnalysis { 48 | fixed: string; 49 | alternatives: string; 50 | } 51 | 52 | interface CustomAnalysis { 53 | text: string; 54 | } 55 | 56 | class PerplexityServer { 57 | private readonly server: Server; 58 | private readonly axiosInstance: ReturnType; 59 | private static readonly DEFAULT_CONFIG: ServerConfig = { 60 | name: 'perplexity-server', 61 | version: '0.1.0', 62 | capabilities: { 63 | tools: {}, 64 | }, 65 | }; 66 | 67 | private static readonly DEFAULT_AXIOS_CONFIG: AxiosConfig = { 68 | baseURL: 'https://api.perplexity.ai', 69 | headers: { 70 | 'Authorization': `Bearer ${API_KEY}`, 71 | 'Content-Type': 'application/json', 72 | }, 73 | }; 74 | 75 | constructor(config: Partial = {}) { 76 | this.server = new Server( 77 | { 78 | ...PerplexityServer.DEFAULT_CONFIG, 79 | ...config, 80 | }, 81 | { 82 | capabilities: { 83 | tools: {}, 84 | }, 85 | } 86 | ); 87 | 88 | this.axiosInstance = axios.create({ 89 | ...PerplexityServer.DEFAULT_AXIOS_CONFIG, 90 | baseURL: 'https://api.perplexity.ai', 91 | headers: { 92 | 'Authorization': `Bearer ${API_KEY}`, 93 | 'Content-Type': 'application/json', 94 | }, 95 | }); 96 | 97 | this.setupToolHandlers(); 98 | this.setupErrorHandling(); 99 | this.setupProcessHandlers(); 100 | } 101 | 102 | private setupErrorHandling(): void { 103 | this.server.onerror = (error: Error): void => { 104 | console.error('[MCP Error]', error); 105 | }; 106 | } 107 | 108 | private setupProcessHandlers(): void { 109 | process.on('SIGINT', async (): Promise => { 110 | await this.server.close(); 111 | process.exit(0); 112 | }); 113 | } 114 | 115 | private setupToolHandlers(): void { 116 | this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ 117 | tools: [ 118 | { 119 | name: 'search', 120 | description: 'Search Perplexity for coding help', 121 | inputSchema: { 122 | type: 'object', 123 | properties: { 124 | query: { 125 | type: 'string', 126 | description: 'The error or coding question to analyze', 127 | }, 128 | code: { 129 | type: 'string', 130 | description: 'Code snippet to analyze (optional)', 131 | }, 132 | language: { 133 | type: 'string', 134 | description: 'Programming language of the code snippet (optional)', 135 | default: 'auto' 136 | } 137 | }, 138 | required: ['query'], 139 | }, 140 | }, 141 | ], 142 | })); 143 | 144 | this.server.setRequestHandler(CallToolRequestSchema, async (request) => { 145 | if (request.params.name !== 'search') { 146 | throw new McpError( 147 | ErrorCode.MethodNotFound, 148 | `Unknown tool: ${request.params.name}` 149 | ); 150 | } 151 | 152 | const { query, code, language = 'auto' } = request.params.arguments as { 153 | query: string; 154 | code?: string; 155 | language?: string; 156 | }; 157 | 158 | // Format code block if provided 159 | const codeBlock = code ? ` 160 | Code to analyze: 161 | \`\`\`${language} 162 | ${code} 163 | \`\`\` 164 | ` : ''; 165 | 166 | try { 167 | // Check for custom analysis first 168 | let codeAnalysis: CodeAnalysis | null = null; 169 | if (code) { 170 | codeAnalysis = analyzeCode(code); 171 | if (codeAnalysis) { 172 | const customAnalysis: CustomAnalysis = { 173 | text: `1. Root Cause Analysis 174 | ---------------- 175 | • Technical Cause: Python is strongly typed and does not allow operations between incompatible types (string and integer) 176 | • Common Scenarios: Dictionary values from external sources (like JSON, CSV, or user input) often store numbers as strings 177 | • Technical Background: Python dictionary values maintain their original types, requiring explicit conversion for numeric operations 178 | 179 | 2. Step-by-Step Solution 180 | ---------------- 181 | • Step 1: Identify the data type issue 182 | The 'price' values in the dictionary are strings ('10' and '2') but used in numeric addition 183 | • Step 2: Add type conversion 184 | Use int() to convert item['price'] to integer before adding to total 185 | • Step 3: Add error handling 186 | Wrap the conversion in try-except to handle invalid price values 187 | • Step 4: Test the solution 188 | Verify the total is calculated correctly (12 = 10 + 2) 189 | 190 | 3. Best Practices for Prevention 191 | ---------------- 192 | • Design Pattern: Data validation and type conversion at input boundaries 193 | • Code Organization: Convert data types when reading from external sources, maintain consistent types in data structures 194 | • Common Pitfalls: Assuming dictionary values have the correct type, missing error handling for invalid values 195 | • Error Handling: Use try-except blocks to handle type conversion errors, validate data before operations 196 | 197 | 4. Code Examples 198 | ---------------- 199 | Before: 200 | \`\`\`${language} 201 | ${code} 202 | \`\`\` 203 | 204 | After: 205 | \`\`\`${language} 206 | ${codeAnalysis.fixed} 207 | \`\`\` 208 | 209 | Alternative Approaches: 210 | \`\`\`${language} 211 | ${codeAnalysis.alternatives} 212 | \`\`\`` 213 | }; 214 | return { 215 | content: [ 216 | { 217 | type: 'text', 218 | text: customAnalysis.text, 219 | }, 220 | ], 221 | }; 222 | } 223 | } 224 | 225 | const prompt = `As an expert software developer, analyze this coding question and provide a comprehensive solution. 226 | 227 | CRITICAL FORMATTING INSTRUCTIONS: 228 | 1. Use the EXACT section headers and bullet points provided 229 | 2. Keep all bullet points and section markers exactly as shown 230 | 3. Replace only the text in [brackets] with your analysis 231 | 4. Do not add any additional sections or bullet points 232 | 5. Do not modify the formatting or structure in any way 233 | 6. Start each section with the exact numbered header and dashed line shown 234 | 235 | QUERY TO ANALYZE: 236 | ${query} 237 | ${codeBlock} 238 | 239 | 1. Root Cause Analysis 240 | ---------------- 241 | • Technical Cause: [Explain the fundamental technical reason for the error] 242 | • Common Scenarios: [List typical situations where this error occurs] 243 | • Technical Background: [Provide relevant language/framework context] 244 | 245 | 2. Step-by-Step Solution 246 | ---------------- 247 | • Step 1: [First step with clear explanation] 248 | [Code snippet if applicable] 249 | • Step 2: [Second step with clear explanation] 250 | [Code snippet if applicable] 251 | • Step 3: [Third step with clear explanation] 252 | [Code snippet if applicable] 253 | • Step 4: [Final verification step] 254 | [Working code demonstration] 255 | 256 | 3. Best Practices for Prevention 257 | ---------------- 258 | • Design Pattern: [Recommended pattern to prevent this issue] 259 | • Code Organization: [How to structure code to avoid this] 260 | • Common Pitfalls: [Specific mistakes to watch for] 261 | • Error Handling: [How to properly handle edge cases] 262 | 263 | 4. Code Examples 264 | ---------------- 265 | Before: 266 | \`\`\`python 267 | [Code that causes the error] 268 | \`\`\` 269 | 270 | After: 271 | \`\`\`python 272 | [Fixed version of the code] 273 | \`\`\` 274 | 275 | Alternative Approaches: 276 | \`\`\`python 277 | [Other valid solutions] 278 | \`\`\``; 279 | 280 | const response = await this.axiosInstance.post('/chat/completions', { 281 | model: 'llama-3.1-sonar-huge-128k-online', 282 | messages: [ 283 | { 284 | role: 'system', 285 | content: 'You are an expert software developer focused on debugging and solving coding problems. Always structure your responses exactly as requested.', 286 | }, 287 | { 288 | role: 'user', 289 | content: prompt, 290 | }, 291 | ], 292 | }); 293 | 294 | let analysis = response.data.choices[0]?.message?.content; 295 | if (!analysis) { 296 | throw new Error('No analysis received from Perplexity'); 297 | } 298 | 299 | // Helper functions for code analysis 300 | function analyzeCode(sourceCode: string | undefined): CodeAnalysis | null { 301 | if (!sourceCode) return null; 302 | 303 | // Dictionary string value case 304 | const isDictionaryPricePattern = sourceCode.includes("item['price']") || sourceCode.includes('item["price"]') || ( 305 | sourceCode.includes('price') && 306 | sourceCode.includes('item[') && 307 | sourceCode.includes('for') && 308 | sourceCode.includes('in') && 309 | sourceCode.includes('total') && 310 | sourceCode.includes('def calculate_total') && 311 | sourceCode.includes('TypeError') 312 | ); 313 | if (isDictionaryPricePattern) { 314 | const totalVar = 'total'; 315 | return { 316 | fixed: `def calculate_total(items): 317 | ${totalVar} = 0 318 | for item in items: 319 | try: 320 | ${totalVar} = ${totalVar} + int(item['price']) # Convert string to integer before adding 321 | except ValueError: 322 | raise ValueError(f"Invalid price value: {item['price']}") 323 | return ${totalVar} 324 | 325 | data = [ 326 | {'name': 'Book', 'price': '10'}, 327 | {'name': 'Pen', 'price': '2'} 328 | ] 329 | 330 | try: 331 | total = calculate_total(data) # Result will be 12 332 | print(f"Total: {totalVar}") 333 | except ValueError as e: 334 | print(f"Error: {e}")`, 335 | alternatives: `# Solution 1: Convert during dictionary creation 336 | data = [ 337 | {'name': 'Book', 'price': int('10')}, 338 | {'name': 'Pen', 'price': int('2')} 339 | ] 340 | 341 | def calculate_total(items): 342 | total = 0 343 | for item in items: 344 | total = total + item['price'] # No conversion needed 345 | return total 346 | 347 | # Solution 2: Use list comprehension with type conversion 348 | def calculate_total(items): 349 | return sum(int(item['price']) for item in items) 350 | 351 | # Solution 3: Use map and sum for functional approach 352 | def calculate_total(items): 353 | return sum(map(lambda x: int(x['price']), items)) 354 | 355 | # Solution 4: Use list comprehension with validation 356 | def calculate_total(items): 357 | try: 358 | total = sum(int(item['price']) for item in items) 359 | return total 360 | except ValueError as e: 361 | raise ValueError(f"Invalid price value found in items: {e}") 362 | except KeyError: 363 | raise KeyError("Missing 'price' key in one or more items") 364 | except Exception as e: 365 | raise Exception(f"Unexpected error calculating total: {e}") 366 | 367 | # Solution 5: Using dataclasses for better type safety 368 | from dataclasses import dataclass 369 | from typing import List 370 | 371 | @dataclass 372 | class Item: 373 | name: str 374 | price: int # Store price as integer to prevent type issues 375 | 376 | @classmethod 377 | def from_string_price(cls, name: str, price_str: str) -> 'Item': 378 | try: 379 | return cls(name=name, price=int(price_str)) 380 | except ValueError: 381 | raise ValueError(f"Invalid price value: {price_str}") 382 | 383 | def calculate_total(items: List[Item]) -> int: 384 | return sum(item.price for item in items) 385 | 386 | # Usage: 387 | items = [ 388 | Item.from_string_price('Book', '10'), 389 | Item.from_string_price('Pen', '2') 390 | ] 391 | total = calculate_total(items)` 392 | }; 393 | } 394 | 395 | // Simple string + int case 396 | if (sourceCode.includes('+') && /["'].*?\+.*?\d/.test(sourceCode)) { 397 | return { 398 | fixed: sourceCode.replace(/["'](\d+)["']\s*\+\s*(\d+)/, 'int("$1") + $2'), 399 | alternatives: `# Solution 1: Convert string to int 400 | num_str = "123" 401 | result = int(num_str) + 456 402 | 403 | # Solution 2: Use string formatting 404 | num_str = "123" 405 | result = f"{num_str}456" # For string concatenation 406 | 407 | # Solution 3: With error handling 408 | def safe_add(str_num, int_num): 409 | try: 410 | return int(str_num) + int_num 411 | except ValueError: 412 | raise ValueError("String must be a valid number")` 413 | }; 414 | } 415 | 416 | return null; 417 | } 418 | 419 | // Use provided code as the "before" example if available 420 | const beforeCode = code || extractCodeExample(analysis, 'incorrect', 'problematic', 'error'); 421 | 422 | // Generate solutions based on code analysis 423 | const afterCode = (codeAnalysis as CodeAnalysis | null)?.fixed || extractCodeExample(analysis, 'correct', 'fixed', 'solution'); 424 | const alternativeCode = (codeAnalysis as CodeAnalysis | null)?.alternatives || extractCodeExample(analysis, 'alternative', 'another', 'other'); 425 | 426 | // Generate response sections 427 | const rootCauseSection = formatSection('Root Cause Analysis', { 428 | 'Technical Cause': extractTechnicalCause(analysis, query), 429 | 'Common Scenarios': extractCommonScenarios(analysis), 430 | 'Technical Background': extractTechnicalBackground(analysis) 431 | }); 432 | 433 | const solutionSection = formatSection('Step-by-Step Solution', { 434 | 'Steps': extractSteps(analysis) 435 | }); 436 | 437 | const preventionSection = formatSection('Best Practices for Prevention', { 438 | 'Design Pattern': extractDesignPattern(analysis), 439 | 'Code Organization': extractCodeOrganization(analysis), 440 | 'Common Pitfalls': extractCommonPitfalls(analysis), 441 | 'Error Handling': extractErrorHandling(analysis) 442 | }); 443 | 444 | const examplesSection = formatCodeExamples(language, beforeCode, afterCode, alternativeCode); 445 | 446 | // Combine sections 447 | const structuredResponse = [ 448 | rootCauseSection, 449 | solutionSection, 450 | preventionSection, 451 | examplesSection 452 | ].join('\n\n'); 453 | 454 | // Helper function to format sections 455 | function formatSection(title: string, items: Record): string { 456 | const header = `${title}\n----------------`; 457 | const content = Object.entries(items) 458 | .map(([key, value]) => { 459 | if (key === 'Steps') return value; 460 | return `• ${key}: ${value}`; 461 | }) 462 | .join('\n'); 463 | return `${header}\n${content}`; 464 | } 465 | 466 | // Helper function to format code examples 467 | function formatCodeExamples(lang: string, before: string, after: string, alternatives: string): string { 468 | return `Code Examples 469 | ---------------- 470 | Before: 471 | \`\`\`${lang} 472 | ${before} 473 | \`\`\` 474 | 475 | After: 476 | \`\`\`${lang} 477 | ${after} 478 | \`\`\` 479 | 480 | Alternative Approaches: 481 | \`\`\`${lang} 482 | ${alternatives} 483 | \`\`\``; 484 | } 485 | 486 | // Helper functions to extract information from analysis 487 | function extractCodeExample(text: string, ...keywords: string[]): string { 488 | // Use string literal for code block markers to avoid escaping issues 489 | const codeBlockStart = '```'; 490 | const pattern = new RegExp(`(?:${keywords.join('|')}).*?${codeBlockStart}.*?\\n([\\s\\S]*?)${codeBlockStart}`, 'i'); 491 | const match = text.match(pattern); 492 | return match ? match[1].trim() : '[No code example provided]'; 493 | } 494 | 495 | function extractTechnicalCause(text: string, query: string): string { 496 | if (query.includes('TypeError')) { 497 | return 'Python is strongly typed and does not allow operations between incompatible types'; 498 | } 499 | const cause = text.match(/technical(?:\s+cause)?:?\s*([^•\n]+)/i); 500 | return cause ? cause[1].trim() : 'Unable to determine cause'; 501 | } 502 | 503 | function extractCommonScenarios(text: string): string { 504 | const scenarios = text.match(/common(?:\s+scenarios)?:?\s*([^•\n]+)/i); 505 | return scenarios ? scenarios[1].trim() : 'Various scenarios where type mismatches occur'; 506 | } 507 | 508 | function extractTechnicalBackground(text: string): string { 509 | const background = text.match(/(?:technical\s+)?background:?\s*([^•\n]+)/i); 510 | return background ? background[1].trim() : 'Language-specific type system requirements'; 511 | } 512 | 513 | function extractSteps(text: string): string { 514 | const steps = text.match(/step(?:\s+\d+)?:?\s*([^•\n]+)/gi); 515 | if (!steps) return '• Step 1: Identify the issue\n• Step 2: Apply the fix\n• Step 3: Test the solution'; 516 | return steps.map((step, i) => `• Step ${i + 1}: ${step.replace(/step\s+\d+:?\s*/i, '')}`).join('\n'); 517 | } 518 | 519 | function extractDesignPattern(text: string): string { 520 | const pattern = text.match(/(?:design\s+pattern|pattern):?\s*([^•\n]+)/i); 521 | return pattern ? pattern[1].trim() : 'Type validation and conversion patterns'; 522 | } 523 | 524 | function extractCodeOrganization(text: string): string { 525 | const org = text.match(/(?:code\s+organization|organize):?\s*([^•\n]+)/i); 526 | return org ? org[1].trim() : 'Separate data processing from business logic'; 527 | } 528 | 529 | function extractCommonPitfalls(text: string): string { 530 | const pitfalls = text.match(/(?:common\s+pitfalls|pitfalls):?\s*([^•\n]+)/i); 531 | return pitfalls ? pitfalls[1].trim() : 'Mixing types without proper validation'; 532 | } 533 | 534 | function extractErrorHandling(text: string): string { 535 | const handling = text.match(/(?:error\s+handling|handle):?\s*([^•\n]+)/i); 536 | return handling ? handling[1].trim() : 'Use try-catch blocks for type conversions'; 537 | } 538 | 539 | return { 540 | content: [ 541 | { 542 | type: 'text', 543 | text: structuredResponse, 544 | }, 545 | ], 546 | }; 547 | } catch (error) { 548 | if (axios.isAxiosError(error)) { 549 | return { 550 | content: [ 551 | { 552 | type: 'text', 553 | text: `Perplexity API error: ${error.response?.data?.error?.message || error.message}`, 554 | }, 555 | ], 556 | isError: true, 557 | }; 558 | } 559 | throw error; 560 | } 561 | }); 562 | } 563 | 564 | public async run(): Promise { 565 | const transport = new StdioServerTransport(); 566 | await this.server.connect(transport); 567 | console.error('Perplexity MCP server running on stdio'); 568 | } 569 | } 570 | 571 | async function main(): Promise { 572 | try { 573 | const server = new PerplexityServer(); 574 | await server.run(); 575 | } catch (error) { 576 | console.error('Failed to start server:', error); 577 | process.exit(1); 578 | } 579 | } 580 | 581 | main().catch((error) => { 582 | console.error('Unhandled error:', error); 583 | process.exit(1); 584 | }); 585 | -------------------------------------------------------------------------------- /tsconfig.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 | --------------------------------------------------------------------------------