├── .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 |
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 |
--------------------------------------------------------------------------------