├── .DS_Store ├── .github └── workflows │ └── release.yml ├── .gitignore ├── .python-version ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── README.md ├── pyproject.toml ├── smithery.yaml ├── src └── mcp_perplexity │ ├── __init__.py │ ├── database.py │ ├── perplexity_client.py │ ├── server.py │ ├── utils.py │ └── web │ ├── __init__.py │ ├── database_extension.py │ ├── routes.py │ └── templates │ ├── _chat_list.html │ ├── _dialog.html │ ├── _message_list.html │ ├── base.html │ ├── chat.html │ ├── error.html │ └── index.html ├── templates ├── .release_notes.md.j2 └── CHANGELOG.md.j2 ├── tests └── test_perplexity_api.py └── uv.lock /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniel-lxs/mcp-perplexity/e791b61a2f509710b7a7b73811d31c594efccf5f/.DS_Store -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release and Publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: write 10 | actions: write 11 | id-token: write 12 | 13 | jobs: 14 | release: 15 | runs-on: ubuntu-latest 16 | outputs: 17 | released: ${{ steps.release.outputs.released }} 18 | version: ${{ steps.release.outputs.version }} 19 | concurrency: release 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 0 25 | 26 | - name: Set up Python 27 | uses: actions/setup-python@v5 28 | with: 29 | python-version: "3.x" 30 | 31 | - name: Install dependencies 32 | run: | 33 | python -m pip install --upgrade pip 34 | python -m pip install build "python-semantic-release>=8.0.0,<9.0.0" 35 | 36 | - name: Build package 37 | run: python -m build 38 | 39 | - name: Python Semantic Release 40 | id: release 41 | env: 42 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | run: | 44 | git config --global user.name "github-actions" 45 | git config --global user.email "action@github.com" 46 | semantic-release version 47 | # Publish to GitHub with release notes 48 | semantic-release publish 49 | 50 | - name: Store the distribution packages 51 | uses: actions/upload-artifact@v4 52 | with: 53 | name: python-package-distributions 54 | path: dist/ 55 | retention-days: 5 56 | if-no-files-found: error 57 | 58 | publish: 59 | needs: [release] 60 | if: needs.release.outputs.released == 'true' 61 | runs-on: ubuntu-latest 62 | environment: 63 | name: pypi 64 | url: https://pypi.org/p/mcp-perplexity 65 | permissions: 66 | id-token: write 67 | 68 | steps: 69 | - name: Download built distributions 70 | uses: actions/download-artifact@v4 71 | with: 72 | name: python-package-distributions 73 | path: dist/ 74 | 75 | - name: Publish package distributions to PyPI 76 | uses: pypa/gh-action-pypi-publish@release/v1 77 | with: 78 | packages-dir: dist/ 79 | verbose: true 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info 8 | 9 | # Virtual environments 10 | .venv 11 | 12 | # Editor Config 13 | .vscode/* 14 | !.vscode/launch.json 15 | !.vscode/tasks.json 16 | !.vscode/extensions.json -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.10 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | 6 | ## v0.5.1 (2025-02-25) 7 | 8 | 9 | 10 | ### 🧹 Maintenance 11 | 12 | 13 | * Version bump ([`3f659f0`](https://github.com/daniel-lxs/mcp-perplexity/commit/3f659f08e23c0e91f3630c2bb6fdc2e7c3d31ed9)) 14 | 15 | 16 | 17 | 18 | ### 🐛 Bug Fixes 19 | 20 | 21 | * Improve database query and message handling in perplexity server ([`b4961f8`](https://github.com/daniel-lxs/mcp-perplexity/commit/b4961f85ca9ccee399d6cbcd9773c7a0f3583e9b)) 22 | 23 | * Refactor perplexity client message handling ([`b7664ea`](https://github.com/daniel-lxs/mcp-perplexity/commit/b7664ea9d47d0eacd52c8e904807bc06b31539a1)) 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ## v0.5.0 (2025-02-25) 32 | 33 | 34 | 35 | ### 🧹 Maintenance 36 | 37 | 38 | * Bump version to allow publishing to pypi ([`bbe089f`](https://github.com/daniel-lxs/mcp-perplexity/commit/bbe089f2036182682c2dd680cb3163f7e5aa19f7)) 39 | 40 | * Remove old installers ([`e3689d0`](https://github.com/daniel-lxs/mcp-perplexity/commit/e3689d0549bbb2ee311793cf362721b901119749)) 41 | 42 | * Increase default timeout for perplexity api client ([`724aae0`](https://github.com/daniel-lxs/mcp-perplexity/commit/724aae0f936b449a37a21e4abed0aea71c953380)) 43 | 44 | * Add pyinstaller spec and development dependencies ([`854132b`](https://github.com/daniel-lxs/mcp-perplexity/commit/854132bcfb31280e764571202d9b18007d9af778)) 45 | 46 | 47 | 48 | 49 | ### 🔄 Continuous Integration 50 | 51 | 52 | * Update semantic release configuration and templates ([`a9a5c3c`](https://github.com/daniel-lxs/mcp-perplexity/commit/a9a5c3ce4e57ddc870919047feffd3635512c91b)) 53 | 54 | * Remove debug step from semantic release workflow ([`e28cc72`](https://github.com/daniel-lxs/mcp-perplexity/commit/e28cc723af6c8b44b325a9ef5e31371ecb415aeb)) 55 | 56 | * Enhance semantic release and build configuration ([`f2fff6d`](https://github.com/daniel-lxs/mcp-perplexity/commit/f2fff6d3f842eca785adcce410a29d604e248038)) 57 | 58 | * Refactor release workflow with improved semantic release and publishing ([`fb80404`](https://github.com/daniel-lxs/mcp-perplexity/commit/fb8040418c3757f996c15db5ce64e7c9e30144a2)) 59 | 60 | * Add python setup and dependencies to release workflow ([`1ef0095`](https://github.com/daniel-lxs/mcp-perplexity/commit/1ef0095ea79f090d5856fb7079242fafcc80e9fa)) 61 | 62 | 63 | 64 | 65 | ### Feature 66 | 67 | 68 | * Improve package resource handling and configuration ([`6de24fd`](https://github.com/daniel-lxs/mcp-perplexity/commit/6de24fd1b0d7d0c31dc0c98e9abd5d13b5f2569b)) 69 | 70 | * Enhance markdown processing with interactive <think> block rendering ([`a9d05fd`](https://github.com/daniel-lxs/mcp-perplexity/commit/a9d05fdebabd9d5f7117777b65badbcc66805fab)) 71 | 72 | * Improve web application resource path handling and logging ([`35be31b`](https://github.com/daniel-lxs/mcp-perplexity/commit/35be31b11323a8858720dc8d71e879f37ddc76e0)) 73 | 74 | 75 | 76 | 77 | ### 🐛 Bug Fixes 78 | 79 | 80 | * Simplify release workflow by removing build and release asset jobs ([`6cfe5e0`](https://github.com/daniel-lxs/mcp-perplexity/commit/6cfe5e0779506d0aa81546cd6cf41aefbe655f87)) 81 | 82 | * Revert pyinstaller changes ([`1487b08`](https://github.com/daniel-lxs/mcp-perplexity/commit/1487b08c1c8044d07f7e4a01570567d2e7e46996)) 83 | 84 | * Enhance semantic release and changelog configuration ([`038854a`](https://github.com/daniel-lxs/mcp-perplexity/commit/038854a3f8f7d9af2958202bc75716cd47ff7991)) 85 | 86 | * Update release artifact file matching pattern ([`63e9881`](https://github.com/daniel-lxs/mcp-perplexity/commit/63e9881af17809feae933f553c43003ab49f6b61)) 87 | 88 | * Bump correctly ([`b017425`](https://github.com/daniel-lxs/mcp-perplexity/commit/b017425f46a60a10749def481650cc70fd52275a)) 89 | 90 | * Improve macos checksum generation in release workflow ([`8723563`](https://github.com/daniel-lxs/mcp-perplexity/commit/8723563d657f7a091eac33a4c02e47a350da01e3)) 91 | 92 | * Ci builds ubuntu and macos ([`53c51ea`](https://github.com/daniel-lxs/mcp-perplexity/commit/53c51eac37679ff7cea406c880da378625b94dd0)) 93 | 94 | * Simplify ci ([`4b45d24`](https://github.com/daniel-lxs/mcp-perplexity/commit/4b45d2449f59c90848d50a99a933d139fa38f056)) 95 | 96 | * Preserve citation order and duplicates in perplexity client ([`4e292fc`](https://github.com/daniel-lxs/mcp-perplexity/commit/4e292fcd28f6fbe265401abe699b8a388db4f17c)) 97 | 98 | 99 | 100 | 101 | ### 🔨 Code Refactoring 102 | 103 | 104 | * Update web ui environment variables and configuration ([`410d0b9`](https://github.com/daniel-lxs/mcp-perplexity/commit/410d0b981ed5b5563d5bc878722384e902296d50)) 105 | 106 | * Improve message rendering and typography in web templates ([`abe031c`](https://github.com/daniel-lxs/mcp-perplexity/commit/abe031c2654689e1c51f43483c5991739e4700dc)) 107 | 108 | * Streamline release workflow and build process ([`4fd2ca8`](https://github.com/daniel-lxs/mcp-perplexity/commit/4fd2ca86a9b37b5bbeaf1ee1644513d9f6f72d77)) 109 | 110 | * Optimize web ui styling and layout ([`8b2fcae`](https://github.com/daniel-lxs/mcp-perplexity/commit/8b2fcae212c574a5676539c26aa118202e5d5f32)) 111 | 112 | 113 | 114 | 115 | ### 💅 Code Style 116 | 117 | 118 | * Enhance interactive <think> block styling and rendering ([`dd2ccfa`](https://github.com/daniel-lxs/mcp-perplexity/commit/dd2ccfa484f955f86783c734195080a106c16509)) 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | ## v0.4.2 (2025-02-21) 127 | 128 | 129 | 130 | ### 🧹 Maintenance 131 | 132 | 133 | * Add mit license to project configuration ([`829ed82`](https://github.com/daniel-lxs/mcp-perplexity/commit/829ed825c847db4db4e94cb79c9f4e387dc69386)) 134 | 135 | * Migrate from hatch to setuptools for project build system ([`c73bb7a`](https://github.com/daniel-lxs/mcp-perplexity/commit/c73bb7a4b1c5018f862996bf6c62ec0c1ef334f9)) 136 | 137 | 138 | 139 | 140 | ### Documentation 141 | 142 | 143 | * Update name ([`d5c3f68`](https://github.com/daniel-lxs/mcp-perplexity/commit/d5c3f68b131177e621872d2b53961f14c0e7fe9d)) 144 | 145 | * Fix missing model on smithery command (ty @cartjacked) ([`abed600`](https://github.com/daniel-lxs/mcp-perplexity/commit/abed600a68f11b2348b1e186a8a0cf76430ed1e3)) 146 | 147 | * Update badge ([`ae5b456`](https://github.com/daniel-lxs/mcp-perplexity/commit/ae5b4564e21d38e3e14370f56acb1eb2aa63f843)) 148 | 149 | 150 | 151 | 152 | ### Feature 153 | 154 | 155 | * Enhance perplexity api client with model-specific configurations ([`78766b9`](https://github.com/daniel-lxs/mcp-perplexity/commit/78766b96f111261b2ca0dca44513fc530da51997)) 156 | 157 | * Implement perplexity api client and refactor server interactions ([`5c68f45`](https://github.com/daniel-lxs/mcp-perplexity/commit/5c68f455f3e3693bf4dbdda938457e19e9e04636)) 158 | 159 | 160 | 161 | 162 | ### 🐛 Bug Fixes 163 | 164 | 165 | * Increase api call timeout to 60 seconds ([`1df2a42`](https://github.com/daniel-lxs/mcp-perplexity/commit/1df2a42ba22336992201e293c7bc88c78ec4044a)) 166 | 167 | 168 | 169 | 170 | 171 | ## v0.4.1 (2025-02-18) 172 | 173 | 174 | 175 | ### 🧹 Maintenance 176 | 177 | 178 | * Consolidate pypi publish workflow into release workflow ([`989afdf`](https://github.com/daniel-lxs/mcp-perplexity/commit/989afdf8550524102496438bbe25b8b666e7b9b5)) 179 | 180 | * Enhance release and pypi publish workflows ([`f8ec70d`](https://github.com/daniel-lxs/mcp-perplexity/commit/f8ec70d83d7c487b0aaf969d3c4a25dc75b91532)) 181 | 182 | * Migrate from hatchling to setuptools build system ([`b2e507b`](https://github.com/daniel-lxs/mcp-perplexity/commit/b2e507b02fdc45ac854b86170735923f71af8cfd)) 183 | 184 | * Refactor pypi and release github actions workflows ([`a8350ef`](https://github.com/daniel-lxs/mcp-perplexity/commit/a8350ef14107159f03742574d62b970fcd9c73f9)) 185 | 186 | * Update github actions checkout configuration ([`3fbab5c`](https://github.com/daniel-lxs/mcp-perplexity/commit/3fbab5cd35b42b201c415690340bd51bcd62d559)) 187 | 188 | * Simplify release workflow and build configuration ([`7d320a0`](https://github.com/daniel-lxs/mcp-perplexity/commit/7d320a0400339fb7526b558c25bca9a363e98c15)) 189 | 190 | * Fix ci missing hatch ([`259bb18`](https://github.com/daniel-lxs/mcp-perplexity/commit/259bb182da9aeb519dc28bc88751c9e2f6a5d469)) 191 | 192 | * Update github actions release workflow configuration ([`5f16fc9`](https://github.com/daniel-lxs/mcp-perplexity/commit/5f16fc90c2e3fec2d770b15bd667d5d379769051)) 193 | 194 | * Bump version to 0.4.1 in pyproject.toml ([`7a15333`](https://github.com/daniel-lxs/mcp-perplexity/commit/7a15333842d5b2bd92f796642f1de384f1fe8f3a)) 195 | 196 | * Bump version to 0.4.1 ([`eea247d`](https://github.com/daniel-lxs/mcp-perplexity/commit/eea247db17bf9d1d2576c705e3fe3ffd1f7e7842)) 197 | 198 | * Bump version to 0.5.0 ([`2752793`](https://github.com/daniel-lxs/mcp-perplexity/commit/2752793e43d590c461243d08c0bc194ce4081751)) 199 | 200 | * Update semantic release configuration ([`7f48f97`](https://github.com/daniel-lxs/mcp-perplexity/commit/7f48f9796c2874b90e599d7fb4f116d898f49994)) 201 | 202 | 203 | 204 | 205 | ### Documentation 206 | 207 | 208 | * Add screenshots of webui to the readme file ([`441832c`](https://github.com/daniel-lxs/mcp-perplexity/commit/441832c89e32e0e08990534f0a692a5df54fc8a3)) 209 | 210 | * Revamp readme with comprehensive environment variables and web ui documentation ([`19f9fb8`](https://github.com/daniel-lxs/mcp-perplexity/commit/19f9fb8ca33361f51c76e105aecef208f1ff9e16)) 211 | 212 | * Update readme.md ([`d00badb`](https://github.com/daniel-lxs/mcp-perplexity/commit/d00badba16dd33a5c110c37b5b3255831db5d9ac)) 213 | 214 | * Add comprehensive contributing.md and update readme.md ([`8698330`](https://github.com/daniel-lxs/mcp-perplexity/commit/8698330c915fb5cf8a098d843f70bcd5017f2a4c)) 215 | 216 | 217 | 218 | 219 | ### Feature 220 | 221 | 222 | * Add port availability check and prevent multiple web ui instances ([`ac4c668`](https://github.com/daniel-lxs/mcp-perplexity/commit/ac4c66853d3ff0bf34122f59c28735412342c0fa)) 223 | 224 | * Enhance logging configuration with environment-based control ([`15b88f9`](https://github.com/daniel-lxs/mcp-perplexity/commit/15b88f908366d39e2749d7911608e4ce2838bc36)) 225 | 226 | * Add markdown rendering support for chat messages ([`249e807`](https://github.com/daniel-lxs/mcp-perplexity/commit/249e807d5796cb7bec8009d7cd73e5f3188eea67)) 227 | 228 | * Add chat deletion functionality and debug logging control ([`031a826`](https://github.com/daniel-lxs/mcp-perplexity/commit/031a82621014a9290ed0b7185ab1ac91e86550e8)) 229 | 230 | * Implement tokyo night dark theme for web interface ([`d8dfe25`](https://github.com/daniel-lxs/mcp-perplexity/commit/d8dfe250d2b6ac67e52e07dd453c2ce3b49c2c65)) 231 | 232 | * Enhance message storage with source citations ([`d19c71e`](https://github.com/daniel-lxs/mcp-perplexity/commit/d19c71e76b851fe0ad5476f2983c6b3b19c7d107)) 233 | 234 | * Add web interface for chat history management ([`a20fdb5`](https://github.com/daniel-lxs/mcp-perplexity/commit/a20fdb5494db3bec0050590f1bc3c2b53db23ef4)) 235 | 236 | 237 | 238 | 239 | ### 🐛 Bug Fixes 240 | 241 | 242 | * Title argument is no longer required as this argument is not necessary for existing chats ([`42180a5`](https://github.com/daniel-lxs/mcp-perplexity/commit/42180a5d9a984302b5301949d2147918d28e3999)) 243 | 244 | 245 | 246 | 247 | ### 🔨 Code Refactoring 248 | 249 | 250 | * Improve ui layout and spacing in chat list and base templates ([`3461ecf`](https://github.com/daniel-lxs/mcp-perplexity/commit/3461ecf7a956defd530191d316959c56b2f5cb87)) 251 | 252 | * Update error page styling with tokyo night theme colors ([`fd0514a`](https://github.com/daniel-lxs/mcp-perplexity/commit/fd0514ab6e58bf38437c1de498625920be691456)) 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | ## v0.4.0 (2025-02-14) 261 | 262 | 263 | 264 | ### 🧹 Maintenance 265 | 266 | 267 | * Update semantic release configuration and version tracking ([`7cf3678`](https://github.com/daniel-lxs/mcp-perplexity/commit/7cf3678ec71de3bfafc7b309d293b3fa8a628e36)) 268 | 269 | * Bump version to 0.4.0 for new chat functionality release ([`001f268`](https://github.com/daniel-lxs/mcp-perplexity/commit/001f268a4bc9305adc0845172b025deff686bc6f)) 270 | 271 | 272 | 273 | 274 | ### Feature 275 | 276 | 277 | * Add list and read chat functionality for perplexity conversations ([`8aeb0ff`](https://github.com/daniel-lxs/mcp-perplexity/commit/8aeb0ff32cb143c6dad82d9edfdbee71e8cd8ada)) 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | ## v0.3.4 (2025-02-13) 286 | 287 | 288 | 289 | ### 🧹 Maintenance 290 | 291 | 292 | * Bump project version to 0.3.4 ([`44c305b`](https://github.com/daniel-lxs/mcp-perplexity/commit/44c305be590893306143d56082bfb398212c2f44)) 293 | 294 | 295 | 296 | 297 | ### Documentation 298 | 299 | 300 | * Update readme.md ([`31c47e5`](https://github.com/daniel-lxs/mcp-perplexity/commit/31c47e5c15eb29ba2f062bee1820a94227b9b4ca)) 301 | 302 | 303 | 304 | 305 | ### 🐛 Bug Fixes 306 | 307 | 308 | * Simplify windows architecture detection in install script since the script is only available for amd64 ([`2ce8977`](https://github.com/daniel-lxs/mcp-perplexity/commit/2ce8977a4bd60d09d57d2bbe3bb5b96cd4474140)) 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | ## v0.3.3 (2025-02-11) 317 | 318 | 319 | 320 | ### 🧹 Maintenance 321 | 322 | 323 | * Improve installation scripts with enhanced error handling and user experience ([`0a55af5`](https://github.com/daniel-lxs/mcp-perplexity/commit/0a55af5b56588afb3ab015d0afb7dae08b13625c)) 324 | 325 | * Add cross-platform installation scripts for mcp-starter ([`729eb2a`](https://github.com/daniel-lxs/mcp-perplexity/commit/729eb2a3b7732d49db5780c50ec6f9fbe64b46b2)) 326 | 327 | * Bump project version to 0.3.2 ([`c269f1d`](https://github.com/daniel-lxs/mcp-perplexity/commit/c269f1d4ed1d332df22d4387ffc65fb29de60c26)) 328 | 329 | 330 | 331 | 332 | ### Documentation 333 | 334 | 335 | * Update windows and unix installation instructions ([`663d7c6`](https://github.com/daniel-lxs/mcp-perplexity/commit/663d7c6bbd6585f6c3e26b38eb077807a71f321c)) 336 | 337 | * Add usage section for ask_perplexity and chat_perplexity tools ([`f8037dc`](https://github.com/daniel-lxs/mcp-perplexity/commit/f8037dca9cbf4ea7ce013c34171067fe40958bda)) 338 | 339 | * Update readme with smithery cli usage and macos note ([`449f172`](https://github.com/daniel-lxs/mcp-perplexity/commit/449f1720598fa91bb53e586f0d40789ae406f54e)) 340 | 341 | 342 | 343 | 344 | ### 🐛 Bug Fixes 345 | 346 | 347 | * Update database path environment variable name to match the documentation ([`9ee1ceb`](https://github.com/daniel-lxs/mcp-perplexity/commit/9ee1ceb88259cb28fd3d0f2913f8ee3b8e281172)) 348 | 349 | 350 | 351 | 352 | 353 | ## v0.3.0 (2025-02-10) 354 | 355 | 356 | 357 | ### 🛠️ Build System 358 | 359 | 360 | * Update hatch build configuration for package sources ([`84db846`](https://github.com/daniel-lxs/mcp-perplexity/commit/84db8465993c7a69359f0fce0fac1e3aa0aa7c48)) 361 | 362 | 363 | 364 | 365 | ### 🧹 Maintenance 366 | 367 | 368 | * Bump project version to 0.3.0 ([`35c981b`](https://github.com/daniel-lxs/mcp-perplexity/commit/35c981b56e70648cc7b8021dd8b12ada567d18a8)) 369 | 370 | 371 | 372 | 373 | ### 🔄 Continuous Integration 374 | 375 | 376 | * Enable manual workflow dispatch for pypi package publishing ([`f8a48ad`](https://github.com/daniel-lxs/mcp-perplexity/commit/f8a48adbd504cf327e71243e7f77c499b199203f)) 377 | 378 | 379 | 380 | 381 | ### Documentation 382 | 383 | 384 | * Revamp readme with comprehensive installation and configuration guide ([`b8a5a86`](https://github.com/daniel-lxs/mcp-perplexity/commit/b8a5a86bac65fdd0d7e0127ec8aa2c6a4e77b2a8)) 385 | 386 | 387 | 388 | 389 | ### Feature 390 | 391 | 392 | * Add configuration options for perplexity model and chat database ([`1228278`](https://github.com/daniel-lxs/mcp-perplexity/commit/12282788e4f586e424eaaa1ca72fb9c8e9644085)) 393 | 394 | 395 | 396 | 397 | ### 🐛 Bug Fixes 398 | 399 | 400 | * Package naming and version ([`cf29a51`](https://github.com/daniel-lxs/mcp-perplexity/commit/cf29a511a02a6c3e3f33e346e6dcfbab87925355)) 401 | 402 | 403 | 404 | 405 | ### 🔨 Code Refactoring 406 | 407 | 408 | * Update mcp start command and add dockerfile configuration ([`56cebde`](https://github.com/daniel-lxs/mcp-perplexity/commit/56cebde6e35c2c04cf4cdfba6acf6494db4b7b28)) 409 | 410 | * Enhance dockerfile for development and runtime configuration ([`b06a8fa`](https://github.com/daniel-lxs/mcp-perplexity/commit/b06a8fa8f9c30e68e14a6174eb89481bcb4c0a5c)) 411 | 412 | * Optimize dockerfile and dependency management ([`c406e59`](https://github.com/daniel-lxs/mcp-perplexity/commit/c406e59a632e53a1a3d63f733dc6b472f5b97a5c)) 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | ## v0.2.1 (2025-02-06) 421 | 422 | 423 | 424 | ### 🧹 Maintenance 425 | 426 | 427 | * Bump project version to 0.2.1 ([`862ed70`](https://github.com/daniel-lxs/mcp-perplexity/commit/862ed708aab049ac212c6b55f92825247682df19)) 428 | 429 | * Update release workflow permissions configuration ([`d4e16f7`](https://github.com/daniel-lxs/mcp-perplexity/commit/d4e16f70506eb7d8c4498b42317f54ebe03ada73)) 430 | 431 | 432 | 433 | 434 | ### Documentation 435 | 436 | 437 | * Update readme with improved feature descriptions and model information ([`a92e563`](https://github.com/daniel-lxs/mcp-perplexity/commit/a92e563e1432d898e562dd88543aa665262cbf62)) 438 | 439 | * Update glama.ai server badge url in readme ([`1565bf0`](https://github.com/daniel-lxs/mcp-perplexity/commit/1565bf01bc3b36e469b6b1ba4548f3d46dd84852)) 440 | 441 | * Add pypi publish workflow badge to readme ([`ad4de00`](https://github.com/daniel-lxs/mcp-perplexity/commit/ad4de0026e8c096fddfb62ac12cf20057fa5c27f)) 442 | 443 | 444 | 445 | 446 | 447 | ## v0.2.0 (2025-02-06) 448 | 449 | 450 | 451 | ### 🧹 Maintenance 452 | 453 | 454 | * Update artifact upload configuration in release workflow ([`bea6336`](https://github.com/daniel-lxs/mcp-perplexity/commit/bea633666e6621b52d1b23c63d454d138f56c7a9)) 455 | 456 | * Optimize github actions release workflow for semantic release ([`700bb61`](https://github.com/daniel-lxs/mcp-perplexity/commit/700bb619d05df7bfc204873129954955a015bfda)) 457 | 458 | * Improve hatch installation and verification in github actions workflow ([`feebf4a`](https://github.com/daniel-lxs/mcp-perplexity/commit/feebf4a1c25924def776c76aefabc725601d41d6)) 459 | 460 | * Adjust hatch build and github actions configuration ([`096dd8c`](https://github.com/daniel-lxs/mcp-perplexity/commit/096dd8c8008ac8433f32c46da58aba5dfed43e8d)) 461 | 462 | * Update hatch github action to latest version ([`3bd663d`](https://github.com/daniel-lxs/mcp-perplexity/commit/3bd663de613d94e95bc956d110dbae1f1596e475)) 463 | 464 | * Refine release workflow hatch integration ([`91949e0`](https://github.com/daniel-lxs/mcp-perplexity/commit/91949e01adc2736aa351dce3d363b5927842c5c0)) 465 | 466 | * Update build and release workflow to use hatch ([`15468d5`](https://github.com/daniel-lxs/mcp-perplexity/commit/15468d58717757f509b532b86699aaa11772ffbb)) 467 | 468 | * Configure semantic release for automated versioning and github releases ([`6c0a6cc`](https://github.com/daniel-lxs/mcp-perplexity/commit/6c0a6ccaad58c4bee60d7fbf0fcfcfc49c39e2e7)) 469 | 470 | * Update vscode configuration in .gitignore ([`c33ac58`](https://github.com/daniel-lxs/mcp-perplexity/commit/c33ac58e00bc163fba8f682ffedd1f056d0614e6)) 471 | 472 | * Remove vscode python settings file ([`e956e37`](https://github.com/daniel-lxs/mcp-perplexity/commit/e956e37c45917b6c2a90b84d0a0471fa3486f846)) 473 | 474 | 475 | 476 | 477 | ### Documentation 478 | 479 | 480 | * Update mcp-server-starter reference to mcp-starter in readme ([`3462012`](https://github.com/daniel-lxs/mcp-perplexity/commit/34620126be7fc41a13adcddebd3ea8d78d2fe1cf)) 481 | 482 | 483 | 484 | 485 | ### Feature 486 | 487 | 488 | * Enhance database initialization with robust error handling and path support ([`42150e5`](https://github.com/daniel-lxs/mcp-perplexity/commit/42150e5139a7c5842023ce3365ac7023febe0a01)) 489 | 490 | 491 | 492 | 493 | ### 🐛 Bug Fixes 494 | 495 | 496 | * Lower the amount of numbers generated for chat ids ([`2dc5e01`](https://github.com/daniel-lxs/mcp-perplexity/commit/2dc5e013a152e4aed0d0b2706cd8d4b967691107)) 497 | 498 | * Remove invalid property from progress notification ([`51673ef`](https://github.com/daniel-lxs/mcp-perplexity/commit/51673efecd536a3161bbf3849fe4f22df34bc30f)) 499 | 500 | 501 | 502 | 503 | ### 🔨 Code Refactoring 504 | 505 | 506 | * Simplify system prompt and improve code formatting ([`3750cf6`](https://github.com/daniel-lxs/mcp-perplexity/commit/3750cf64526199783e312667123c662bb27c7d29)) 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | ## v0.1.2 (2025-02-05) 515 | 516 | 517 | 518 | ### 🧹 Maintenance 519 | 520 | 521 | * Bump version to 0.1.2 ([`db10e3f`](https://github.com/daniel-lxs/mcp-perplexity/commit/db10e3f739ee3101d2c0a5eb7c939dc5f5acac80)) 522 | 523 | 524 | 525 | 526 | ### Documentation 527 | 528 | 529 | * Update readme with new perplexity model configuration details ([`b19ff40`](https://github.com/daniel-lxs/mcp-perplexity/commit/b19ff4008d00383e9bd385952a547aaa4f950346)) 530 | 531 | 532 | 533 | 534 | ### Feature 535 | 536 | 537 | * Add configurable perplexity models for ask and chat tools ([`904cf82`](https://github.com/daniel-lxs/mcp-perplexity/commit/904cf82a13c8f9ed845bccc13d1f5173480f93ae)) 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | ## v0.1.1 (2025-02-05) 546 | 547 | 548 | 549 | ### 🧹 Maintenance 550 | 551 | 552 | * Bump version to 0.1.1 ([`99b8bc7`](https://github.com/daniel-lxs/mcp-perplexity/commit/99b8bc70f193071f701aa3ada4b5428aac625628)) 553 | 554 | * Make version match release ([`996739a`](https://github.com/daniel-lxs/mcp-perplexity/commit/996739ad688a247e23f26e7e2b405cba3ba8492e)) 555 | 556 | * Remove hirofumi tanigami from project authors ([`9c80a0a`](https://github.com/daniel-lxs/mcp-perplexity/commit/9c80a0acd8537efa4289fa2480d2f39b54c368c0)) 557 | 558 | * Add pypi publish github actions workflow ([`ca8e5dc`](https://github.com/daniel-lxs/mcp-perplexity/commit/ca8e5dcd48a95094e23b14e2f090429861f3f146)) 559 | 560 | * Rename package to mcp-perplexity ([`3c6fbe6`](https://github.com/daniel-lxs/mcp-perplexity/commit/3c6fbe6100f392a63351cef35e7ef8514a3acf36)) 561 | 562 | * Update project metadata and version ([`f8b98fd`](https://github.com/daniel-lxs/mcp-perplexity/commit/f8b98fdaa46f1a28aef3dcf8c57c12107a6fdc21)) 563 | 564 | 565 | 566 | 567 | ### Documentation 568 | 569 | 570 | * Update readme with comprehensive installation instructions for mcp server ([`3fd6c1a`](https://github.com/daniel-lxs/mcp-perplexity/commit/3fd6c1a5511585ae825a965f2583d524cb527137)) 571 | 572 | * Update mcp client configuration instructions in readme ([`8314f4a`](https://github.com/daniel-lxs/mcp-perplexity/commit/8314f4ae56bb6a3c9c856f2e1ad712bcfa064584)) 573 | 574 | * Update project repository and readme with new features and installation instructions ([`0570610`](https://github.com/daniel-lxs/mcp-perplexity/commit/05706108253589d977af0e72d2b0cbb1077262b7)) 575 | 576 | 577 | 578 | 579 | ### Feature 580 | 581 | 582 | * Add persistent chat functionality and database storage ([`a56c875`](https://github.com/daniel-lxs/mcp-perplexity/commit/a56c8753c04b2578527ebd7bfd1924bde1e1d73b)) 583 | 584 | * Enhance perplexity tool with streaming response and improved error handling ([`bdce645`](https://github.com/daniel-lxs/mcp-perplexity/commit/bdce64515fba45f614c60c3053bdb32cf30cdd32)) 585 | 586 | 587 | 588 | 589 | ### 🐛 Bug Fixes 590 | 591 | 592 | * Add note about timeout to readme ([`e332ed3`](https://github.com/daniel-lxs/mcp-perplexity/commit/e332ed322a9dd29b4edd44f1ca5bd61131405dce)) 593 | 594 | 595 | 596 | 597 | ### 🧪 Tests 598 | 599 | 600 | * Add comprehensive perplexity api test suite ([`c7a4327`](https://github.com/daniel-lxs/mcp-perplexity/commit/c7a43276d5b67c58f98539636fe0bba431c0e118)) 601 | 602 | 603 | 604 | 605 | 606 | 607 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to mcp-perplexity 2 | 3 | Thank you for your interest in contributing to the mcp-perplexity project! We welcome contributions from the community. Please take a moment to review this document for guidelines on how to contribute. 4 | 5 | ## Getting Started 6 | 7 | 1. **Fork the repository** on GitHub: [https://github.com/daniel-lxs/mcp-perplexity](https://github.com/daniel-lxs/mcp-perplexity) 8 | 2. **Clone your fork** locally: 9 | ```bash 10 | git clone https://github.com/YOUR_USERNAME/mcp-perplexity.git 11 | cd mcp-perplexity 12 | ``` 13 | 3. **Set up the development environment** using Hatch: 14 | ```bash 15 | pip install hatch 16 | hatch env create 17 | hatch shell 18 | ``` 19 | 20 | ## Making Changes 21 | 22 | 1. Create a new branch for your changes: 23 | ```bash 24 | git checkout -b my-feature-branch 25 | ``` 26 | 2. Make your changes and ensure they follow the project's coding style 27 | 3. Write tests for any new functionality 28 | 4. Verify all tests pass: 29 | ```bash 30 | hatch run test 31 | ``` 32 | 5. Commit your changes with a descriptive message following the [Conventional Commits](https://www.conventionalcommits.org/) format 33 | 34 | ## Submitting Changes 35 | 36 | 1. Push your branch to your fork: 37 | ```bash 38 | git push origin my-feature-branch 39 | ``` 40 | 2. Open a **Pull Request** against the `main` branch of the main repository 41 | 3. Provide a clear description of your changes in the PR, including: 42 | - The problem you're solving 43 | - Your solution approach 44 | - Any relevant screenshots or test results 45 | 46 | ## Code Style 47 | 48 | - Follow existing code patterns and style 49 | - Keep code clean and well-documented 50 | - Use type hints where appropriate 51 | - Write clear commit messages following Conventional Commits 52 | 53 | ## Reporting Issues 54 | 55 | If you find a bug or have a feature request, please open an issue on GitHub: 56 | [https://github.com/daniel-lxs/mcp-perplexity/issues](https://github.com/daniel-lxs/mcp-perplexity/issues) 57 | 58 | Include as much detail as possible, including: 59 | - Steps to reproduce the issue 60 | - Expected behavior 61 | - Actual behavior 62 | - Any relevant error messages or screenshots 63 | 64 | ## License 65 | 66 | By contributing to this project, you agree that your contributions will be licensed under the project's MIT License. -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile 2 | # Updated base image approach 3 | FROM python:3.10-bookworm AS builder 4 | 5 | WORKDIR /app 6 | 7 | # Install UV and create virtual environment 8 | RUN pip install uv==0.2.5 && \ 9 | uv venv -n .venv 10 | 11 | # Copy all necessary files first 12 | COPY pyproject.toml README.md ./ 13 | COPY src ./src 14 | 15 | # Generate lock file and install dependencies 16 | RUN --mount=type=cache,target=/root/.cache/uv \ 17 | . .venv/bin/activate && \ 18 | uv pip compile pyproject.toml -o uv.lock && \ 19 | uv pip install --no-deps -r uv.lock && \ 20 | uv pip install --no-deps -e . 21 | 22 | # Final stage 23 | FROM python:3.10-slim-bookworm 24 | WORKDIR /app 25 | 26 | # Copy virtual environment and source code from builder 27 | COPY --from=builder /app/.venv ./.venv 28 | COPY --from=builder /app/src ./src 29 | 30 | # Ensure scripts from .venv/bin are in PATH 31 | ENV PATH="/app/.venv/bin:$PATH" 32 | 33 | RUN apt-get update && apt-get install -y \ 34 | gcc \ 35 | python3-dev \ 36 | && rm -rf /var/lib/apt/lists/* 37 | 38 | # Verify package discovery 39 | RUN .venv/bin/python -c "import sys; print(sys.path)" 40 | 41 | # Ensure unbuffered stdio 42 | ENV PYTHONUNBUFFERED=1 43 | ENV PYTHONFAULTHANDLER=1 44 | 45 | # Add logging level control 46 | ENV LOG_LEVEL=INFO 47 | 48 | # Run as non-root user 49 | RUN useradd -m appuser && chown -R appuser /app 50 | USER appuser 51 | 52 | ENTRYPOINT ["mcp-perplexity"] 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Hirofumi Tanigami 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | include CHANGELOG.md 4 | include CONTRIBUTING.md 5 | 6 | recursive-include src/mcp_perplexity/web/templates *.html 7 | recursive-include src *.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Perplexity Chat MCP Server 2 | 3 | The Perplexity MCP Server provides a Python-based interface to the Perplexity API, offering tools for querying responses, maintaining chat history, and managing conversations. It supports model configuration via environment variables and stores chat data locally. Built with Python and setuptools, it's designed for integration with development environments. 4 | 5 | The MCP Server is desined to mimick how users interact with the Perplexity Chat on their browser by allowing your models to ask questions, continue conversations, and list all your chats. 6 | 7 | [![smithery badge](https://smithery.ai/badge/@daniel-lxs/mcp-perplexity)](https://smithery.ai/server/@daniel-lxs/mcp-perplexity) [![Release and Publish](https://github.com/daniel-lxs/mcp-perplexity/actions/workflows/release.yml/badge.svg)](https://github.com/daniel-lxs/mcp-perplexity/actions/workflows/release.yml) 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ## Components 16 | 17 | ### Tools 18 | 19 | - **ask_perplexity**: Request expert programming assistance through Perplexity. Focuses on coding solutions, error debugging, and technical explanations. Returns responses with source citations and alternative suggestions. 20 | - **chat_perplexity**: Maintains ongoing conversations with Perplexity AI. Creates new chats or continues existing ones with full history context. Returns chat ID for future continuation. 21 | - **list_chats_perplexity**: Lists all available chat conversations with Perplexity AI. Returns chat IDs, titles, and creation dates (displayed in relative time format, e.g., "5 minutes ago", "2 days ago"). Results are paginated with 50 chats per page. 22 | - **read_chat_perplexity**: Retrieves the complete conversation history for a specific chat. Returns the full chat history with all messages and their timestamps. No API calls are made to Perplexity - this only reads from local storage. 23 | 24 | ## Key Features 25 | 26 | - **Model Configuration via Environment Variable:** Allows you to specify the Perplexity model using the `PERPLEXITY_MODEL` environment variable for flexible model selection. 27 | 28 | You can also specify `PERPLEXITY_MODEL_ASK` and `PERPLEXITY_MODEL_CHAT` to use different models for the `ask_perplexity` and `chat_perplexity` tools, respectively. 29 | 30 | These will override `PERPLEXITY_MODEL`. You can check which models are available on the [Perplexity](https://docs.perplexity.ai/guides/model-cards) documentation. 31 | - **Persistent Chat History:** The `chat_perplexity` tool maintains ongoing conversations with Perplexity AI. Creates new chats or continues existing ones with full history context. Returns chat ID for future continuation. 32 | - **Streaming Responses with Progress Reporting:** Uses progress reporting to prevent timeouts on slow responses. 33 | 34 | ## Quickstart 35 | 36 | ### Prerequisites 37 | 38 | Before using this MCP server, ensure you have: 39 | 40 | - Python 3.10 or higher 41 | - [uvx](https://docs.astral.sh/uv/#installation) package manager installed 42 | 43 | Note: Installation instructions for uvx are available [here](https://docs.astral.sh/uv/#installation). 44 | 45 | ### Configuration for All Clients 46 | 47 | To use this MCP server, configure your client with these settings (configuration method varies by client): 48 | 49 | ```json 50 | "mcpServers": { 51 | "mcp-perplexity": { 52 | "command": "uvx", 53 | "args": ["mcp-perplexity"], 54 | "env": { 55 | "PERPLEXITY_API_KEY": "your-api-key", 56 | "PERPLEXITY_MODEL": "sonar-pro", 57 | "DB_PATH": "chats.db" 58 | } 59 | } 60 | } 61 | ``` 62 | 63 | ## Environment Variables 64 | 65 | Configure the MCP Perplexity server using the following environment variables: 66 | 67 | | Variable | Description | Default Value | Required | 68 | |----------|-------------|---------------|----------| 69 | | `PERPLEXITY_API_KEY` | Your Perplexity API key | None | Yes | 70 | | `PERPLEXITY_MODEL` | Default model for interactions | `sonar-pro` | No | 71 | | `PERPLEXITY_MODEL_ASK` | Specific model for `ask_perplexity` tool | Uses `PERPLEXITY_MODEL` | No | 72 | | `PERPLEXITY_MODEL_CHAT` | Specific model for `chat_perplexity` tool | Uses `PERPLEXITY_MODEL` | No | 73 | | `DB_PATH` | Path to store chat history database | `chats.db` | No | 74 | | `WEB_UI_ENABLED` | Enable or disable web UI | `false` | No | 75 | | `WEB_UI_PORT` | Port for web UI | `8050` | No | 76 | | `WEB_UI_HOST` | Host for web UI | `127.0.0.1` | No | 77 | | `DEBUG_LOGS` | Enable detailed logging | `false` | No | 78 | 79 | #### Using Smithery CLI 80 | ```bash 81 | npx -y @smithery/cli@latest run @daniel-lxs/mcp-perplexity --config "{\"perplexityApiKey\":\"pplx-abc\",\"perplexityModel\":\"sonar-pro\"}" 82 | ``` 83 | 84 | ## Usage 85 | 86 | ### ask_perplexity 87 | 88 | The `ask_perplexity` tool is used for specific questions, this tool doesn't maintain a chat history, every request is a new chat. 89 | 90 | The tool will return a response from Perplexity AI using the `PERPLEXITY_MODEL_ASK` model if specified, otherwise it will use the `PERPLEXITY_MODEL` model. 91 | 92 | ### chat_perplexity 93 | 94 | The `chat_perplexity` tool is used for ongoing conversations, this tool maintains a chat history. 95 | A chat is identified by a chat ID, this ID is returned by the tool when a new chat is created. Chat IDs look like this: `wild-horse-12`. 96 | 97 | This tool is useful for debugging, research, and any other task that requires a chat history. 98 | 99 | The tool will return a response from Perplexity AI using the `PERPLEXITY_MODEL_CHAT` model if specified, otherwise it will use the `PERPLEXITY_MODEL` model. 100 | 101 | ### list_chats_perplexity 102 | Lists all available chat conversations. It returns a paginated list of chats, showing the chat ID, title, and creation time (in relative format). You can specify the page number using the `page` argument (defaults to 1, with 50 chats per page). 103 | 104 | ### read_chat_perplexity 105 | Retrieves the complete conversation history for a given `chat_id`. This tool returns all messages in the chat, including timestamps and roles (user or assistant). This tool does *not* make any API calls to Perplexity; it only reads from the local database. 106 | 107 | ## Web UI 108 | 109 | The MCP Perplexity server now includes a web interface for easier interaction and management of chats. 110 | 111 | ### Features 112 | - Interactive chat interface 113 | - Chat history management 114 | - Real-time message display 115 | 116 | ### Screenshots 117 | 118 | #### Chat List View 119 | ![image](https://github.com/user-attachments/assets/a8aebd19-f58a-4d6c-988e-ea1c1ca7f174) 120 | 121 | #### Chat Interface 122 | ![image](https://github.com/user-attachments/assets/627bfcdb-2214-47e6-a55e-3987737ad00f) 123 | 124 | ### Accessing the Web UI 125 | 126 | When `WEB_UI_ENABLED` is set to `true`, the web UI will be available at `http://WEB_UI_HOST:WEB_UI_PORT`. 127 | 128 | By default, this is `http://127.0.0.1:8050`. 129 | 130 | ## Development 131 | 132 | This project uses setuptools for development and builds. To get started: 133 | 134 | 1. Create a virtual environment: 135 | ```bash 136 | python -m venv .venv 137 | source .venv/bin/activate # On Linux/macOS 138 | # or 139 | .venv\Scripts\activate # On Windows 140 | ``` 141 | 142 | 2. Install the project in editable mode with all dependencies: 143 | ```bash 144 | pip install -e . 145 | ``` 146 | 147 | 3. Build the project: 148 | ```bash 149 | python -m build 150 | ``` 151 | 152 | The virtual environment will contain all required dependencies for development. 153 | 154 | ## Contributing 155 | 156 | This project is open to contributions. Please see the [CONTRIBUTING.md](CONTRIBUTING.md) file for more information. 157 | 158 | ## License 159 | 160 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 161 | 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "mcp-perplexity" 3 | version = "0.5.8" 4 | description = "MCP Server for the Perplexity API." 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | license = { text = "MIT" } 8 | dependencies = [ 9 | "mcp>=1.0.0", 10 | "httpx", 11 | "haikunator>=2.1.0", 12 | "quart>=0.19.4", 13 | "sqlalchemy>=2.0.0", 14 | "hypercorn>=0.15.0", 15 | "markdown2>=2.4.0" 16 | ] 17 | 18 | [project.urls] 19 | Homepage = "https://github.com/daniel-lxs/mcp-perplexity" 20 | Repository = "https://github.com/daniel-lxs/mcp-perplexity" 21 | 22 | [[project.authors]] 23 | name = "Daniel Riccio" 24 | email = "ricciodaniel98@gmail.com" 25 | 26 | [project.scripts] 27 | mcp-perplexity = "mcp_perplexity:main" 28 | 29 | [build-system] 30 | requires = ["setuptools>=45", "wheel"] 31 | build-backend = "setuptools.build_meta" 32 | 33 | [tool.semantic_release] 34 | version_variable = ["src/mcp_perplexity/__init__.py:__version__"] 35 | branch = "main" 36 | upload_to_pypi = false 37 | upload_to_release = true 38 | build_command = "python -m build" 39 | commit_parser = "angular" 40 | major_on_zero = false 41 | tag_format = "v{version}" 42 | # Use custom templates 43 | templates_dir = "templates" 44 | 45 | # Changelog configuration with proper section titles 46 | [tool.semantic_release.changelog] 47 | changelog_file = "CHANGELOG.md" 48 | template_dir = "templates" 49 | # Define mode for changelog generation 50 | mode = "init" 51 | 52 | [tool.semantic_release.remote] 53 | type = "github" 54 | token = { env = "GH_TOKEN" } 55 | 56 | [tool.semantic_release.remote.github] 57 | release_notes = true 58 | 59 | [tool.setuptools] 60 | include-package-data = true 61 | 62 | [tool.setuptools.package-data] 63 | "mcp_perplexity.web" = ["templates/*.html"] 64 | -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | type: object 8 | required: 9 | - perplexityApiKey 10 | properties: 11 | perplexityApiKey: 12 | type: string 13 | description: The API key for the Perplexity API. 14 | perplexityModel: 15 | type: string 16 | default: sonar-pro 17 | description: "Optional: The model for the Perplexity API." 18 | modelAsk: 19 | type: string 20 | description: "Optional: Override model for ask_perplexity tool" 21 | modelChat: 22 | type: string 23 | description: "Optional: Override model for chat_perplexity tool" 24 | dbPath: 25 | type: string 26 | description: "Optional: Custom path for SQLite chat history database" 27 | commandFunction: 28 | # A function that produces the CLI command to start the MCP on stdio. 29 | |- 30 | (config) => ({command: 'mcp-perplexity', args: [], env: { 31 | PERPLEXITY_API_KEY: config.perplexityApiKey, 32 | PERPLEXITY_MODEL: config.perplexityModel, 33 | PERPLEXITY_MODEL_ASK: config.modelAsk, 34 | PERPLEXITY_MODEL_CHAT: config.modelChat, 35 | DB_PATH: config.dbPath 36 | }}) 37 | 38 | build: 39 | dockerfile: Dockerfile 40 | dockerBuildPath: . 41 | -------------------------------------------------------------------------------- /src/mcp_perplexity/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import sys 4 | import logging 5 | from typing import Optional 6 | import socket 7 | from pathlib import Path 8 | 9 | from .utils import get_logs_dir 10 | from .server import main as server_main 11 | from .web import create_app, WEB_UI_ENABLED, WEB_UI_PORT, WEB_UI_HOST 12 | 13 | __version__ = "0.5.8" 14 | 15 | web_ui_running = False 16 | 17 | 18 | async def is_port_in_use(host: str, port: int) -> bool: 19 | """Checks if a port is in use.""" 20 | try: 21 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 22 | s.settimeout(1) # Short timeout to avoid hanging 23 | s.bind((host, port)) 24 | return False # Port is free 25 | except OSError: 26 | return True # Port is in use 27 | 28 | 29 | async def run_web_ui(): 30 | global web_ui_running 31 | if web_ui_running: 32 | print("Web UI is already running. Skipping.") 33 | return 34 | 35 | app = create_app() 36 | if app: 37 | web_ui_running = True 38 | try: 39 | from hypercorn.asyncio import serve 40 | from hypercorn.config import Config 41 | 42 | config = Config() 43 | config.bind = [f"{WEB_UI_HOST}:{WEB_UI_PORT}"] 44 | 45 | # Only configure logging if DEBUG_LOGS is enabled 46 | if os.getenv('DEBUG_LOGS', 'false').lower() == 'true': 47 | # Set up file logging for Hypercorn 48 | logs_dir = get_logs_dir() 49 | logs_dir.mkdir(parents=True, exist_ok=True) 50 | 51 | hypercorn_logger = logging.getLogger('hypercorn') 52 | hypercorn_logger.handlers = [] # Remove any existing handlers 53 | handler = logging.FileHandler(str(logs_dir / "hypercorn.log")) 54 | handler.setFormatter(logging.Formatter( 55 | '%(asctime)s - %(name)s - %(levelname)s - %(message)s')) 56 | hypercorn_logger.addHandler(handler) 57 | hypercorn_logger.propagate = False 58 | 59 | # Enable logging in config 60 | config.accesslog = hypercorn_logger 61 | config.errorlog = hypercorn_logger 62 | hypercorn_logger.setLevel(logging.INFO) 63 | else: 64 | # Completely disable all Hypercorn logging 65 | config.accesslog = None 66 | config.errorlog = None 67 | 68 | await serve(app, config) 69 | except Exception as e: 70 | # Only log errors if DEBUG_LOGS is enabled 71 | if os.getenv('DEBUG_LOGS', 'false').lower() == 'true': 72 | logs_dir = get_logs_dir() 73 | logs_dir.mkdir(parents=True, exist_ok=True) 74 | with open(logs_dir / "web_error.log", 'a') as f: 75 | f.write(f"Failed to start web UI: {e}\n") 76 | finally: 77 | web_ui_running = False 78 | 79 | 80 | async def run_server(args: Optional[list] = None): 81 | if args is None: 82 | args = sys.argv[1:] 83 | 84 | # Create logs directory only if debug logs are enabled 85 | if os.getenv('DEBUG_LOGS', 'false').lower() == 'true': 86 | logs_dir = get_logs_dir() 87 | logs_dir.mkdir(parents=True, exist_ok=True) 88 | 89 | # Start both the web UI and MCP server 90 | tasks = [] 91 | 92 | # Add web UI task if enabled 93 | if WEB_UI_ENABLED: 94 | tasks.append(run_web_ui()) 95 | 96 | # Add MCP server task - this is the only one that should use stdio 97 | tasks.append(server_main()) 98 | 99 | # Run both tasks concurrently 100 | await asyncio.gather(*tasks) 101 | 102 | 103 | def main(args: Optional[list] = None): 104 | """Main entry point for the package.""" 105 | try: 106 | # Configure logging based on DEBUG_LOGS environment variable 107 | if os.getenv('DEBUG_LOGS', 'false').lower() != 'true': 108 | # Disable all root logging to prevent any stdout logging 109 | logging.getLogger().handlers = [] 110 | # Set root logger level to CRITICAL to minimize any accidental logging 111 | logging.getLogger().setLevel(logging.CRITICAL) 112 | else: 113 | # Ensure logs directory exists 114 | logs_dir = get_logs_dir() 115 | logs_dir.mkdir(parents=True, exist_ok=True) 116 | 117 | # Configure root logger for debug mode 118 | root_logger = logging.getLogger() 119 | root_logger.setLevel(logging.INFO) 120 | # Add file handler for general logs 121 | handler = logging.FileHandler(str(logs_dir / "app.log")) 122 | handler.setFormatter(logging.Formatter( 123 | '%(asctime)s - %(name)s - %(levelname)s - %(message)s')) 124 | root_logger.addHandler(handler) 125 | 126 | asyncio.run(run_server(args)) 127 | except KeyboardInterrupt: 128 | pass 129 | 130 | if __name__ == "__main__": 131 | main() 132 | 133 | __all__ = ["main", "server"] 134 | -------------------------------------------------------------------------------- /src/mcp_perplexity/database.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import datetime 3 | from contextlib import contextmanager 4 | import logging 5 | from typing import Optional, List, TypeVar, Type, Callable 6 | from pathlib import Path 7 | 8 | from sqlalchemy import create_engine, Column, String, DateTime, ForeignKey 9 | from sqlalchemy.orm import sessionmaker, declarative_base, relationship, Session 10 | from sqlalchemy.pool import QueuePool 11 | 12 | from .utils import get_logs_dir 13 | 14 | # Setup logging 15 | logger = logging.getLogger(__name__) 16 | 17 | # Only enable logging if DEBUG_LOGS is set to true 18 | if os.getenv('DEBUG_LOGS', 'false').lower() == 'true': 19 | logger.setLevel(logging.INFO) 20 | 21 | # Ensure logs directory exists 22 | logs_dir = get_logs_dir() 23 | logs_dir.mkdir(parents=True, exist_ok=True) 24 | 25 | # File handler for database operations 26 | db_handler = logging.FileHandler(str(logs_dir / "database.log")) 27 | db_handler.setFormatter(logging.Formatter( 28 | '%(asctime)s - %(name)s - %(levelname)s - %(message)s')) 29 | logger.addHandler(db_handler) 30 | else: 31 | logger.setLevel(logging.CRITICAL) # Effectively disable logging 32 | 33 | # Disable propagation to prevent stdout logging 34 | logger.propagate = False 35 | 36 | Base = declarative_base() 37 | 38 | 39 | class Chat(Base): 40 | __tablename__ = 'chats' 41 | 42 | id = Column(String, primary_key=True) 43 | created_at = Column(DateTime, default=datetime.utcnow) 44 | title = Column(String) 45 | 46 | # Relationship with messages 47 | messages = relationship( 48 | "Message", back_populates="chat", cascade="all, delete-orphan") 49 | 50 | 51 | class Message(Base): 52 | __tablename__ = 'messages' 53 | 54 | id = Column(String, primary_key=True) 55 | chat_id = Column(String, ForeignKey('chats.id')) 56 | role = Column(String) 57 | content = Column(String) 58 | timestamp = Column(DateTime, default=datetime.utcnow) 59 | 60 | # Relationship with chat 61 | chat = relationship("Chat", back_populates="messages") 62 | 63 | 64 | T = TypeVar('T') 65 | 66 | 67 | class SessionManager: 68 | """A proper context manager for database sessions.""" 69 | 70 | def __init__(self, session_factory: sessionmaker[Session]): 71 | self.session_factory = session_factory 72 | self.session: Optional[Session] = None 73 | 74 | def __enter__(self) -> Session: 75 | self.session = self.session_factory() 76 | return self.session 77 | 78 | def __exit__(self, exc_type, exc_val, exc_tb): 79 | if self.session is None: 80 | return 81 | 82 | try: 83 | if exc_type is None: 84 | self.session.commit() 85 | else: 86 | self.session.rollback() 87 | logger.error(f"Database session error: {exc_val}") 88 | finally: 89 | self.session.close() 90 | self.session = None 91 | 92 | 93 | class DatabaseManager: 94 | def __init__(self, db_path: Optional[str] = None): 95 | if db_path is None: 96 | db_path = os.getenv('DB_PATH', 'chats.db') 97 | 98 | self.db_url = f"sqlite:///{db_path}" 99 | self.engine = create_engine( 100 | self.db_url, 101 | poolclass=QueuePool, 102 | pool_size=10, 103 | max_overflow=20, 104 | pool_timeout=30, 105 | connect_args={'check_same_thread': False} # Required for SQLite 106 | ) 107 | self.SessionLocal = sessionmaker( 108 | autocommit=False, autoflush=False, bind=self.engine) 109 | 110 | # Create tables 111 | try: 112 | Base.metadata.create_all(bind=self.engine) 113 | logger.info("Database tables created successfully") 114 | except Exception as e: 115 | logger.error(f"Error creating database tables: {e}") 116 | raise 117 | 118 | def get_session(self): 119 | """Get a database session context manager.""" 120 | return SessionManager(self.SessionLocal) 121 | 122 | def get_all_chats(self) -> List[Chat]: 123 | with self.get_session() as session: 124 | try: 125 | chats = session.query(Chat).order_by( 126 | Chat.created_at.desc()).all() 127 | return chats 128 | except Exception as e: 129 | logger.error(f"Error fetching chats: {e}") 130 | raise 131 | 132 | def get_chat(self, chat_id: str) -> Optional[Chat]: 133 | with self.get_session() as session: 134 | try: 135 | return session.query(Chat).filter(Chat.id == chat_id).first() 136 | except Exception as e: 137 | logger.error(f"Error fetching chat {chat_id}: {e}") 138 | raise 139 | 140 | def get_chat_messages(self, chat_id: str) -> List[Message]: 141 | with self.get_session() as session: 142 | try: 143 | return session.query(Message).filter( 144 | Message.chat_id == chat_id 145 | ).order_by(Message.timestamp.asc()).all() 146 | except Exception as e: 147 | logger.error( 148 | f"Error fetching messages for chat {chat_id}: {e}") 149 | raise 150 | 151 | def delete_chat(self, chat_id: str) -> None: 152 | """Delete a chat and all its messages.""" 153 | with self.get_session() as session: 154 | chat = session.query(Chat).filter(Chat.id == chat_id).first() 155 | if chat: 156 | session.delete(chat) 157 | session.commit() 158 | -------------------------------------------------------------------------------- /src/mcp_perplexity/perplexity_client.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import logging 4 | from typing import AsyncGenerator, Dict, List, Optional, Tuple 5 | 6 | import httpx 7 | 8 | # Setup logging 9 | logger = logging.getLogger(__name__) 10 | 11 | # Get API key and log whether it's set (without revealing the actual key) 12 | PERPLEXITY_API_KEY = os.getenv("PERPLEXITY_API_KEY") 13 | if PERPLEXITY_API_KEY: 14 | logger.info("PERPLEXITY_API_KEY is set") 15 | else: 16 | logger.warning("PERPLEXITY_API_KEY is not set - API calls will fail with unauthorized errors") 17 | 18 | PERPLEXITY_MODEL = os.getenv("PERPLEXITY_MODEL") or "sonar-pro" 19 | PERPLEXITY_MODEL_ASK = os.getenv("PERPLEXITY_MODEL_ASK") or PERPLEXITY_MODEL 20 | PERPLEXITY_MODEL_CHAT = os.getenv("PERPLEXITY_MODEL_CHAT") or PERPLEXITY_MODEL 21 | PERPLEXITY_API_BASE_URL = "https://api.perplexity.ai" 22 | 23 | SYSTEM_PROMPT = """You are an expert assistant providing accurate answers to technical questions. 24 | Your responses must: 25 | 1. Be based on the most relevant web sources 26 | 2. Include source citations for all factual claims 27 | 3. If no relevant results are found, suggest 2-3 alternative search queries that might better uncover the needed information 28 | 4. Prioritize technical accuracy, especially for programming-related questions""" 29 | 30 | TIMEOUT = 120.0 31 | 32 | # Define model profiles with validation ranges 33 | MODEL_PROFILES = { 34 | "sonar": { 35 | "temperature": 0.2, 36 | "top_p": 0.9, 37 | }, 38 | "sonar-pro": { 39 | "temperature": 0.2, 40 | "top_p": 0.9, 41 | }, 42 | "sonar-reasoning": { 43 | "temperature": 0.6, 44 | "top_p": 0.95, 45 | }, 46 | "sonar-reasoning-pro": { 47 | "temperature": 0.6, 48 | "top_p": 0.95, 49 | }, 50 | } 51 | 52 | 53 | class PerplexityClient: 54 | def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None): 55 | self.api_key = api_key or PERPLEXITY_API_KEY 56 | self.base_url = base_url or PERPLEXITY_API_BASE_URL 57 | if not self.api_key: 58 | raise ValueError("Perplexity API key is required") 59 | 60 | async def _stream_completion( 61 | self, 62 | messages: List[Dict[str, str]], 63 | model: Optional[str] = None, 64 | ) -> AsyncGenerator[Tuple[str, List[str], Dict[str, int]], None]: 65 | """ 66 | Stream completion from Perplexity API. 67 | 68 | Args: 69 | messages: List of message dictionaries with 'role' and 'content' 70 | model: Optional model override 71 | 72 | Yields: 73 | Tuple of (content_chunk, citations, usage_stats) 74 | """ 75 | model = model or PERPLEXITY_MODEL 76 | profile = MODEL_PROFILES.get(model, {}) 77 | request_body = { 78 | "model": model, 79 | "messages": messages, 80 | "stream": True, 81 | } 82 | 83 | if profile: 84 | # Apply validated profile settings 85 | for setting in ["temperature", "top_p"]: 86 | if setting in profile: 87 | request_body[setting] = profile[setting] 88 | 89 | async with httpx.AsyncClient() as client: 90 | response = await client.post( 91 | f"{self.base_url}/chat/completions", 92 | headers={ 93 | "Authorization": f"Bearer {self.api_key}", 94 | "Content-Type": "application/json", 95 | }, 96 | json=request_body, 97 | timeout=TIMEOUT, 98 | ) 99 | response.raise_for_status() 100 | 101 | citations = [] 102 | usage = {} 103 | 104 | async for chunk in response.aiter_text(): 105 | for line in chunk.split('\n'): 106 | line = line.strip() 107 | if line.startswith("data: "): 108 | try: 109 | data = json.loads(line[6:]) 110 | if "usage" in data: 111 | usage.update(data["usage"]) 112 | if "citations" in data: 113 | # Clear existing citations and use the complete list 114 | citations = data["citations"] 115 | if data.get("choices"): 116 | content = data["choices"][0].get( 117 | "delta", {}).get("content", "") 118 | if content: 119 | yield content, citations.copy(), usage 120 | except json.JSONDecodeError: 121 | continue 122 | 123 | async def ask( 124 | self, 125 | query: str, 126 | ) -> AsyncGenerator[Tuple[str, List[str], Dict[str, int]], None]: 127 | """ 128 | Send a one-off question to Perplexity. 129 | 130 | Args: 131 | query: The question to ask 132 | 133 | Yields: 134 | Tuple of (content_chunk, citations, usage_stats) 135 | """ 136 | messages = [ 137 | {"role": "system", "content": SYSTEM_PROMPT}, 138 | {"role": "user", "content": query} 139 | ] 140 | 141 | async for content, citations, usage in self._stream_completion( 142 | messages, 143 | model=PERPLEXITY_MODEL_ASK, 144 | ): 145 | yield content, citations, usage 146 | 147 | async def chat( 148 | self, 149 | messages: List[Dict[str, str]], 150 | ) -> AsyncGenerator[Tuple[str, List[str], Dict[str, int]], None]: 151 | """ 152 | Continue a chat conversation with Perplexity. 153 | 154 | Args: 155 | messages: List of previous messages with 'role' and 'content' 156 | 157 | Yields: 158 | Tuple of (content_chunk, citations, usage_stats) 159 | """ 160 | system_message = {"role": "system", "content": SYSTEM_PROMPT} 161 | full_messages = [system_message] + messages 162 | 163 | async for content, citations, usage in self._stream_completion( 164 | full_messages, 165 | model=PERPLEXITY_MODEL_CHAT, 166 | ): 167 | yield content, citations, usage 168 | -------------------------------------------------------------------------------- /src/mcp_perplexity/server.py: -------------------------------------------------------------------------------- 1 | import os 2 | from textwrap import dedent 3 | import json 4 | from collections import deque 5 | from datetime import datetime 6 | import uuid 7 | from typing import List, Dict, Optional 8 | import sqlite3 9 | from pathlib import Path 10 | 11 | import mcp.server.stdio 12 | import mcp.types as types 13 | from mcp.server import NotificationOptions, Server 14 | from mcp.server.models import InitializationOptions 15 | from haikunator import Haikunator 16 | 17 | from .perplexity_client import PerplexityClient 18 | from .database import DatabaseManager, Chat, Message 19 | from .utils import get_logs_dir 20 | 21 | haikunator = Haikunator() 22 | 23 | # Get default DB path in user's home directory 24 | def get_default_db_path(): 25 | data_dir = Path.home() / ".mcp-perplexity" / "data" 26 | data_dir.mkdir(parents=True, exist_ok=True) 27 | return str(data_dir / "chats.db") 28 | 29 | DB_PATH = os.getenv("DB_PATH", get_default_db_path()) 30 | SYSTEM_PROMPT = """You are an expert assistant providing accurate answers to technical questions. 31 | Your responses must: 32 | 1. Be based on the most relevant web sources 33 | 2. Include source citations for all factual claims 34 | 3. If no relevant results are found, suggest 2-3 alternative search queries that might better uncover the needed information 35 | 4. Prioritize technical accuracy, especially for programming-related questions""" 36 | 37 | server = Server("mcp-server-perplexity") 38 | perplexity_client = PerplexityClient() 39 | db_manager = DatabaseManager(DB_PATH) 40 | 41 | 42 | def init_db(): 43 | try: 44 | # Create parent directories if needed 45 | db_dir = os.path.dirname(DB_PATH) 46 | if db_dir: # Only create directories if path contains them 47 | os.makedirs(db_dir, exist_ok=True) 48 | 49 | conn = sqlite3.connect(DB_PATH) 50 | c = conn.cursor() 51 | 52 | # Create tables with enhanced error handling 53 | c.execute('''CREATE TABLE IF NOT EXISTS chats 54 | (id TEXT PRIMARY KEY, 55 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 56 | title TEXT)''') 57 | 58 | c.execute('''CREATE TABLE IF NOT EXISTS messages 59 | (id TEXT PRIMARY KEY, 60 | chat_id TEXT, 61 | role TEXT, 62 | content TEXT, 63 | timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 64 | FOREIGN KEY(chat_id) REFERENCES chats(id))''') 65 | 66 | # Verify table creation 67 | c.execute( 68 | "SELECT name FROM sqlite_master WHERE type='table' AND name IN ('chats', 'messages')") 69 | existing_tables = {row[0] for row in c.fetchall()} 70 | if 'chats' not in existing_tables or 'messages' not in existing_tables: 71 | raise RuntimeError("Failed to create database tables") 72 | 73 | conn.commit() 74 | except sqlite3.Error as e: 75 | raise RuntimeError(f"Database connection error: {str(e)}") 76 | except Exception as e: 77 | raise RuntimeError( 78 | f"Failed to initialize database at '{DB_PATH}': {str(e)}") 79 | finally: 80 | if 'conn' in locals(): 81 | conn.close() 82 | 83 | 84 | # Initialize database on startup 85 | init_db() 86 | 87 | 88 | @server.list_tools() 89 | async def handle_list_tools() -> list[types.Tool]: 90 | return [ 91 | types.Tool( 92 | name="ask_perplexity", 93 | description=dedent( 94 | """ 95 | Provides expert programming assistance through Perplexity. 96 | This tool only has access to the context you have provided. It cannot read any file unless you provide it with the file content. 97 | Focuses on coding solutions, error debugging, and technical explanations. 98 | Returns responses with source citations and alternative suggestions. 99 | """ 100 | ), 101 | inputSchema={ 102 | "type": "object", 103 | "properties": { 104 | "query": { 105 | "type": "string", 106 | "description": "Technical question or problem to solve" 107 | } 108 | }, 109 | "required": ["query"] 110 | }, 111 | ), 112 | types.Tool( 113 | name="chat_perplexity", 114 | description=dedent(""" 115 | Maintains ongoing conversations with Perplexity AI. 116 | Creates new chats or continues existing ones with full history context. 117 | This tool only has access to the context you have provided. It cannot read any file unless you provide it with the file content. 118 | Returns chat ID for future continuation. 119 | 120 | For new chats: Provide 'message' and 'title' 121 | For existing chats: Provide 'chat_id' and 'message' 122 | """), 123 | inputSchema={ 124 | "type": "object", 125 | "properties": { 126 | "message": { 127 | "type": "string", 128 | "description": "New message to add to the conversation" 129 | }, 130 | "chat_id": { 131 | "type": "string", 132 | "description": "ID of an existing chat to continue. If not provided, a new chat will be created and title is required." 133 | }, 134 | "title": { 135 | "type": "string", 136 | "description": "Title for the new chat. Required when creating a new chat (when chat_id is not provided)." 137 | } 138 | }, 139 | "required": ["message"] 140 | }, 141 | ), 142 | types.Tool( 143 | name="list_chats_perplexity", 144 | description=dedent(""" 145 | Lists all available chat conversations with Perplexity AI. 146 | Returns chat IDs, titles, and creation dates. 147 | Results are paginated with 50 chats per page. 148 | """), 149 | inputSchema={ 150 | "type": "object", 151 | "properties": { 152 | "page": { 153 | "type": "integer", 154 | "description": "Page number (defaults to 1)", 155 | "minimum": 1 156 | } 157 | } 158 | }, 159 | ), 160 | types.Tool( 161 | name="read_chat_perplexity", 162 | description=dedent(""" 163 | Retrieves the complete conversation history for a specific chat. 164 | Returns the full chat history with all messages and their timestamps. 165 | No API calls are made to Perplexity - this only reads from local storage. 166 | """), 167 | inputSchema={ 168 | "type": "object", 169 | "properties": { 170 | "chat_id": { 171 | "type": "string", 172 | "description": "ID of the chat to retrieve" 173 | } 174 | }, 175 | "required": ["chat_id"] 176 | }, 177 | ) 178 | ] 179 | 180 | 181 | def generate_chat_id(): 182 | return haikunator.haikunate(token_length=2, delimiter='-').lower() 183 | 184 | 185 | def store_message(chat_id: str, role: str, content: str, title: Optional[str] = None) -> None: 186 | with db_manager.get_session() as session: 187 | # Create chat if it doesn't exist 188 | chat = session.query(Chat).filter(Chat.id == chat_id).first() 189 | if not chat: 190 | chat = Chat(id=chat_id, title=title) 191 | session.add(chat) 192 | session.flush() # Ensure chat is created before adding message 193 | 194 | # Create and store message 195 | message = Message( 196 | id=str(uuid.uuid4()), 197 | chat_id=chat_id, 198 | role=role, 199 | content=content 200 | ) 201 | session.add(message) 202 | 203 | 204 | def get_chat_history(chat_id: str) -> List[Dict[str, str]]: 205 | with db_manager.get_session() as session: 206 | messages = session.query(Message).filter( 207 | Message.chat_id == chat_id 208 | ).order_by(Message.timestamp.asc()).all() 209 | 210 | # Access attributes within the session context 211 | result = [] 212 | for msg in messages: 213 | content = msg.content 214 | # For assistant messages, we need to keep the content as is 215 | result.append({"role": msg.role, "content": content}) 216 | 217 | # Add system message at the beginning if not present 218 | has_system = any(msg["role"] == "system" for msg in result) 219 | if not has_system: 220 | result.insert(0, {"role": "system", "content": SYSTEM_PROMPT}) 221 | 222 | return result 223 | 224 | 225 | def get_relative_time(timestamp: datetime) -> str: 226 | try: 227 | # Get current time in UTC for comparison 228 | now_utc = datetime.utcnow() 229 | 230 | # Calculate the time difference 231 | diff = now_utc - timestamp 232 | seconds = diff.total_seconds() 233 | 234 | # For future dates or dates too far in the future/past, show the actual date 235 | if abs(seconds) > 31536000: # More than a year 236 | # Convert to local time for display 237 | local_dt = timestamp + (datetime.now() - datetime.utcnow()) 238 | return local_dt.strftime("%Y-%m-%d %H:%M:%S") 239 | 240 | if seconds < 0: # Future dates within a year 241 | seconds = abs(seconds) 242 | prefix = "in " 243 | suffix = "" 244 | else: 245 | prefix = "" 246 | suffix = " ago" 247 | 248 | if seconds < 60: 249 | return "just now" 250 | elif seconds < 3600: 251 | minutes = int(seconds / 60) 252 | return f"{prefix}{minutes} minute{'s' if minutes != 1 else ''}{suffix}" 253 | elif seconds < 86400: 254 | hours = int(seconds / 3600) 255 | return f"{prefix}{hours} hour{'s' if hours != 1 else ''}{suffix}" 256 | elif seconds < 604800: # 7 days 257 | days = int(seconds / 86400) 258 | return f"{prefix}{days} day{'s' if days != 1 else ''}{suffix}" 259 | elif seconds < 2592000: # 30 days 260 | weeks = int(seconds / 604800) 261 | return f"{prefix}{weeks} week{'s' if weeks != 1 else ''}{suffix}" 262 | else: # less than a year 263 | months = int(seconds / 2592000) 264 | return f"{prefix}{months} month{'s' if months != 1 else ''}{suffix}" 265 | except Exception: 266 | return str(timestamp) # Return original datetime if parsing fails 267 | 268 | 269 | @server.call_tool() 270 | async def handle_call_tool( 271 | name: str, arguments: dict 272 | ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: 273 | context = server.request_context 274 | progress_token = context.meta.progressToken if context.meta else None 275 | 276 | if name == "ask_perplexity": 277 | try: 278 | # Initialize progress tracking 279 | initial_estimate = 1000 280 | progress_counter = 0 281 | total_estimate = initial_estimate 282 | chunk_sizes = deque(maxlen=10) # Store last 10 chunk sizes 283 | chunk_count = 0 284 | 285 | if progress_token: 286 | await context.session.send_progress_notification( 287 | progress_token=progress_token, 288 | progress=0, 289 | total=initial_estimate, 290 | ) 291 | 292 | full_response = "" 293 | citations = [] 294 | usage = {} 295 | 296 | async for content, current_citations, current_usage in perplexity_client.ask(arguments["query"]): 297 | full_response += content 298 | citations = current_citations 299 | usage = current_usage 300 | 301 | # Update progress tracking 302 | tokens_in_chunk = len(content.split()) 303 | progress_counter += tokens_in_chunk 304 | chunk_count += 1 305 | chunk_sizes.append(tokens_in_chunk) 306 | 307 | # Update total estimate every 5 chunks 308 | if chunk_count % 5 == 0 and chunk_sizes: 309 | avg_chunk_size = sum(chunk_sizes) / len(chunk_sizes) 310 | total_estimate = max(initial_estimate, 311 | int(progress_counter + avg_chunk_size * 10)) 312 | 313 | if progress_token: 314 | await context.session.send_progress_notification( 315 | progress_token=progress_token, 316 | progress=progress_counter, 317 | total=total_estimate, 318 | ) 319 | 320 | citation_list = "\n".join( 321 | f"{i}. {url}" for i, url in enumerate(citations, start=1)) 322 | 323 | # Handle empty citations 324 | if not citation_list: 325 | citation_list = "No sources available for this response." 326 | 327 | # Format the response text for display 328 | response_text = ( 329 | f"{full_response}\n\n" 330 | f"Sources:\n{citation_list}\n\n" 331 | f"API Usage:\n" 332 | f"- Prompt tokens: {usage.get('prompt_tokens', 'N/A')}\n" 333 | f"- Completion tokens: {usage.get('completion_tokens', 'N/A')}\n" 334 | f"- Total tokens: {usage.get('total_tokens', 'N/A')}" 335 | ) 336 | 337 | if progress_token: 338 | await context.session.send_progress_notification( 339 | progress_token=progress_token, 340 | progress=progress_counter, 341 | total=progress_counter # Set final total to actual tokens received 342 | ) 343 | 344 | return [ 345 | types.TextContent( 346 | type="text", 347 | text=response_text 348 | ) 349 | ] 350 | 351 | except Exception as e: 352 | if progress_token: 353 | await context.session.send_progress_notification( 354 | progress_token=progress_token, 355 | progress=progress_counter if 'progress_counter' in locals() else 0, 356 | total=progress_counter if 'progress_counter' in locals() else 0, 357 | ) 358 | raise RuntimeError(f"API error: {str(e)}") 359 | 360 | elif name == "chat_perplexity": 361 | chat_id = arguments.get("chat_id") or generate_chat_id() 362 | user_message = arguments["message"] 363 | title = arguments.get("title") 364 | 365 | # Store user message 366 | if not arguments.get("chat_id"): # Only store title for new chats 367 | store_message(chat_id, "user", user_message, title or "Untitled") 368 | else: 369 | store_message(chat_id, "user", user_message) 370 | 371 | # Get full chat history 372 | chat_history = get_chat_history(chat_id) 373 | 374 | try: 375 | # Initialize progress tracking 376 | initial_estimate = 1000 377 | progress_counter = 0 378 | total_estimate = initial_estimate 379 | chunk_sizes = deque(maxlen=10) 380 | chunk_count = 0 381 | 382 | if progress_token: 383 | await context.session.send_progress_notification( 384 | progress_token=progress_token, 385 | progress=0, 386 | total=initial_estimate, 387 | ) 388 | 389 | full_response = "" 390 | citations = [] 391 | usage = {} 392 | 393 | async for content, current_citations, current_usage in perplexity_client.chat(chat_history): 394 | full_response += content 395 | citations = current_citations 396 | usage = current_usage 397 | 398 | # Update progress tracking 399 | tokens_in_chunk = len(content.split()) 400 | progress_counter += tokens_in_chunk 401 | chunk_count += 1 402 | chunk_sizes.append(tokens_in_chunk) 403 | 404 | # Update total estimate every 5 chunks 405 | if chunk_count % 5 == 0 and chunk_sizes: 406 | avg_chunk_size = sum(chunk_sizes) / len(chunk_sizes) 407 | total_estimate = max(initial_estimate, 408 | int(progress_counter + avg_chunk_size * 10)) 409 | 410 | if progress_token: 411 | await context.session.send_progress_notification( 412 | progress_token=progress_token, 413 | progress=progress_counter, 414 | total=total_estimate, 415 | ) 416 | 417 | citation_list = "\n".join( 418 | f"{i}. {url}" for i, url in enumerate(citations, start=1)) 419 | 420 | # Handle empty citations 421 | if not citation_list: 422 | citation_list = "No sources available for this response." 423 | 424 | # Format full response with sources for storage 425 | response_with_sources = ( 426 | f"{full_response}\n\n" 427 | f"Sources:\n{citation_list}" 428 | ) 429 | 430 | # Store assistant response 431 | store_message(chat_id, "assistant", response_with_sources) 432 | 433 | # Format chat history 434 | history_text = "\nChat History:\n" 435 | for msg in chat_history: 436 | role = "You" if msg["role"] == "user" else "Assistant" 437 | history_text += f"\n{role}: {msg['content']}\n" 438 | 439 | response_text = ( 440 | f"Chat ID: {chat_id}\n" 441 | f"{history_text}\n" 442 | f"Current Response:\n{full_response}\n\n" 443 | f"Sources:\n{citation_list}" 444 | ) 445 | 446 | if progress_token: 447 | await context.session.send_progress_notification( 448 | progress_token=progress_token, 449 | progress=progress_counter, 450 | total=progress_counter, # Set final total to actual tokens received 451 | ) 452 | 453 | return [ 454 | types.TextContent( 455 | type="text", 456 | text=response_text 457 | ) 458 | ] 459 | 460 | except Exception as e: 461 | if progress_token: 462 | await context.session.send_progress_notification( 463 | progress_token=progress_token, 464 | progress=progress_counter if 'progress_counter' in locals() else 0, 465 | total=progress_counter if 'progress_counter' in locals() else 0, 466 | ) 467 | raise RuntimeError(f"API error: {str(e)}") 468 | 469 | elif name == "list_chats_perplexity": 470 | page = arguments.get("page", 1) 471 | page_size = 50 472 | offset = (page - 1) * page_size 473 | 474 | with db_manager.get_session() as session: 475 | # Get total count for pagination info 476 | total_chats = session.query(Chat).count() 477 | total_pages = (total_chats + page_size - 1) // page_size 478 | 479 | # Get paginated chats with message count 480 | chats = session.query(Chat).order_by( 481 | Chat.created_at.desc()).offset(offset).limit(page_size).all() 482 | 483 | # Format the response 484 | header = ( 485 | f"Page {page} of {total_pages}\n" 486 | f"Total chats: {total_chats}\n\n" 487 | f"{'=' * 40}\n" 488 | ) 489 | 490 | chat_list = [] 491 | for chat in chats: 492 | message_count = len(chat.messages) 493 | relative_time = get_relative_time(chat.created_at) 494 | title = chat.title if chat.title is not None else 'Untitled' 495 | chat_list.append( 496 | f"Chat ID: {chat.id}\n" 497 | f"Title: {title}\n" 498 | f"Created: {relative_time}\n" 499 | f"Messages: {message_count}" 500 | ) 501 | 502 | response_text = header + "\n\n".join(chat_list) 503 | 504 | return [types.TextContent(type="text", text=response_text)] 505 | 506 | elif name == "read_chat_perplexity": 507 | chat_id = arguments["chat_id"] 508 | 509 | with db_manager.get_session() as session: 510 | chat = session.query(Chat).filter(Chat.id == chat_id).first() 511 | if not chat: 512 | raise ValueError(f"Chat with ID {chat_id} not found") 513 | 514 | messages = session.query(Message).filter( 515 | Message.chat_id == chat_id 516 | ).order_by(Message.timestamp.asc()).all() 517 | 518 | # Format the response 519 | chat_header = ( 520 | f"Chat ID: {chat.id}\n" 521 | f"Title: {chat.title if chat.title is not None else 'Untitled'}\n" 522 | f"Created: {chat.created_at}\n" 523 | f"Messages: {len(messages)}\n\n" 524 | f"{'=' * 40}\n\n" 525 | ) 526 | 527 | message_history = [] 528 | for message in messages: 529 | role_display = "You" if message.role == "user" else "Assistant" 530 | message_history.append( 531 | f"[{message.timestamp}] {role_display}:\n{message.content}\n" 532 | ) 533 | 534 | response_text = chat_header + "\n".join(message_history) 535 | 536 | return [types.TextContent(type="text", text=response_text)] 537 | 538 | else: 539 | raise ValueError(f"Unknown tool: {name}") 540 | 541 | 542 | async def main(): 543 | try: 544 | async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): 545 | await server.run( 546 | read_stream, 547 | write_stream, 548 | InitializationOptions( 549 | server_name="mcp-server-perplexity", 550 | server_version="0.1.2", 551 | capabilities=server.get_capabilities( 552 | notification_options=NotificationOptions( 553 | tools_changed=True), 554 | experimental_capabilities={}, 555 | ), 556 | ), 557 | ) 558 | except Exception as e: 559 | print(f"Server error: {str(e)}", flush=True) 560 | raise 561 | print("Server shutdown", flush=True) 562 | -------------------------------------------------------------------------------- /src/mcp_perplexity/utils.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | def get_logs_dir(): 4 | """Get the logs directory path. 5 | 6 | Returns a path in the user's home directory to ensure it's writable. 7 | """ 8 | home_dir = Path.home() 9 | logs_dir = home_dir / ".mcp-perplexity" / "logs" 10 | return logs_dir -------------------------------------------------------------------------------- /src/mcp_perplexity/web/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | from pathlib import Path 4 | from quart import Quart 5 | from markdown2 import markdown 6 | from ..database import DatabaseManager 7 | from .database_extension import db 8 | from .. import get_logs_dir 9 | 10 | # Setup logging 11 | logger = logging.getLogger(__name__) 12 | 13 | # Only create logs directory and set up file handlers if DEBUG_LOGS is enabled 14 | if os.getenv('DEBUG_LOGS', 'false').lower() == 'true': 15 | # Ensure logs directory exists 16 | logs_dir = get_logs_dir() 17 | logs_dir.mkdir(parents=True, exist_ok=True) 18 | 19 | # File handler for web operations 20 | web_handler = logging.FileHandler(str(logs_dir / "web.log")) 21 | web_handler.setFormatter(logging.Formatter( 22 | '%(asctime)s - %(name)s - %(levelname)s - %(message)s')) 23 | logger.addHandler(web_handler) 24 | 25 | # File handler for template debugging 26 | template_handler = logging.FileHandler(str(logs_dir / "templates.log")) 27 | template_handler.setFormatter(logging.Formatter( 28 | '%(asctime)s - %(name)s - %(levelname)s - %(message)s')) 29 | 30 | # Set log level for debug mode 31 | logger.setLevel(logging.INFO) 32 | template_handler.setLevel(logging.INFO) 33 | 34 | # Add template handler to the template logger 35 | template_logger = logging.getLogger('template_debug') 36 | template_logger.addHandler(template_handler) 37 | template_logger.propagate = False 38 | else: 39 | # Set critical log level when debug logs are disabled 40 | logger.setLevel(logging.CRITICAL) 41 | template_logger = logging.getLogger('template_debug') 42 | template_logger.setLevel(logging.CRITICAL) 43 | 44 | # Disable propagation to prevent stdout logging 45 | logger.propagate = False 46 | 47 | # Environment variables 48 | WEB_UI_ENABLED = os.getenv('WEB_UI_ENABLED', 'false').lower() == 'true' 49 | WEB_UI_PORT = int(os.getenv('WEB_UI_PORT', '8050')) 50 | WEB_UI_HOST = os.getenv('WEB_UI_HOST', '127.0.0.1') 51 | 52 | 53 | def create_app(): 54 | if not WEB_UI_ENABLED: 55 | logger.info("Web UI is disabled via environment variables") 56 | return None 57 | 58 | try: 59 | app = Quart(__name__) 60 | 61 | # Configure template and static directories 62 | app.template_folder = str(Path(__file__).parent / 'templates') 63 | app.static_folder = str(Path(__file__).parent / 'static') 64 | 65 | # Add markdown filter 66 | def custom_markdown_filter(text): 67 | # Handle tags preservation and transformation to collapsible elements 68 | import re 69 | import html 70 | 71 | # First, let's log the original text for debugging 72 | template_logger.info(f"Original text before processing: {text[:100]}...") 73 | 74 | # Extract and save blocks 75 | think_pattern = re.compile(r'(.*?)', re.DOTALL) 76 | think_matches = think_pattern.findall(text) 77 | 78 | # If no think blocks found, just process markdown normally 79 | if not think_matches: 80 | return markdown(text, extras=['fenced-code-blocks', 'tables']) 81 | 82 | # Replace each block with a unique placeholder 83 | # Use a format that's unlikely to be affected by markdown processing 84 | for i, content in enumerate(think_matches): 85 | placeholder = f"THINKBLOCK{i}PLACEHOLDER" 86 | text = text.replace(f"{content}", placeholder) 87 | 88 | # Process markdown 89 | html_content = markdown(text, extras=['fenced-code-blocks', 'tables']) 90 | template_logger.info(f"After markdown processing: {html_content[:100]}...") 91 | 92 | # Restore blocks as collapsible details elements 93 | for i, content in enumerate(think_matches): 94 | placeholder = f"THINKBLOCK{i}PLACEHOLDER" 95 | # Process the content with markdown 96 | processed_content = markdown(content, extras=['fenced-code-blocks', 'tables']) 97 | 98 | # Create a collapsible details element 99 | details_element = ( 100 | f'
' 101 | f'Thought process' 102 | f'
{processed_content}
' 103 | f'
' 104 | ) 105 | 106 | # Try different possible formats the placeholder might have 107 | if placeholder in html_content: 108 | html_content = html_content.replace(placeholder, details_element) 109 | elif f"

{placeholder}

" in html_content: 110 | html_content = html_content.replace(f"

{placeholder}

", details_element) 111 | else: 112 | # If we can't find the exact placeholder, try a more aggressive approach 113 | template_logger.info(f"Placeholder {placeholder} not found in exact form, trying regex") 114 | pattern = re.compile(fr'{placeholder}|

\s*{placeholder}\s*

', re.IGNORECASE) 115 | html_content = pattern.sub(details_element, html_content) 116 | 117 | template_logger.info(f"Final HTML after restoring think blocks: {html_content[:100]}...") 118 | return html_content 119 | 120 | app.jinja_env.filters['markdown'] = custom_markdown_filter 121 | 122 | # Initialize database extension 123 | db.init_app(app) 124 | 125 | # Register routes 126 | from .routes import register_routes 127 | register_routes(app) 128 | 129 | logger.info( 130 | f"Web UI initialized successfully on {WEB_UI_HOST}:{WEB_UI_PORT}") 131 | return app 132 | except Exception as e: 133 | logger.error(f"Failed to initialize web UI: {e}") 134 | return None 135 | -------------------------------------------------------------------------------- /src/mcp_perplexity/web/database_extension.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List, Dict 2 | from quart import Quart 3 | from ..database import DatabaseManager, Chat, Message 4 | from datetime import datetime 5 | 6 | 7 | class Database: 8 | def __init__(self, app: Optional[Quart] = None): 9 | self._database_manager: Optional[DatabaseManager] = None 10 | if app is not None: 11 | self.init_app(app) 12 | 13 | def init_app(self, app: Quart): 14 | self._database_manager = DatabaseManager() 15 | 16 | # Register extension with Quart 17 | if not hasattr(app, 'extensions'): 18 | app.extensions = {} 19 | app.extensions['database'] = self 20 | 21 | @property 22 | def database_manager(self) -> DatabaseManager: 23 | if self._database_manager is None: 24 | raise RuntimeError( 25 | "Database extension not initialized. " 26 | "Did you forget to call init_app()?") 27 | return self._database_manager 28 | 29 | def get_all_chats(self, page: int = 1, per_page: int = 50) -> Dict: 30 | """Get all chats with pagination and convert them to dictionaries to avoid session issues. 31 | 32 | Args: 33 | page: The page number (1-based) 34 | per_page: Number of items per page (default 50) 35 | 36 | Returns: 37 | Dict containing chats and pagination info 38 | """ 39 | with self.database_manager.get_session() as session: 40 | # Calculate offset 41 | offset = (page - 1) * per_page 42 | 43 | # Get total count 44 | total = session.query(Chat).count() 45 | 46 | # Get paginated chats 47 | chats = session.query(Chat).order_by(Chat.created_at.desc())\ 48 | .offset(offset).limit(per_page).all() 49 | 50 | # Convert to dictionaries while session is still open 51 | return { 52 | 'chats': [{ 53 | 'id': chat.id, 54 | 'title': chat.title, 55 | 'created_at': chat.created_at 56 | } for chat in chats], 57 | 'pagination': { 58 | 'page': page, 59 | 'per_page': per_page, 60 | 'total': total, 61 | 'pages': (total + per_page - 1) // per_page 62 | } 63 | } 64 | 65 | def get_chat(self, chat_id: str) -> Optional[Dict]: 66 | """Get a chat and convert it to a dictionary to avoid session issues.""" 67 | with self.database_manager.get_session() as session: 68 | chat = session.query(Chat).filter(Chat.id == chat_id).first() 69 | if not chat: 70 | return None 71 | # Convert to dictionary while session is still open 72 | return { 73 | 'id': chat.id, 74 | 'title': chat.title, 75 | 'created_at': chat.created_at 76 | } 77 | 78 | def get_chat_messages(self, chat_id: str) -> List[Dict]: 79 | """Get chat messages and convert them to dictionaries to avoid session issues.""" 80 | with self.database_manager.get_session() as session: 81 | messages = session.query(Message).filter( 82 | Message.chat_id == chat_id 83 | ).order_by(Message.timestamp.asc()).all() 84 | # Convert to dictionaries while session is still open 85 | return [{ 86 | 'id': msg.id, 87 | 'role': msg.role, 88 | 'content': msg.content, 89 | 'timestamp': msg.timestamp 90 | } for msg in messages] 91 | 92 | def delete_chat(self, chat_id: str) -> None: 93 | """Delete a chat and all its messages.""" 94 | with self.database_manager.get_session() as session: 95 | chat = session.query(Chat).filter(Chat.id == chat_id).first() 96 | if chat: 97 | session.delete(chat) 98 | session.commit() 99 | 100 | 101 | # Create the extension instance 102 | db = Database() 103 | -------------------------------------------------------------------------------- /src/mcp_perplexity/web/routes.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from quart import render_template, jsonify, request 4 | from .database_extension import db 5 | 6 | # Setup logging 7 | logger = logging.getLogger(__name__) 8 | 9 | # Only enable logging if DEBUG_LOGS is set to true 10 | if os.getenv('DEBUG_LOGS', 'false').lower() == 'true': 11 | logger.setLevel(logging.INFO) 12 | else: 13 | logger.setLevel(logging.CRITICAL) # Effectively disable logging 14 | 15 | 16 | def register_routes(app): 17 | @app.route('/') 18 | async def index(): 19 | try: 20 | page = request.args.get('page', 1, type=int) 21 | chats = db.get_all_chats(page=page) 22 | return await render_template('index.html', chats=chats) 23 | except Exception as e: 24 | logger.error(f"Error in index route: {e}") 25 | return await render_template('error.html', error="Failed to load chats"), 500 26 | 27 | @app.route('/chat/') 28 | async def chat(chat_id): 29 | try: 30 | chat = db.get_chat(chat_id) 31 | if not chat: 32 | return await render_template('error.html', error="Chat not found"), 404 33 | 34 | messages = db.get_chat_messages(chat_id) 35 | return await render_template('chat.html', chat=chat, messages=messages) 36 | except Exception as e: 37 | logger.error(f"Error in chat route: {e}") 38 | return await render_template('error.html', error="Failed to load chat"), 500 39 | 40 | @app.route('/api/chats') 41 | async def api_chats(): 42 | try: 43 | page = request.args.get('page', 1, type=int) 44 | chats = db.get_all_chats(page=page) 45 | # If it's an HTMX request, return the chat list HTML 46 | if request.headers.get('HX-Request'): 47 | return await render_template('_chat_list.html', chats=chats) 48 | # Otherwise return JSON for API consumers 49 | return jsonify({ 50 | 'chats': [{ 51 | 'id': chat['id'], 52 | 'title': chat['title'], 53 | 'created_at': chat['created_at'].isoformat() 54 | } for chat in chats['chats']], 55 | 'pagination': chats['pagination'] 56 | }) 57 | except Exception as e: 58 | logger.error(f"Error in api_chats route: {e}") 59 | return jsonify({'error': 'Failed to load chats'}), 500 60 | 61 | @app.route('/api/chat//messages') 62 | async def api_chat_messages(chat_id): 63 | try: 64 | messages = db.get_chat_messages(chat_id) 65 | # If it's an HTMX request, return the messages list HTML 66 | if request.headers.get('HX-Request'): 67 | return await render_template('_message_list.html', messages=messages) 68 | # Otherwise return JSON for API consumers 69 | return jsonify([{ 70 | 'id': msg['id'], 71 | 'role': msg['role'], 72 | 'content': msg['content'], 73 | 'timestamp': msg['timestamp'].isoformat() 74 | } for msg in messages]) 75 | except Exception as e: 76 | logger.error(f"Error in api_chat_messages route: {e}") 77 | return jsonify({'error': 'Failed to load messages'}), 500 78 | 79 | @app.route('/api/chat/', methods=['DELETE']) 80 | async def delete_chat(chat_id): 81 | try: 82 | db.delete_chat(chat_id) 83 | return '', 204 84 | except Exception as e: 85 | logger.error(f"Error deleting chat: {e}") 86 | return jsonify({'error': 'Failed to delete chat'}), 500 87 | -------------------------------------------------------------------------------- /src/mcp_perplexity/web/templates/_chat_list.html: -------------------------------------------------------------------------------- 1 | {% if chats['chats'] %} 2 | {% for chat in chats['chats'] %} 3 |
  • 4 | 47 |
  • 48 | {% endfor %} 49 | 50 | {# Pagination Controls #} 51 | {% if chats['pagination']['pages'] > 1 %} 52 |
    53 |
    54 | {% if chats['pagination']['page'] > 1 %} 55 | 56 | Previous 57 | 58 | {% endif %} 59 | {% if chats['pagination']['page'] < chats['pagination']['pages'] %} 60 | 61 | Next 62 | 63 | {% endif %} 64 |
    65 | 106 |
    107 | {% endif %} 108 | {% else %} 109 |
  • 110 |
    111 | No chats found 112 |
    113 |
  • 114 | {% endif %} 115 | 116 | -------------------------------------------------------------------------------- /src/mcp_perplexity/web/templates/_dialog.html: -------------------------------------------------------------------------------- 1 | 2 | 34 | 35 | -------------------------------------------------------------------------------- /src/mcp_perplexity/web/templates/_message_list.html: -------------------------------------------------------------------------------- 1 | {% if messages %} 2 | {% for message in messages %} 3 |
    4 |
    5 |
    6 | {% if message['role'] == 'assistant' %} 7 |
    8 | AI 9 |
    10 | {% else %} 11 |
    12 | U 13 |
    14 | {% endif %} 15 |
    16 |
    17 |
    18 | {{ message['content'] | markdown | safe }} 19 |
    20 |
    21 | {{ message['timestamp'].strftime('%H:%M:%S') }} 22 |
    23 |
    24 |
    25 |
    26 | {% endfor %} 27 | {% else %} 28 |
    29 | No messages in this chat 30 |
    31 | {% endif %} -------------------------------------------------------------------------------- /src/mcp_perplexity/web/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {% block title %}MCP Perplexity{% endblock %} 8 | 9 | 10 | 11 | 12 | 13 | 48 | 49 | 50 | 231 | 232 | 233 | 234 | 235 | 308 | 309 | 310 | 311 | 324 | 325 |
    326 | {% block content %}{% endblock %} 327 |
    328 | 329 | 336 | 337 | {% include '_dialog.html' %} 338 | 339 | 340 | -------------------------------------------------------------------------------- /src/mcp_perplexity/web/templates/chat.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{{ chat['title'] or 'Untitled Chat' }} - MCP Perplexity{% endblock %} 4 | 5 | {% block content %} 6 |
    7 |
    8 |
    9 |
    10 |

    11 | {{ chat['title'] or 'Untitled Chat' }} 12 |

    13 |

    14 | Created {{ chat['created_at'].strftime('%Y-%m-%d %H:%M:%S') }} 15 |

    16 |
    17 |
    18 | 19 | ID: {{ chat['id'] }} 20 | 21 | 29 | 30 | 31 | 32 | Back to Chats 33 | 34 |
    35 |
    36 |
    37 | 38 |
    39 | 40 |
    41 | {% include '_message_list.html' %} 42 |
    43 |
    44 |
    45 | 46 | 47 | 54 | 55 | 147 | {% endblock %} -------------------------------------------------------------------------------- /src/mcp_perplexity/web/templates/error.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Error - MCP Perplexity{% endblock %} 4 | 5 | {% block content %} 6 |
    7 |
    8 |
    9 | 10 | 11 | 12 |
    13 |

    14 | An Error Occurred 15 |

    16 |

    17 | {{ error }} 18 |

    19 | 20 | Return to Home 21 | 22 |
    23 |
    24 | {% endblock %} -------------------------------------------------------------------------------- /src/mcp_perplexity/web/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Chats - MCP Perplexity{% endblock %} 4 | 5 | {% block content %} 6 |
    7 |
    8 |

    9 | Chat History 10 |

    11 |

    12 | Your conversations with Perplexity AI 13 |

    14 |
    15 | 16 |
    17 |
      22 | {% include '_chat_list.html' %} 23 |
    24 |
    25 |
    26 | {% endblock %} -------------------------------------------------------------------------------- /templates/.release_notes.md.j2: -------------------------------------------------------------------------------- 1 | ## What's Changed 2 | {% for type_, commits in release["elements"] | dictsort %} 3 | {% if type_ != "unknown" %} 4 | ### {% if type_ == "feat" %}🚀 Features{% elif type_ == "fix" %}🐛 Bug Fixes{% elif type_ == "perf" %}⚡ Performance Improvements{% elif type_ == "docs" %}📚 Documentation{% elif type_ == "style" %}💅 Code Style{% elif type_ == "refactor" %}🔨 Code Refactoring{% elif type_ == "test" %}🧪 Tests{% elif type_ == "build" %}🛠️ Build System{% elif type_ == "ci" %}🔄 Continuous Integration{% elif type_ == "chore" %}🧹 Maintenance{% else %}{{ type_ | title }}{% endif %} 5 | 6 | {% for commit in commits %} 7 | * {{ commit.descriptions[0] | capitalize }} by {{ commit.commit.author.name }} in [`{{ commit.hexsha[:7] }}`]({{ commit.hexsha | commit_hash_url }}){% if commit.breaking %} **BREAKING CHANGE** {% endif %} 8 | {% endfor %} 9 | {% endif %} 10 | {% endfor %} 11 | 12 | --- 13 | 14 | {% if version.patch > 0 %} 15 | **Full Changelog**: https://github.com/daniel-lxs/mcp-perplexity/compare/v{{ version.major }}.{{ version.minor }}.{{ version.patch - 1 }}...v{{ version.major }}.{{ version.minor }}.{{ version.patch }} 16 | {% else %} 17 | **Full Changelog**: https://github.com/daniel-lxs/mcp-perplexity/releases/tag/v{{ version.major }}.{{ version.minor }}.{{ version.patch }} 18 | {% endif %} -------------------------------------------------------------------------------- /templates/CHANGELOG.md.j2: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | {% for version, release in context.history.released.items() %} 6 | ## {{ version.as_tag() }} ({{ release.tagged_date.strftime("%Y-%m-%d") }}) 7 | 8 | {% for type_, commits in release["elements"] | dictsort %} 9 | {% if type_ != "unknown" %} 10 | ### {% if type_ == "feat" %}🚀 Features{% elif type_ == "fix" %}🐛 Bug Fixes{% elif type_ == "perf" %}⚡ Performance Improvements{% elif type_ == "docs" %}📚 Documentation{% elif type_ == "style" %}💅 Code Style{% elif type_ == "refactor" %}🔨 Code Refactoring{% elif type_ == "test" %}🧪 Tests{% elif type_ == "build" %}🛠️ Build System{% elif type_ == "ci" %}🔄 Continuous Integration{% elif type_ == "chore" %}🧹 Maintenance{% else %}{{ type_ | title }}{% endif %} 11 | 12 | {% for commit in commits %} 13 | * {{ commit.descriptions[0] | capitalize }} ([`{{ commit.hexsha[:7] }}`]({{ commit.hexsha | commit_hash_url }})){% if commit.breaking %} **BREAKING CHANGE** {% endif %} 14 | {% endfor %} 15 | {% endif %} 16 | {% endfor %} 17 | 18 | {% endfor %} -------------------------------------------------------------------------------- /tests/test_perplexity_api.py: -------------------------------------------------------------------------------- 1 | import os 2 | import asyncio 3 | import json 4 | import httpx 5 | 6 | PERPLEXITY_API_KEY = os.getenv("PERPLEXITY_API_KEY") 7 | API_URL = "https://api.perplexity.ai/chat/completions" 8 | 9 | async def test_perplexity_query(): 10 | system_prompt = """You are an expert assistant providing accurate answers using real-time web searches. 11 | Your responses must: 12 | 1. Be based on the most relevant web sources 13 | 2. Include source citations for all factual claims 14 | 3. If no relevant results are found, suggest 2-3 alternative search queries 15 | 4. Prioritize technical accuracy, especially for programming-related questions""" 16 | 17 | test_query = "Explain the Python GIL in detail" 18 | 19 | try: 20 | async with httpx.AsyncClient() as client: 21 | response = await client.post( 22 | API_URL, 23 | headers={ 24 | "Authorization": f"Bearer {PERPLEXITY_API_KEY}", 25 | "Content-Type": "application/json", 26 | }, 27 | json={ 28 | "model": "sonar", 29 | "messages": [ 30 | {"role": "system", "content": system_prompt.strip()}, 31 | {"role": "user", "content": test_query} 32 | ], 33 | "stream": True 34 | }, 35 | timeout=30.0 36 | ) 37 | response.raise_for_status() 38 | 39 | print("✅ API request successful. Streaming response...") 40 | 41 | full_response = "" 42 | citations = set() 43 | usage = {} 44 | 45 | async for chunk in response.aiter_text(): 46 | for line in chunk.split('\n'): 47 | line = line.strip() 48 | if line.startswith("data: "): 49 | try: 50 | data = json.loads(line[6:]) 51 | # Collect citations if present 52 | if "citations" in data: 53 | for citation in data["citations"]: 54 | citations.add(citation) 55 | # Existing content handling 56 | if data.get("choices"): 57 | content = data["choices"][0]["delta"].get("content", "") 58 | full_response += content 59 | print(f"Received chunk: {content}") 60 | # Add usage tracking 61 | if "usage" in data: 62 | usage.update(data["usage"]) 63 | except json.JSONDecodeError: 64 | continue 65 | elif line == "[DONE]": 66 | print("Stream completed") 67 | 68 | print("\n📝 Full response:") 69 | print(full_response) 70 | 71 | # Print collected citations 72 | if citations: 73 | print("\n🔍 Citations:") 74 | for idx, citation in enumerate(citations, 1): 75 | print(f"{idx}. {citation}") 76 | else: 77 | print("\n⚠️ No citations found in response") 78 | 79 | # Add usage display 80 | print("\n📊 Usage Stats:") 81 | print(json.dumps(usage, indent=2)) 82 | 83 | except Exception as e: 84 | print(f"❌ API request failed: {str(e)}") 85 | 86 | if __name__ == "__main__": 87 | if not PERPLEXITY_API_KEY: 88 | print("❌ Missing PERPLEXITY_API_KEY environment variable") 89 | else: 90 | asyncio.run(test_perplexity_query()) -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by uv via the following command: 2 | # uv pip compile --python-version 3.10 pyproject.toml -o uv.lock 3 | annotated-types==0.7.0 4 | # via pydantic 5 | anyio==4.8.0 6 | # via 7 | # httpx 8 | # mcp 9 | # sse-starlette 10 | # starlette 11 | certifi==2025.1.31 12 | # via 13 | # httpcore 14 | # httpx 15 | click==8.1.8 16 | # via uvicorn 17 | exceptiongroup==1.2.2 18 | # via anyio 19 | h11==0.14.0 20 | # via 21 | # httpcore 22 | # uvicorn 23 | haikunator==2.1.0 24 | # via mcp-perplexity (pyproject.toml) 25 | httpcore==1.0.7 26 | # via httpx 27 | httpx==0.28.1 28 | # via 29 | # mcp-perplexity (pyproject.toml) 30 | # mcp 31 | httpx-sse==0.4.0 32 | # via mcp 33 | idna==3.10 34 | # via 35 | # anyio 36 | # httpx 37 | mcp==1.2.1 38 | # via mcp-perplexity (pyproject.toml) 39 | pydantic==2.10.6 40 | # via 41 | # mcp 42 | # pydantic-settings 43 | pydantic-core==2.27.2 44 | # via pydantic 45 | pydantic-settings==2.7.1 46 | # via mcp 47 | python-dotenv==1.0.1 48 | # via pydantic-settings 49 | sniffio==1.3.1 50 | # via anyio 51 | sse-starlette==2.2.1 52 | # via mcp 53 | starlette==0.45.3 54 | # via 55 | # mcp 56 | # sse-starlette 57 | typing-extensions==4.12.2 58 | # via 59 | # anyio 60 | # pydantic 61 | # pydantic-core 62 | # uvicorn 63 | uvicorn==0.34.0 64 | # via mcp 65 | --------------------------------------------------------------------------------