├── .github └── workflows │ └── publish-to-pypi.yml ├── .gitignore ├── .python-version ├── Dockerfile ├── README-ko.md ├── README.md ├── pyproject.toml ├── smithery.yaml ├── src └── perplexity_advanced_mcp │ ├── __init__.py │ ├── api_client.py │ ├── cli.py │ ├── config.py │ ├── logging.py │ ├── search_tool.py │ └── types.py └── uv.lock /.github/workflows/publish-to-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python 🐍 distribution 📦 to PyPI 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | name: Build distribution 📦 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | persist-credentials: false 14 | - name: Set up Python 15 | uses: actions/setup-python@v5 16 | with: 17 | python-version: "3.12" 18 | - name: Install pypa/build 19 | run: >- 20 | python3 -m 21 | pip install 22 | build 23 | --user 24 | - name: Build a binary wheel and a source tarball 25 | run: python3 -m build 26 | - name: Store the distribution packages 27 | uses: actions/upload-artifact@v4 28 | with: 29 | name: python-package-distributions 30 | path: dist/ 31 | 32 | publish-to-pypi: 33 | name: >- 34 | Publish Python 🐍 distribution 📦 to PyPI 35 | if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes 36 | needs: 37 | - build 38 | runs-on: ubuntu-latest 39 | environment: 40 | name: release 41 | url: https://pypi.org/p/perplexity-advanced-mcp 42 | permissions: 43 | id-token: write # IMPORTANT: mandatory for trusted publishing 44 | 45 | steps: 46 | - name: Download all the dists 47 | uses: actions/download-artifact@v4 48 | with: 49 | name: python-package-distributions 50 | path: dist/ 51 | - name: Publish distribution 📦 to PyPI 52 | uses: pypa/gh-action-pypi-publish@release/v1 53 | 54 | github-release: 55 | name: >- 56 | Sign the Python 🐍 distribution 📦 with Sigstore 57 | and upload them to GitHub Release 58 | needs: 59 | - publish-to-pypi 60 | runs-on: ubuntu-latest 61 | 62 | permissions: 63 | contents: write # IMPORTANT: mandatory for making GitHub Releases 64 | id-token: write # IMPORTANT: mandatory for sigstore 65 | 66 | steps: 67 | - name: Download all the dists 68 | uses: actions/download-artifact@v4 69 | with: 70 | name: python-package-distributions 71 | path: dist/ 72 | - name: Sign the dists with Sigstore 73 | uses: sigstore/gh-action-sigstore-python@v3.0.0 74 | with: 75 | inputs: >- 76 | ./dist/*.tar.gz 77 | ./dist/*.whl 78 | - name: Create GitHub Release 79 | env: 80 | GITHUB_TOKEN: ${{ github.token }} 81 | run: >- 82 | gh release create 83 | "$GITHUB_REF_NAME" 84 | --repo "$GITHUB_REPOSITORY" 85 | --notes "" 86 | - name: Upload artifact signatures to GitHub Release 87 | env: 88 | GITHUB_TOKEN: ${{ github.token }} 89 | run: >- 90 | gh release upload 91 | "$GITHUB_REF_NAME" dist/** 92 | --repo "$GITHUB_REPOSITORY" 93 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/python 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # poetry 102 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 103 | # This is especially recommended for binary packages to ensure reproducibility, and is more 104 | # commonly ignored for libraries. 105 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 106 | #poetry.lock 107 | 108 | # pdm 109 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 110 | #pdm.lock 111 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 112 | # in version control. 113 | # https://pdm.fming.dev/#use-with-ide 114 | .pdm.toml 115 | 116 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 117 | __pypackages__/ 118 | 119 | # Celery stuff 120 | celerybeat-schedule 121 | celerybeat.pid 122 | 123 | # SageMath parsed files 124 | *.sage.py 125 | 126 | # Environments 127 | .env 128 | .venv 129 | env/ 130 | venv/ 131 | ENV/ 132 | env.bak/ 133 | venv.bak/ 134 | 135 | # Spyder project settings 136 | .spyderproject 137 | .spyproject 138 | 139 | # Rope project settings 140 | .ropeproject 141 | 142 | # mkdocs documentation 143 | /site 144 | 145 | # mypy 146 | .mypy_cache/ 147 | .dmypy.json 148 | dmypy.json 149 | 150 | # Pyre type checker 151 | .pyre/ 152 | 153 | # pytype static type analyzer 154 | .pytype/ 155 | 156 | # Cython debug symbols 157 | cython_debug/ 158 | 159 | # PyCharm 160 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 161 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 162 | # and can be added to the global gitignore or merged into this file. For a more nuclear 163 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 164 | #.idea/ 165 | 166 | ### Python Patch ### 167 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration 168 | poetry.toml 169 | 170 | # ruff 171 | .ruff_cache/ 172 | 173 | # LSP config files 174 | pyrightconfig.json 175 | 176 | # End of https://www.toptal.com/developers/gitignore/api/python 177 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.12 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile 2 | # Use a Python image with uv pre-installed 3 | FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS uv 4 | 5 | # Set the working directory 6 | WORKDIR /app 7 | 8 | # Copy the project files into the container 9 | COPY . . 10 | 11 | # Install the project's dependencies using the lockfile and settings 12 | RUN --mount=type=cache,target=/root/.cache/uv \ 13 | --mount=type=bind,source=uv.lock,target=uv.lock \ 14 | --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ 15 | uv sync --frozen --no-install-project --no-dev --no-editable 16 | 17 | # Then, add the rest of the project source code and install it 18 | ADD . /app 19 | RUN --mount=type=cache,target=/root/.cache/uv \ 20 | uv sync --frozen --no-dev --no-editable 21 | 22 | # Define the base image for runtime 23 | FROM python:3.12-slim-bookworm 24 | 25 | # Set the working directory 26 | WORKDIR /app 27 | 28 | # Copy necessary files from the build stage 29 | COPY --from=uv /root/.local /root/.local 30 | COPY --from=uv --chown=app:app /app/.venv /app/.venv 31 | 32 | # Place executables in the environment at the front of the path 33 | ENV PATH="/app/.venv/bin:$PATH" 34 | 35 | # Set the entrypoint for the Docker container 36 | ENTRYPOINT ["perplexity-advanced-mcp"] 37 | -------------------------------------------------------------------------------- /README-ko.md: -------------------------------------------------------------------------------- 1 | # Perplexity Advanced MCP 2 | 3 |
4 | 5 | [![GitHub](https://img.shields.io/badge/GitHub-100000?style=for-the-badge&logo=github&logoColor=white)](https://github.com/code-yeongyu/perplexity-advanced-mcp) 6 | [![PyPI](https://img.shields.io/badge/pypi-3775A9?style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/perplexity-advanced-mcp) 7 | 8 |
9 | 10 | --- 11 | 12 | ## 개요 13 | 14 | Perplexity Advanced MCP는 [OpenRouter](https://openrouter.ai/)와 [Perplexity](https://docs.perplexity.ai/home) API를 활용하여 향상된 쿼리 처리 기능을 제공하는 고급 통합 패키지입니다. 직관적인 명령줄 인터페이스와 강력한 API 클라이언트를 통해 단순 및 복잡한 쿼리 모두에 대해 AI 모델과의 원활한 상호작용을 지원합니다. 15 | 16 | ## [perplexity-mcp](https://github.com/jsonallen/perplexity-mcp)와의 비교 17 | 18 | [perplexity-mcp](https://github.com/jsonallen/perplexity-mcp)가 [Perplexity](https://docs.perplexity.ai/home) AI의 API를 사용한 기본적인 웹 검색 기능을 제공하는 반면, Perplexity Advanced MCP는 다음과 같은 추가 기능을 제공합니다: 19 | 20 | - **다중 공급자 지원:** [Perplexity](https://docs.perplexity.ai/home)와 [OpenRouter](https://openrouter.ai/) API를 모두 지원하여 공급자 선택의 유연성 제공 21 | - **쿼리 타입 최적화:** 단순 쿼리와 복잡한 쿼리를 구분하여 비용과 성능을 최적화 22 | - **파일 첨부 지원:** 쿼리에 파일 내용을 컨텍스트로 포함하여 더 정확하고 맥락에 맞는 응답 제공 23 | - **향상된 재시도 로직:** 안정성 향상을 위한 강력한 재시도 메커니즘 구현 24 | 25 | 전반적으로, 이는 [Cline](https://cline.bot/)이나 [Cursor](https://www.cursor.com/)와 같은 에디터와 통합할 때 코드베이스를 다루는 데 가장 적합한 MCP입니다. 26 | 27 | ## 기능 28 | 29 | - **통합 API 클라이언트:** 단순 및 복잡한 쿼리 처리를 위한 구성 가능한 모델과 함께 [OpenRouter](https://openrouter.ai/)와 [Perplexity](https://docs.perplexity.ai/home) API 지원 30 | - **명령줄 인터페이스 (CLI):** [Typer](https://typer.tiangolo.com/)를 사용한 API 키 구성 및 MCP 서버 실행 관리 31 | - **고급 쿼리 처리:** 쿼리에 컨텍스트 데이터를 포함할 수 있는 파일 첨부 처리 기능 통합 32 | - **강력한 재시도 메커니즘:** 일관되고 안정적인 API 통신을 위한 Tenacity 기반 재시도 로직 33 | - **사용자 정의 가능한 로깅:** 상세한 디버깅 및 런타임 모니터링을 위한 유연한 로깅 구성 34 | 35 | ## 최적의 AI 구성 36 | 37 | AI 어시스턴트([Cursor](https://www.cursor.com/), [Claude for Desktop](https://claude.ai/download) 등)와의 최상의 경험을 위해 프로젝트 지침이나 AI 규칙에 다음 구성을 추가하는 것을 권장합니다: 38 | 39 | ```xml 40 | 41 | 42 | Perplexity는 인터넷을 검색하고, 정보를 수집하며, 사용자의 질문에 답변할 수 있는 LLM입니다. 43 | 44 | 예를 들어, Python의 최신 버전을 알아내고 싶다고 가정해 봅시다. 45 | 1. Google에서 검색합니다. 46 | 2. 상위 2-3개의 결과를 직접 읽고 확인합니다. 47 | 48 | Perplexity가 이 작업을 대신 수행합니다. 49 | 50 | 사용자의 질문에 답하기 위해 Perplexity는 검색을 수행하고, 상위 검색 결과를 열어 해당 웹사이트에서 정보를 찾은 다음 답변을 제공합니다. 51 | 52 | Perplexity는 단순 및 복잡한 두 가지 유형의 쿼리에 사용할 수 있습니다. 사용자의 요청을 충족시키기 위해 적절한 쿼리 유형을 선택하는 것이 가장 중요합니다. 53 | 54 | 55 | 56 | 저렴하고 빠릅니다. 하지만 복잡한 쿼리에는 적합하지 않습니다. 평균적으로 복잡한 쿼리보다 10배 이상 저렴하고 3배 더 빠릅니다. 57 | "Python의 최신 버전은 무엇인가요?"와 같은 간단한 질문에 사용합니다. 58 | 59 | 60 | 입력 토큰당 $1/M 61 | 출력 토큰당 $1/M 62 | 63 | 64 | 65 | 66 | 67 | 더 느리고 비쌉니다. 단순 쿼리와 비교하여 평균적으로 10배 이상 비싸고 3배 더 느립니다. 68 | "첨부된 코드를 분석하여 특정 라이브러리의 현재 상태를 검토하고 마이그레이션 계획을 수립하세요"와 같이 여러 단계의 추론이나 심층 분석이 필요한 요청에 사용합니다. 69 | 70 | 71 | 입력 토큰당 $1/M 72 | 출력 토큰당 $5/M 73 | 74 | 75 | 76 | 77 | 사용자의 요청을 검토할 때 예상치 못하거나, 불확실하거나, 의문스러운 점이 있다면, **그리고 인터넷에서 답을 얻을 수 있다고 생각된다면** "ask_perplexity" 도구를 사용하여 Perplexity에 문의하는 것을 주저하지 마세요. 하지만 인터넷이 사용자의 요청을 만족시키는 데 필요하지 않다면, perplexity에 문의하는 것은 의미가 없습니다. 78 | Perplexity도 LLM이므로 프롬프트 엔지니어링 기법이 매우 중요합니다. 79 | 명확한 지침 제공, 충분한 컨텍스트, 예시 제공 등 프롬프트 엔지니어링의 기본을 기억하세요. 80 | 사용자의 요청을 원활하게 충족시키기 위해 가능한 한 많은 컨텍스트와 관련 파일을 포함하세요. 파일을 첨부할 때는 반드시 절대 경로를 사용해야 합니다. 81 | 82 | 83 | ``` 84 | 85 | 이 구성은 AI 어시스턴트가 Perplexity 검색 기능을 언제 어떻게 사용할지 더 잘 이해하도록 도와주며, 비용과 성능 모두를 최적화합니다. 86 | 87 | ## 사용법 88 | 89 | ### [uvx](https://docs.astral.sh/uv/guides/tools/)를 사용한 빠른 시작 90 | 91 | MCP 서버를 실행하는 가장 쉬운 방법은 [uvx](https://docs.astral.sh/uv/guides/tools/)를 사용하는 것입니다: 92 | 93 | ```sh 94 | uvx perplexity-advanced-mcp -o # 또는 -p 95 | ``` 96 | 97 | 환경 변수를 사용하여 API 키를 구성할 수도 있습니다: 98 | 99 | ```sh 100 | export OPENROUTER_API_KEY="your_key_here" 101 | # 또는 102 | export PERPLEXITY_API_KEY="your_key_here" 103 | 104 | uvx perplexity-advanced-mcp 105 | ``` 106 | 107 | 참고: 108 | - OpenRouter와 Perplexity API 키를 동시에 제공하면 오류가 발생합니다 109 | - CLI 인수와 환경 변수가 모두 제공된 경우 CLI 인수가 우선합니다 110 | 111 | CLI는 [Typer](https://typer.tiangolo.com/)로 구축되어 사용자 친화적인 명령줄 경험을 제공합니다. 112 | 113 | ### MCP 검색 도구 114 | 115 | 이 패키지는 `ask_perplexity` 함수를 통해 통합된 MCP 검색 도구를 포함합니다. 단순 및 복잡한 쿼리를 모두 지원하며 추가 컨텍스트를 제공하기 위한 파일 첨부를 처리합니다. 116 | 117 | - **단순 쿼리:** 빠르고 효율적인 응답 제공 118 | - **복잡한 쿼리:** 상세한 추론을 수행하고 XML 형식의 파일 첨부를 지원 119 | 120 | ## 구성 121 | 122 | - **API 키:** 명령줄 옵션이나 환경 변수를 통해 `OPENROUTER_API_KEY` 또는 `PERPLEXITY_API_KEY` 구성 123 | - **모델 선택:** 구성(`src/perplexity_advanced_mcp/config.py`)에서 쿼리 유형을 특정 모델에 매핑: 124 | - **[OpenRouter](https://openrouter.ai/):** 125 | - 단순 쿼리: `perplexity/sonar` 126 | - 복잡한 쿼리: `perplexity/sonar-reasoning` 127 | - **[Perplexity](https://docs.perplexity.ai/home):** 128 | - 단순 쿼리: `sonar-pro` 129 | - 복잡한 쿼리: `sonar-reasoning-pro` 130 | 131 | ## 개발 배경 및 철학 132 | 133 | 이 프로젝트는 개인적인 호기심과 실험에서 시작되었습니다. 최근의 ["vibe coding"](https://x.com/karpathy/status/1886192184808149383) 트렌드를 따라, 코드의 95% 이상이 [Cline](https://cline.bot/) + [Cursor](https://www.cursor.com/) IDE를 통해 작성되었습니다. "말은 쉽고 코드를 보여달라"고들 하죠 - [Wispr Flow](https://wisprflow.ai/)의 음성-텍스트 변환 마법 덕분에, 저는 말 그대로 이야기를 했고 코드가 나타났습니다! 대부분의 개발은 "x y z에 대한 코드를 작성해줘, x y z의 버그를 수정해줘"라고 말하고 엔터를 누르는 것으로 이루어졌습니다. 놀랍게도 이 완전히 기능하는 프로젝트를 만드는 데 몇 시간도 걸리지 않았습니다. 134 | 135 | 프로젝트 스캐폴딩부터 파일 구조까지, 모든 것이 LLM을 통해 작성되고 검토되었습니다. GitHub Actions 워크플로우를 통한 PyPI 배포와 릴리즈 승인 과정까지도 Cursor를 통해 처리되었습니다. 인간 개발자로서 제 역할은 다음과 같았습니다: 136 | 137 | - AI가 적절한 테스트를 수행할 수 있도록 MCP 서버 시작 및 중지 138 | - 오류 발생 시 로그 복사 및 제공 139 | - 인터넷에서 [Python MCP SDK](https://github.com/modelcontextprotocol/python-sdk) 문서와 예제 찾아서 제공 140 | - 올바르지 않아 보이는 코드에 대한 수정 요청 141 | - AI가 전체 CI/CD 파이프라인을 설정한 후 최종 릴리즈 승인 142 | 143 | 많은 것들이 자동화되고 대체될 수 있는 오늘날의 세상에서, 이 MCP가 여러분과 같은 개발자들이 단순히 코드를 작성하는 것을 넘어서는 가치를 발견하는 데 도움이 되기를 바랍니다. 이 도구가 여러분이 더 높은 수준의 결정과 고려사항을 다루는 새로운 시대의 개발자가 되는 데 도움이 되기를 바랍니다. 144 | 145 | ## 개발 146 | 147 | 이 패키지에 기여하거나 수정하려면: 148 | 149 | ### 1. **저장소 복제:** 150 | 151 | ```sh 152 | gh repo clone code-yeongyu/perplexity-advanced-mcp 153 | ``` 154 | 155 | ### 2. **의존성 설치:** 156 | 157 | ```sh 158 | uv sync 159 | ``` 160 | 161 | ### 3. **기여:** 162 | 163 | 기여는 환영합니다! 기존 코드 스타일과 커밋 가이드라인을 따라주세요. 164 | 165 | ## 라이선스 166 | 167 | 이 프로젝트는 MIT 라이선스를 따릅니다. 168 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![MseeP.ai Security Assessment Badge](https://mseep.net/pr/code-yeongyu-perplexity-advanced-mcp-badge.png)](https://mseep.ai/app/code-yeongyu-perplexity-advanced-mcp) 2 | 3 |
4 | 5 | # Perplexity Advanced MCP 6 | 7 | [![GitHub](https://img.shields.io/badge/GitHub-100000?style=for-the-badge&logo=github&logoColor=white)](https://github.com/code-yeongyu/perplexity-advanced-mcp) 8 | [![PyPI](https://img.shields.io/badge/pypi-3775A9?style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/perplexity-advanced-mcp) 9 | [![smithery badge](https://smithery.ai/badge/@code-yeongyu/perplexity-advanced-mcp)](https://smithery.ai/server/@code-yeongyu/perplexity-advanced-mcp) 10 | 11 | [한국어](README-ko.md) 12 | 13 |
14 | 15 | --- 16 | 17 | ## Overview 18 | 19 | Perplexity Advanced MCP is an advanced integration package that leverages the [OpenRouter](https://openrouter.ai/) and [Perplexity](https://docs.perplexity.ai/home) APIs to provide enhanced query processing capabilities. With an intuitive command-line interface and a robust API client, this package facilitates seamless interactions with AI models for both simple and complex queries. 20 | 21 | ## Comparison with [perplexity-mcp](https://github.com/jsonallen/perplexity-mcp) 22 | 23 | While [perplexity-mcp](https://github.com/jsonallen/perplexity-mcp) provides basic web search functionality using [Perplexity](https://docs.perplexity.ai/home) AI's API, Perplexity Advanced MCP offers several additional features: 24 | 25 | - **Multi-vendor Support:** Supports both [Perplexity](https://docs.perplexity.ai/home) and [OpenRouter](https://openrouter.ai/) APIs, giving you flexibility in choosing your provider 26 | - **Query Type Optimization:** Distinguishes between simple and complex queries, optimizing for cost and performance 27 | - **File Attachment Support:** Allows including file contents as context in your queries, enabling more precise and contextual responses 28 | - **Enhanced Retry Logic:** Implements robust retry mechanisms for improved reliability 29 | 30 | Overall, this is the most suitable MCP for handling codebases when integrated with editors like [Cline](https://cline.bot/) or [Cursor](https://www.cursor.com/). 31 | 32 | 33 | ## Features 34 | 35 | - **Unified API Client:** Supports both [OpenRouter](https://openrouter.ai/) and [Perplexity](https://docs.perplexity.ai/home) APIs with configurable models for handling simple and complex queries. 36 | - **Command-Line Interface (CLI):** Manage API key configuration and run the MCP server using [Typer](https://typer.tiangolo.com/). 37 | - **Advanced Query Processing:** Incorporates file attachment processing, allowing you to include contextual data in your queries. 38 | - **Robust Retry Mechanism:** Utilizes Tenacity for retry logic to ensure consistent and reliable API communications. 39 | - **Customizable Logging:** Flexible logging configuration for detailed debugging and runtime monitoring. 40 | 41 | ## Optimal AI Configuration 42 | 43 | For the best experience with AI assistants (e.g., [Cursor](https://www.cursor.com/), [Claude for Desktop](https://claude.ai/download)), I recommend adding the following configuration to your project instructions or AI rules: 44 | 45 | ```xml 46 | 47 | 48 | Perplexity is an LLM that can search the internet, gather information, and answer users' queries. 49 | 50 | For example, let's suppose we want to find out the latest version of Python. 51 | 1. You would search on Google. 52 | 2. Then read the top two or three results directly to verify. 53 | 54 | Perplexity does that work for you. 55 | 56 | To answer a user's query, Perplexity searches, opens the top search results, finds information on those websites, and then provides the answer. 57 | 58 | Perplexity can be used with two types of queries: simple and complex. Choosing the right query type to fulfill the user's request is most important. 59 | 60 | 61 | 62 | It's cheap and fast. However, it's not suitable for complex queries. On average, it's more than 10 times cheaper and 3 times faster than complex queries. 63 | Use it for simple questions such as "What is the latest version of Python?" 64 | 65 | 66 | $1/M input tokens 67 | $1/M output tokens 68 | 69 | 70 | 71 | 72 | 73 | It's slower and more expensive. Compared to simple queries, it's on average more than 10 times more expensive and 3 times slower. 74 | Use it for more complex requests like "Analyze the attached code to examine the current status of a specific library and create a migration plan." 75 | 76 | 77 | $1/M input tokens 78 | $5/M output tokens 79 | 80 | 81 | 82 | 83 | When reviewing the user's request, if you find anything unexpected, uncertain, or questionable, **and you think you can get answer from the internet**, do not hesitate to use the "ask_perplexity" tool to consult Perplexity. However, if the internet is not required to satisfy users' request, it's meaningless to ask to perplexity. 84 | Since Perplexity is also an LLM, prompt engineering techniques are paramount. 85 | Remember the basics of prompt engineering, such as providing clear instructions, sufficient context, and examples 86 | Include as much context and relevant files as possible to smoothly fulfill the user's request. When adding files as attachments, make sure they are absolute paths. 87 | 88 | 89 | ``` 90 | 91 | This configuration helps AI assistants better understand when and how to use the Perplexity search functionality, optimizing for both cost and performance. 92 | 93 | ## Usage 94 | 95 | ### Installing via Smithery 96 | 97 | To install Perplexity Advanced MCP for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@code-yeongyu/perplexity-advanced-mcp): 98 | 99 | ```bash 100 | npx -y @smithery/cli install @code-yeongyu/perplexity-advanced-mcp --client claude 101 | ``` 102 | 103 | ### Quick Start with [uvx](https://docs.astral.sh/uv/guides/tools/) 104 | 105 | The easiest way to run the MCP server is using [uvx](https://docs.astral.sh/uv/guides/tools/): 106 | 107 | ```sh 108 | uvx perplexity-advanced-mcp -o # or -p 109 | ``` 110 | 111 | You can also configure the API keys using environment variables: 112 | 113 | ```sh 114 | export OPENROUTER_API_KEY="your_key_here" 115 | # or 116 | export PERPLEXITY_API_KEY="your_key_here" 117 | 118 | uvx perplexity-advanced-mcp 119 | ``` 120 | 121 | Note: 122 | - Providing both OpenRouter and Perplexity API keys simultaneously will result in an error 123 | - When both CLI arguments and environment variables are provided, CLI arguments take precedence 124 | 125 | The CLI is built with [Typer](https://typer.tiangolo.com/), ensuring a user-friendly command-line experience. 126 | 127 | ### MCP Search Tool 128 | 129 | The package includes an MCP search tool integrated via the `ask_perplexity` function. It supports both simple and complex queries and processes file attachments to provide additional context. 130 | 131 | - **Simple Queries:** Provides fast, efficient responses. 132 | - **Complex Queries:** Engages in detailed reasoning and supports file attachments formatted as XML. 133 | 134 | ## Configuration 135 | 136 | - **API Keys:** Configure either the `OPENROUTER_API_KEY` or `PERPLEXITY_API_KEY` through command-line options or environment variables. 137 | - **Model Selection:** The configuration (in `src/perplexity_advanced_mcp/config.py`) maps query types to specific models: 138 | - **[OpenRouter](https://openrouter.ai/):** 139 | - Simple Queries: `perplexity/sonar` 140 | - Complex Queries: `perplexity/sonar-reasoning` 141 | - **[Perplexity](https://docs.perplexity.ai/home):** 142 | - Simple Queries: `sonar-pro` 143 | - Complex Queries: `sonar-reasoning-pro` 144 | 145 | ## Development Background & Philosophy 146 | 147 | This project emerged from my personal curiosity and experimentation. Following the recent ["vibe coding"](https://x.com/karpathy/status/1886192184808149383) trend, over 95% of the code was written through [Cline](https://cline.bot/) + [Cursor](https://www.cursor.com/) IDE. They say "talk is cheap, show me the code" - well, with [Wispr Flow](https://wisprflow.ai/)'s speech-to-text magic, I literally just talked and the code showed up! Most of the development was done by me saying things like "Write me the code for x y z, fix the bug here x y z." and pressing enter. Remarkably, creating this fully functional project took less than a few hours. 148 | 149 | From project scaffolding to file structure, everything was written and reviewed through LLM. Even the GitHub Actions workflow for PyPI publishing and the release approval process were handled through Cursor. As a human developer, my role was to: 150 | 151 | - Starting and stopping the MCP server to help AI conduct proper testing 152 | - Copying and providing error logs when issues occurred 153 | - Finding and providing [Python MCP SDK](https://github.com/modelcontextprotocol/python-sdk) documentation and examples from the internet 154 | - Requesting modifications for code that didn't seem correct 155 | 156 | In today's world where many things can be automated and replaced, I hope this MCP can help developers like you who use it to discover value beyond just writing code. May this tool assist you in becoming a new era developer who can make higher-level decisions and considerations. 157 | 158 | ## Development 159 | 160 | To contribute or modify this package: 161 | 162 | ### 1. **Clone the Repository:** 163 | 164 | ```sh 165 | gh repo clone code-yeongyu/perplexity-advanced-mcp 166 | ``` 167 | 168 | ### 2. **Install Dependencies:** 169 | 170 | ```sh 171 | uv sync 172 | ``` 173 | 174 | ### 3. **Contribute:** 175 | 176 | Contributions are welcome! Please follow the existing code style and commit guidelines. 177 | 178 | ## License 179 | 180 | This project is licensed under the MIT License. 181 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "perplexity-advanced-mcp" 3 | version = "0.1.3" 4 | description = """ 5 | Advanced MCP tool for Perplexity and OpenRouter API integration. 6 | Supports both simple and complex queries with file attachments. 7 | Built with AI-first development approach using Cline, Cursor, and Wispr Flow. 8 | """ 9 | authors = [{ name = "YeonGyu Kim", email = "public.kim.yeon.gyu@gmail.com" }] 10 | dependencies = [ 11 | "httpx>=0.28.1", 12 | "mcp>=1.3.0", 13 | "pydantic>=2.10.6", 14 | "setuptools>=75.8.0", 15 | "typer>=0.15.1", 16 | "tenacity>=8.2.3", 17 | ] 18 | requires-python = ">=3.12" 19 | readme = "README.md" 20 | license = { text = "MIT" } 21 | keywords = [ 22 | "mcp", 23 | "perplexity", 24 | "openrouter", 25 | "ai", 26 | "llm", 27 | "search", 28 | "claude", 29 | "cursor", 30 | "cline", 31 | ] 32 | classifiers = [ 33 | "Environment :: Console", 34 | "Intended Audience :: Developers", 35 | "License :: OSI Approved :: MIT License", 36 | "Operating System :: OS Independent", 37 | "Programming Language :: Python :: 3.12", 38 | "Topic :: Software Development :: Libraries :: Python Modules", 39 | ] 40 | 41 | [project.urls] 42 | Homepage = "https://github.com/code-yeongyu/perplexity-advanced-mcp" 43 | Repository = "https://github.com/code-yeongyu/perplexity-advanced-mcp.git" 44 | "Bug Tracker" = "https://github.com/code-yeongyu/perplexity-advanced-mcp/issues" 45 | 46 | [project.scripts] 47 | perplexity-advanced-mcp = "perplexity_advanced_mcp.cli:app" 48 | 49 | [build-system] 50 | requires = ["hatchling"] 51 | build-backend = "hatchling.build" 52 | 53 | [project.optional-dependencies] 54 | dev = ["mypy>=1.15.0", "ruff>=0.9.6"] 55 | 56 | [tool.ruff] 57 | line-length = 119 58 | target-version = "py312" 59 | lint.select = [ 60 | "E", # pycodestyle errors 61 | "W", # pycodestyle warnings 62 | "F", # pyflakes 63 | "I", # isort 64 | "UP", # pyupgrade 65 | "PT", # pytest 66 | ] 67 | 68 | [tool.ruff.lint.flake8-quotes] 69 | inline-quotes = "double" 70 | docstring-quotes = "double" 71 | multiline-quotes = "double" 72 | -------------------------------------------------------------------------------- /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 | - apiKey 10 | - apiType 11 | properties: 12 | apiKey: 13 | type: string 14 | description: API key for either OpenRouter or Perplexity. 15 | apiType: 16 | type: string 17 | description: "Specifies which API to use: openrouter or perplexity." 18 | commandFunction: 19 | # A function that produces the CLI command to start the MCP on stdio. 20 | |- 21 | (config) => ({ command: 'perplexity-advanced-mcp', args: [`-${config.apiType === 'openrouter' ? 'o' : 'p'}`, config.apiKey] }) 22 | -------------------------------------------------------------------------------- /src/perplexity_advanced_mcp/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Perplexity Advanced MCP Integration 3 | 4 | An advanced MCP tool that seamlessly integrates OpenRouter and Perplexity APIs 5 | for enhanced model capabilities and intelligent query processing. 6 | """ 7 | 8 | from .api_client import call_provider as call_provider 9 | from .cli import app as app 10 | from .cli import main as main 11 | from .config import PROVIDER_CONFIG as PROVIDER_CONFIG 12 | from .config import get_api_keys as get_api_keys 13 | from .search_tool import ask_perplexity as ask_perplexity 14 | from .search_tool import mcp as mcp 15 | from .types import APIKeyError as APIKeyError 16 | from .types import APIRequestError as APIRequestError 17 | from .types import ApiResponse as ApiResponse 18 | from .types import ChatCompletionChoice as ChatCompletionChoice 19 | from .types import ChatCompletionMessage as ChatCompletionMessage 20 | from .types import ChatCompletionResponse as ChatCompletionResponse 21 | from .types import ChatCompletionUsage as ChatCompletionUsage 22 | from .types import ModelConfig as ModelConfig 23 | from .types import ProviderType as ProviderType 24 | from .types import QueryType as QueryType 25 | -------------------------------------------------------------------------------- /src/perplexity_advanced_mcp/api_client.py: -------------------------------------------------------------------------------- 1 | """ 2 | API Client Module 3 | 4 | Manages communication with OpenRouter and Perplexity APIs, handling authentication, 5 | request processing, and error management. 6 | """ 7 | 8 | import logging 9 | from typing import Any, NoReturn, cast 10 | 11 | import httpx 12 | from tenacity import ( 13 | AsyncRetrying, 14 | RetryError, 15 | before_sleep_log, 16 | retry_if_exception, 17 | stop_after_attempt, 18 | wait_exponential, 19 | ) 20 | 21 | from .config import PROVIDER_CONFIG 22 | from .types import ( 23 | APIKeyError, 24 | APIRequestError, 25 | ApiResponse, 26 | ChatCompletionMessage, 27 | ChatCompletionResponse, 28 | ProviderType, 29 | ) 30 | 31 | logger = logging.getLogger(__name__) 32 | 33 | 34 | def is_retryable_error(exc: BaseException) -> bool: 35 | """ 36 | Determines if an exception qualifies for retry based on error type and conditions. 37 | 38 | The following conditions are considered retryable: 39 | 1. Network timeouts 40 | 2. Connection errors 41 | 3. Rate limit responses (HTTP 429) 42 | 4. Server errors (HTTP 5xx) 43 | 44 | Args: 45 | exc: The exception to evaluate 46 | 47 | Returns: 48 | bool: True if the error is retryable, False otherwise 49 | """ 50 | if isinstance(exc, APIRequestError): 51 | cause = exc.__cause__ 52 | if cause: 53 | # Retry on timeout or connection errors 54 | if isinstance(cause, httpx.TimeoutException | httpx.TransportError): 55 | return True 56 | # Check status code for HTTP errors 57 | if isinstance(cause, httpx.HTTPStatusError): 58 | return cause.response.status_code == 429 or (500 <= cause.response.status_code < 600) 59 | return False 60 | 61 | 62 | def raise_api_error(message: str, cause: Exception | None = None) -> NoReturn: 63 | """ 64 | Raises an APIRequestError with the specified message and optional cause. 65 | 66 | Args: 67 | message: Error description 68 | cause: The underlying exception that caused this error 69 | 70 | Raises: 71 | APIRequestError: Always raises this exception 72 | """ 73 | if cause: 74 | raise APIRequestError(message) from cause 75 | raise APIRequestError(message) 76 | 77 | 78 | async def call_api( 79 | endpoint: str, payload: dict[str, Any], token: str, provider: ProviderType 80 | ) -> ChatCompletionResponse: 81 | """ 82 | Executes an API request with retry logic and returns the parsed response. 83 | 84 | Args: 85 | endpoint: Target API endpoint URL 86 | payload: Request payload data 87 | token: API authentication token 88 | provider: API provider type 89 | 90 | Returns: 91 | ChatCompletionResponse: Parsed API response data 92 | 93 | Raises: 94 | APIRequestError: When the API request fails after all retry attempts 95 | """ 96 | headers: dict[str, str] = { 97 | "Authorization": f"Bearer {token}", 98 | "Content-Type": "application/json", 99 | } 100 | 101 | if provider == "openrouter": 102 | headers.update( 103 | { 104 | "HTTP-Referer": "https://github.com/code-yeongyu/perplexity-advanced-mcp", 105 | "X-Title": "Perplexity Advanced MCP", 106 | } 107 | ) 108 | 109 | # Disable HTTPX timeouts to let MCP client handle timeout management 110 | async with httpx.AsyncClient(timeout=None) as client: 111 | try: 112 | # Implement retry logic using Tenacity 113 | async for attempt in AsyncRetrying( 114 | retry=retry_if_exception(is_retryable_error), 115 | stop=stop_after_attempt(5), # Maximum 5 attempts 116 | wait=wait_exponential(multiplier=1, min=1, max=10), # Exponential backoff between 1-10 seconds 117 | before_sleep=before_sleep_log(logger, logging.WARNING), 118 | reraise=True, 119 | ): 120 | with attempt: 121 | response: httpx.Response = await client.post(endpoint, json=payload, headers=headers) 122 | response.raise_for_status() 123 | return cast(ChatCompletionResponse, response.json()) 124 | 125 | except RetryError as e: 126 | # All retry attempts have failed 127 | raise_api_error(f"All retry attempts failed: {str(e.__cause__)}", e) 128 | except httpx.HTTPError as e: 129 | # Non-retryable HTTP error 130 | raise_api_error(f"API request failed: {str(e)}", e) 131 | 132 | # This code is unreachable but needed to satisfy the type checker 133 | raise_api_error("Unexpected error occurred") 134 | 135 | 136 | async def call_provider( 137 | provider: ProviderType, 138 | model: str, 139 | messages: list[dict[str, str]], 140 | include_reasoning: bool = False, 141 | ) -> ApiResponse: 142 | """ 143 | Calls the specified provider's API and returns a parsed response. 144 | 145 | Args: 146 | provider: Target API provider 147 | model: Model identifier to use 148 | messages: List of conversation messages 149 | include_reasoning: Whether to include reasoning in the response (OpenRouter only) 150 | 151 | Returns: 152 | ApiResponse: Parsed API response containing content and optional reasoning 153 | 154 | Raises: 155 | APIKeyError: When the required API key is not configured 156 | APIRequestError: When the API request fails 157 | """ 158 | # Validate token 159 | token: str | None = PROVIDER_CONFIG[provider]["key"] 160 | if not token: 161 | raise APIKeyError(f"{provider} API key not set") 162 | 163 | # Configure provider-specific endpoints 164 | endpoints: dict[ProviderType, str] = { 165 | "openrouter": "https://openrouter.ai/api/v1/chat/completions", 166 | "perplexity": "https://api.perplexity.ai/chat/completions", 167 | } 168 | endpoint: str = endpoints[provider] 169 | 170 | # Prepare request payload 171 | payload: dict[str, Any] = { 172 | "model": model, 173 | "messages": messages, 174 | } 175 | if provider == "openrouter": 176 | payload["include_reasoning"] = include_reasoning 177 | 178 | # Make API call and process response 179 | data: ChatCompletionResponse = await call_api(endpoint, payload, token, provider) 180 | message_data = cast(ChatCompletionMessage, (data.get("choices", [{}])[0]).get("message", {})) 181 | 182 | result: ApiResponse = { 183 | "content": message_data.get("content", ""), 184 | "reasoning": None, 185 | } 186 | reasoning = message_data.get("reasoning") 187 | if reasoning is not None: 188 | result["reasoning"] = reasoning 189 | 190 | return result 191 | -------------------------------------------------------------------------------- /src/perplexity_advanced_mcp/cli.py: -------------------------------------------------------------------------------- 1 | """ 2 | CLI Interface Module 3 | 4 | Defines the command-line interface for the perplexity-advanced-mcp package, 5 | providing API key configuration and server management functionality. 6 | """ 7 | 8 | import logging 9 | 10 | import typer 11 | 12 | from perplexity_advanced_mcp.types import ProviderType 13 | 14 | from .config import PROVIDER_CONFIG, get_api_keys 15 | from .search_tool import mcp 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | app = typer.Typer() 20 | 21 | # Global flag for graceful shutdown 22 | shutdown_requested = False 23 | 24 | 25 | @app.command() 26 | def main( 27 | ctx: typer.Context, 28 | openrouter_key: str | None = typer.Option( 29 | None, 30 | "--openrouter-api-key", 31 | "-o", 32 | help="OpenRouter API key", 33 | envvar="OPENROUTER_API_KEY", 34 | ), 35 | perplexity_key: str | None = typer.Option( 36 | None, 37 | "--perplexity-api-key", 38 | "-p", 39 | help="Perplexity API key", 40 | envvar="PERPLEXITY_API_KEY", 41 | ), 42 | ) -> None: 43 | logger.info("Starting MCP server...") 44 | openrouter_key_val, perplexity_key_val = get_api_keys(openrouter_key, perplexity_key) 45 | PROVIDER_CONFIG["openrouter"]["key"] = openrouter_key_val 46 | PROVIDER_CONFIG["perplexity"]["key"] = perplexity_key_val 47 | 48 | provider: ProviderType 49 | if openrouter_key_val: 50 | provider = "openrouter" 51 | elif perplexity_key_val: 52 | provider = "perplexity" 53 | else: 54 | raise typer.Abort() 55 | 56 | logger.info("Using %s as the provider", provider) 57 | 58 | mcp.run() 59 | 60 | 61 | if __name__ == "__main__": 62 | app() 63 | -------------------------------------------------------------------------------- /src/perplexity_advanced_mcp/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration Module 3 | 4 | Manages global settings and environment configuration for the perplexity-advanced-mcp package, 5 | including provider-specific settings and API key management. 6 | """ 7 | 8 | import logging 9 | from typing import cast 10 | 11 | import typer 12 | 13 | from .logging import setup_logging 14 | from .types import ModelConfig, ProviderType, QueryType 15 | 16 | # Initialize logging configuration 17 | setup_logging(level=logging.INFO) 18 | 19 | # Provider configurations (API keys are assigned at runtime) 20 | PROVIDER_CONFIG: dict[ProviderType, ModelConfig] = { 21 | "openrouter": { 22 | "models": { 23 | QueryType.SIMPLE: "perplexity/sonar", 24 | QueryType.COMPLEX: "perplexity/sonar-reasoning", 25 | }, 26 | "key": None, 27 | }, 28 | "perplexity": { 29 | "models": { 30 | QueryType.SIMPLE: "sonar-pro", 31 | QueryType.COMPLEX: "sonar-reasoning-pro", 32 | }, 33 | "key": None, 34 | }, 35 | } 36 | 37 | 38 | def get_api_keys( 39 | openrouter_key: str | None = typer.Option( 40 | None, 41 | "--openrouter-api-key", 42 | "-o", 43 | help="OpenRouter API key", 44 | envvar="OPENROUTER_API_KEY", 45 | ), 46 | perplexity_key: str | None = typer.Option( 47 | None, 48 | "--perplexity-api-key", 49 | "-p", 50 | help="Perplexity API key", 51 | envvar="PERPLEXITY_API_KEY", 52 | ), 53 | ) -> tuple[str | None, str | None]: 54 | """ 55 | Retrieves API keys from command line arguments or environment variables. 56 | Ensures exactly one API key is provided. 57 | 58 | Args: 59 | openrouter_key: OpenRouter API key from CLI or environment 60 | perplexity_key: Perplexity API key from CLI or environment 61 | 62 | Returns: 63 | tuple: A tuple containing (OpenRouter API key, Perplexity API key) 64 | 65 | Raises: 66 | typer.BadParameter: If both keys are provided or if no key is provided 67 | """ 68 | has_openrouter: bool = bool(openrouter_key and openrouter_key.strip()) 69 | has_perplexity: bool = bool(perplexity_key and perplexity_key.strip()) 70 | 71 | if has_openrouter and has_perplexity: 72 | raise typer.BadParameter( 73 | "Cannot specify both OpenRouter and Perplexity API keys. Please provide only one of them." 74 | ) 75 | 76 | if not has_openrouter and not has_perplexity: 77 | raise typer.BadParameter( 78 | "No API keys found. Please provide either OPENROUTER_API_KEY or PERPLEXITY_API_KEY " 79 | "through command line arguments (--openrouter-api-key/--perplexity-api-key) " 80 | "or environment variables (OPENROUTER_API_KEY/PERPLEXITY_API_KEY)" 81 | ) 82 | 83 | return ( 84 | cast(str, openrouter_key) if has_openrouter else None, 85 | cast(str, perplexity_key) if has_perplexity else None, 86 | ) 87 | -------------------------------------------------------------------------------- /src/perplexity_advanced_mcp/logging.py: -------------------------------------------------------------------------------- 1 | """ 2 | Logging Configuration Module 3 | 4 | Provides logging setup and configuration utilities. 5 | """ 6 | 7 | import logging 8 | import sys 9 | 10 | 11 | def setup_logging( 12 | level: int = logging.INFO, 13 | *, 14 | show_time: bool = True, 15 | show_path: bool = True, 16 | ) -> None: 17 | """ 18 | Initializes the default logging configuration with customizable options. 19 | 20 | Args: 21 | level: Logging level (defaults to logging.INFO) 22 | show_time: Whether to include timestamps in log messages 23 | show_path: Whether to include file paths in log messages 24 | """ 25 | formatter = logging.Formatter( 26 | fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s" 27 | if show_path 28 | else "%(asctime)s - %(levelname)s - %(message)s", 29 | datefmt="%Y-%m-%d %H:%M:%S" if show_time else None, 30 | ) 31 | 32 | handler = logging.StreamHandler(stream=sys.stderr) 33 | handler.setFormatter(formatter) 34 | 35 | logging.basicConfig( 36 | level=level, 37 | handlers=[handler], 38 | force=True, 39 | ) 40 | 41 | # Adjust logging levels for external libraries 42 | logging.getLogger("httpx").setLevel(logging.WARNING) 43 | logging.getLogger("httpcore").setLevel(logging.WARNING) 44 | 45 | # Set all MCP-related loggers to WARNING level 46 | logging.getLogger("mcp").setLevel(logging.WARNING) 47 | logging.getLogger("mcp.server").setLevel(logging.WARNING) 48 | logging.getLogger("mcp.server.lowlevel").setLevel(logging.WARNING) 49 | logging.getLogger("mcp.server.lowlevel.server").setLevel(logging.WARNING) 50 | logging.getLogger("mcp.server.fastmcp").setLevel(logging.WARNING) 51 | -------------------------------------------------------------------------------- /src/perplexity_advanced_mcp/search_tool.py: -------------------------------------------------------------------------------- 1 | """ 2 | MCP Search Tool Module 3 | 4 | Defines search tools that are registered with the MCP server for advanced query processing 5 | and file attachment handling. 6 | """ 7 | 8 | import textwrap 9 | from pathlib import Path 10 | from typing import Literal 11 | 12 | from mcp.server.fastmcp import FastMCP 13 | from pydantic import BaseModel, Field 14 | 15 | from .api_client import call_provider 16 | from .config import PROVIDER_CONFIG 17 | from .types import APIKeyError, ModelConfig, ProviderType, QueryType 18 | 19 | 20 | class Message(BaseModel): 21 | """Message model for chat interactions.""" 22 | 23 | role: str = Field(description="The role of the message sender (user/assistant)") 24 | content: str = Field(description="The content of the message") 25 | 26 | 27 | # Create MCP server instance 28 | mcp: FastMCP = FastMCP( 29 | "perplexity-advanced", 30 | log_level="WARNING", 31 | dependencies=["httpx"], 32 | ) 33 | 34 | 35 | def process_attachments(attachment_paths: list[str]) -> str: 36 | """ 37 | Processes file attachments and formats them into an XML string. 38 | 39 | Reads the contents of each file and wraps them in XML tags with the following structure: 40 | 41 | 42 | [file1 contents] 43 | 44 | 45 | [file2 contents] 46 | 47 | 48 | 49 | Args: 50 | attachment_paths: List of absolute file paths to process 51 | 52 | Returns: 53 | str: XML-formatted string containing file contents 54 | 55 | Raises: 56 | ValueError: If a file is not found, is invalid, or cannot be read 57 | """ 58 | if not attachment_paths: 59 | return "" 60 | 61 | result = [""] 62 | 63 | # Process each file 64 | for file_path in attachment_paths: 65 | try: 66 | abs_path = Path(file_path).resolve(strict=True) 67 | if not abs_path.is_file(): 68 | raise ValueError(f"'{abs_path}' is not a valid file") 69 | 70 | # Read file content 71 | with abs_path.open(encoding="utf-8") as f: 72 | file_content = f.read() 73 | 74 | # Add file content with proper indentation 75 | result.append(f'\t') 76 | # Indent each line of the content 77 | content_lines = file_content.splitlines() 78 | result.extend(f"\t\t{line}" for line in content_lines) 79 | result.append("\t") 80 | 81 | except FileNotFoundError: 82 | raise ValueError(f"File not found: {file_path}") 83 | except Exception as e: 84 | raise ValueError(f"Error processing file '{file_path}': {e}") from e 85 | 86 | result.append("") 87 | formatted_xml = "\n".join(result) 88 | return formatted_xml 89 | 90 | 91 | @mcp.tool( 92 | name="ask_perplexity", 93 | description=( 94 | """Perplexity is fundamentally an LLM that can search the internet, gather information, and answer users' queries. 95 | 96 | For example, let's suppose we want to find out the latest version of Python. 97 | 1. You would search on Google. 98 | 2. Then read the top two or three results directly to verify. 99 | 100 | Perplexity does that work for you. 101 | 102 | To answer a user's query, Perplexity searches, opens the top search results, finds information on those websites, and then provides the answer. 103 | 104 | Perplexity can be used with two types of queries: simple and complex. Choosing the right query type to fulfill the user's request is most important. 105 | 106 | SIMPLE Query: 107 | - Cheap and fast (on average, 10x cheaper and 3x faster than complex queries). 108 | - Suitable for straightforward questions such as "What is the latest version of Python?" 109 | - Pricing: $1/M input tokens, $1/M output tokens. 110 | 111 | COMPLEX Query: 112 | - Slower and more expensive (on average, 10x more expensive and 3x slower). 113 | - Suitable for tasks requiring multiple steps of reasoning or deep analysis, such as "Analyze the attached code to examine the current status of a specific library and create a migration plan." 114 | - Pricing: $1/M input tokens, $5/M output tokens. 115 | 116 | Instructions: 117 | - When reviewing the user's request, if you find anything unexpected, uncertain, or questionable, do not hesitate to use the "ask_perplexity" tool to consult Perplexity. 118 | - Since Perplexity is also an LLM, prompt engineering techniques are paramount. 119 | - Remember the basics of prompt engineering, such as providing clear instructions, sufficient context, and examples. 120 | - Include as much context and relevant files as possible to smoothly fulfill the user's request. 121 | - IMPORTANT: When adding files as attachments, you MUST use absolute paths (e.g., '/absolute/path/to/file.py'). Relative paths will not work. 122 | 123 | Note: All queries must be in English for optimal results. 124 | """ 125 | ), 126 | ) 127 | async def ask_perplexity( 128 | query: str = Field(description="The query to search for"), 129 | query_type: Literal["simple", "complex"] = Field(description="Type of query to determine model selection"), 130 | attachment_paths: list[str] = Field( 131 | description="An optional list of absolute file paths to attach as context for the search query", 132 | ), 133 | ) -> str: 134 | """ 135 | Performs an advanced search using the appropriate API provider and model. 136 | 137 | This function processes any attached files by reading their contents and formatting them 138 | into XML before appending them to the original query. The combined query is then sent 139 | to either OpenRouter or Perplexity API based on the available configuration. 140 | 141 | Args: 142 | query: The search query text 143 | query_type: Query complexity type ('simple' or 'complex') 144 | attachment_paths: Optional list of files to include as context 145 | 146 | Returns: 147 | str: XML-formatted result containing reasoning (if available) and answer 148 | 149 | Raises: 150 | ValueError: If the query is empty or attachments cannot be processed 151 | APIKeyError: If no API provider is configured 152 | """ 153 | if not query: 154 | raise ValueError("Query must not be empty") 155 | 156 | # Process any file attachments and get the XML string 157 | attachments_xml = "" 158 | if attachment_paths: 159 | attachments_xml = process_attachments(attachment_paths) 160 | 161 | # Combine the original query with the attachment contents 162 | if query_type == "complex": 163 | query = textwrap.dedent( 164 | f""" 165 | 166 | Think or reason about the user's words in as much detail as possible. Summarize everything thoroughly. 167 | List all the elements that need to be considered regarding the user's question or prompt. 168 | 169 | 170 | Form your opinions about these elements from an objective standpoint, avoiding an overly pessimistic or overly optimistic view. Opinions should be specific and realistic. 171 | Then logically verify these opinions once more. If they hold up, proceed to the next thought; if not, re-examine them. 172 | 173 | 174 | By carrying out this reflection process, you can accumulate opinions that have been logically reviewed and verified. 175 | Finally, combine these logically validated pieces of reasoning to form your answer. By doing this way, provide responses that are verifiable and logically sound. 176 | 177 | 178 | 179 | {query} 180 | 181 | """[1:] 182 | ) 183 | query_with_attachments = query + attachments_xml 184 | 185 | # Retrieve available providers 186 | available_providers: list[ProviderType] = [p for p, cfg in PROVIDER_CONFIG.items() if cfg["key"]] 187 | if not available_providers: 188 | raise APIKeyError("No API key available") 189 | provider: ProviderType = available_providers[0] 190 | config: ModelConfig = PROVIDER_CONFIG[provider] 191 | 192 | # Map query_type from string to QueryType enum 193 | match query_type: 194 | case "simple": 195 | query_type_enum = QueryType.SIMPLE 196 | case "complex": 197 | query_type_enum = QueryType.COMPLEX 198 | case _: 199 | raise ValueError(f"Invalid query type: {query_type}") 200 | 201 | model: str = config["models"][query_type_enum] 202 | include_reasoning = provider == "openrouter" and query_type_enum == QueryType.COMPLEX 203 | 204 | # Call the provider with the combined query 205 | response = await call_provider( 206 | provider, model, [{"role": "user", "content": query_with_attachments}], include_reasoning 207 | ) 208 | # Format the result as raw text with XML-like tags 209 | result = "" 210 | 211 | # Add reasoning if available 212 | reasoning = response.get("reasoning") 213 | answer = response.get("content", "") 214 | 215 | reasoning_text = f"\n{reasoning}\n\n\n" if reasoning else "" 216 | answer_text = f"\n{answer}\n" 217 | 218 | result += reasoning_text + answer_text 219 | return result 220 | -------------------------------------------------------------------------------- /src/perplexity_advanced_mcp/types.py: -------------------------------------------------------------------------------- 1 | """ 2 | Type Definitions Module 3 | 4 | Contains all type definitions used throughout the perplexity-advanced-mcp package, 5 | including custom exceptions, enums, and type aliases. 6 | """ 7 | 8 | from enum import StrEnum 9 | from typing import Literal, TypedDict 10 | 11 | 12 | # Custom exception definitions 13 | class APIKeyError(Exception): 14 | """Raised when an API key is missing or invalid.""" 15 | 16 | pass 17 | 18 | 19 | class APIRequestError(Exception): 20 | """Raised when an API request fails.""" 21 | 22 | pass 23 | 24 | 25 | class QueryType(StrEnum): 26 | """Defines query types for model selection.""" 27 | 28 | SIMPLE = "simple" 29 | COMPLEX = "complex" 30 | 31 | 32 | # Provider and model type definitions 33 | ProviderType = Literal["openrouter", "perplexity"] 34 | ModelType = Literal["simple", "complex"] 35 | 36 | 37 | class ModelConfig(TypedDict): 38 | """Provider-specific model configuration type.""" 39 | 40 | models: dict[QueryType, str] 41 | key: str | None 42 | 43 | 44 | class ApiResponse(TypedDict): 45 | """Internal API response type.""" 46 | 47 | content: str 48 | reasoning: str | None 49 | 50 | 51 | class ChatCompletionMessage(TypedDict, total=False): 52 | """Chat completion API message type.""" 53 | 54 | role: Literal["system", "user", "assistant"] 55 | content: str 56 | reasoning: str 57 | 58 | 59 | class ChatCompletionChoice(TypedDict, total=False): 60 | """Chat completion API choice type.""" 61 | 62 | message: ChatCompletionMessage 63 | finish_reason: str 64 | 65 | 66 | class ChatCompletionUsage(TypedDict, total=False): 67 | """Chat completion API usage statistics type.""" 68 | 69 | prompt_tokens: int 70 | completion_tokens: int 71 | total_tokens: int 72 | 73 | 74 | class ChatCompletionResponse(TypedDict, total=False): 75 | """Chat completion API response type.""" 76 | 77 | id: str 78 | choices: list[ChatCompletionChoice] 79 | usage: ChatCompletionUsage 80 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | requires-python = ">=3.12" 3 | 4 | [[package]] 5 | name = "annotated-types" 6 | version = "0.7.0" 7 | source = { registry = "https://pypi.org/simple" } 8 | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } 9 | wheels = [ 10 | { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, 11 | ] 12 | 13 | [[package]] 14 | name = "anyio" 15 | version = "4.8.0" 16 | source = { registry = "https://pypi.org/simple" } 17 | dependencies = [ 18 | { name = "idna" }, 19 | { name = "sniffio" }, 20 | { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 21 | ] 22 | sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 } 23 | wheels = [ 24 | { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 }, 25 | ] 26 | 27 | [[package]] 28 | name = "certifi" 29 | version = "2025.1.31" 30 | source = { registry = "https://pypi.org/simple" } 31 | sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } 32 | wheels = [ 33 | { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, 34 | ] 35 | 36 | [[package]] 37 | name = "click" 38 | version = "8.1.8" 39 | source = { registry = "https://pypi.org/simple" } 40 | dependencies = [ 41 | { name = "colorama", marker = "platform_system == 'Windows'" }, 42 | ] 43 | sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } 44 | wheels = [ 45 | { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, 46 | ] 47 | 48 | [[package]] 49 | name = "colorama" 50 | version = "0.4.6" 51 | source = { registry = "https://pypi.org/simple" } 52 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 53 | wheels = [ 54 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, 55 | ] 56 | 57 | [[package]] 58 | name = "h11" 59 | version = "0.14.0" 60 | source = { registry = "https://pypi.org/simple" } 61 | sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } 62 | wheels = [ 63 | { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, 64 | ] 65 | 66 | [[package]] 67 | name = "httpcore" 68 | version = "1.0.7" 69 | source = { registry = "https://pypi.org/simple" } 70 | dependencies = [ 71 | { name = "certifi" }, 72 | { name = "h11" }, 73 | ] 74 | sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } 75 | wheels = [ 76 | { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, 77 | ] 78 | 79 | [[package]] 80 | name = "httpx" 81 | version = "0.28.1" 82 | source = { registry = "https://pypi.org/simple" } 83 | dependencies = [ 84 | { name = "anyio" }, 85 | { name = "certifi" }, 86 | { name = "httpcore" }, 87 | { name = "idna" }, 88 | ] 89 | sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } 90 | wheels = [ 91 | { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, 92 | ] 93 | 94 | [[package]] 95 | name = "httpx-sse" 96 | version = "0.4.0" 97 | source = { registry = "https://pypi.org/simple" } 98 | sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } 99 | wheels = [ 100 | { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, 101 | ] 102 | 103 | [[package]] 104 | name = "idna" 105 | version = "3.10" 106 | source = { registry = "https://pypi.org/simple" } 107 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } 108 | wheels = [ 109 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, 110 | ] 111 | 112 | [[package]] 113 | name = "markdown-it-py" 114 | version = "3.0.0" 115 | source = { registry = "https://pypi.org/simple" } 116 | dependencies = [ 117 | { name = "mdurl" }, 118 | ] 119 | sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } 120 | wheels = [ 121 | { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, 122 | ] 123 | 124 | [[package]] 125 | name = "mcp" 126 | version = "1.3.0" 127 | source = { registry = "https://pypi.org/simple" } 128 | dependencies = [ 129 | { name = "anyio" }, 130 | { name = "httpx" }, 131 | { name = "httpx-sse" }, 132 | { name = "pydantic" }, 133 | { name = "pydantic-settings" }, 134 | { name = "sse-starlette" }, 135 | { name = "starlette" }, 136 | { name = "uvicorn" }, 137 | ] 138 | sdist = { url = "https://files.pythonhosted.org/packages/6b/b6/81e5f2490290351fc97bf46c24ff935128cb7d34d68e3987b522f26f7ada/mcp-1.3.0.tar.gz", hash = "sha256:f409ae4482ce9d53e7ac03f3f7808bcab735bdfc0fba937453782efb43882d45", size = 150235 } 139 | wheels = [ 140 | { url = "https://files.pythonhosted.org/packages/d0/d2/a9e87b506b2094f5aa9becc1af5178842701b27217fa43877353da2577e3/mcp-1.3.0-py3-none-any.whl", hash = "sha256:2829d67ce339a249f803f22eba5e90385eafcac45c94b00cab6cef7e8f217211", size = 70672 }, 141 | ] 142 | 143 | [[package]] 144 | name = "mdurl" 145 | version = "0.1.2" 146 | source = { registry = "https://pypi.org/simple" } 147 | sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } 148 | wheels = [ 149 | { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, 150 | ] 151 | 152 | [[package]] 153 | name = "mypy" 154 | version = "1.15.0" 155 | source = { registry = "https://pypi.org/simple" } 156 | dependencies = [ 157 | { name = "mypy-extensions" }, 158 | { name = "typing-extensions" }, 159 | ] 160 | sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 } 161 | wheels = [ 162 | { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981 }, 163 | { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175 }, 164 | { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675 }, 165 | { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020 }, 166 | { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582 }, 167 | { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614 }, 168 | { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592 }, 169 | { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611 }, 170 | { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443 }, 171 | { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541 }, 172 | { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348 }, 173 | { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648 }, 174 | { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 }, 175 | ] 176 | 177 | [[package]] 178 | name = "mypy-extensions" 179 | version = "1.0.0" 180 | source = { registry = "https://pypi.org/simple" } 181 | sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } 182 | wheels = [ 183 | { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, 184 | ] 185 | 186 | [[package]] 187 | name = "perplexity-advanced-mcp" 188 | version = "0.1.2" 189 | source = { editable = "." } 190 | dependencies = [ 191 | { name = "httpx" }, 192 | { name = "mcp" }, 193 | { name = "pydantic" }, 194 | { name = "setuptools" }, 195 | { name = "tenacity" }, 196 | { name = "typer" }, 197 | ] 198 | 199 | [package.optional-dependencies] 200 | dev = [ 201 | { name = "mypy" }, 202 | { name = "ruff" }, 203 | ] 204 | 205 | [package.metadata] 206 | requires-dist = [ 207 | { name = "httpx", specifier = ">=0.28.1" }, 208 | { name = "mcp", specifier = ">=1.3.0" }, 209 | { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.15.0" }, 210 | { name = "pydantic", specifier = ">=2.10.6" }, 211 | { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.9.6" }, 212 | { name = "setuptools", specifier = ">=75.8.0" }, 213 | { name = "tenacity", specifier = ">=8.2.3" }, 214 | { name = "typer", specifier = ">=0.15.1" }, 215 | ] 216 | 217 | [[package]] 218 | name = "pydantic" 219 | version = "2.10.6" 220 | source = { registry = "https://pypi.org/simple" } 221 | dependencies = [ 222 | { name = "annotated-types" }, 223 | { name = "pydantic-core" }, 224 | { name = "typing-extensions" }, 225 | ] 226 | sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } 227 | wheels = [ 228 | { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, 229 | ] 230 | 231 | [[package]] 232 | name = "pydantic-core" 233 | version = "2.27.2" 234 | source = { registry = "https://pypi.org/simple" } 235 | dependencies = [ 236 | { name = "typing-extensions" }, 237 | ] 238 | sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } 239 | wheels = [ 240 | { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, 241 | { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, 242 | { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, 243 | { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, 244 | { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, 245 | { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, 246 | { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, 247 | { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, 248 | { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, 249 | { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, 250 | { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, 251 | { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, 252 | { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, 253 | { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, 254 | { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, 255 | { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, 256 | { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, 257 | { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, 258 | { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, 259 | { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, 260 | { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, 261 | { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, 262 | { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, 263 | { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, 264 | { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, 265 | { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, 266 | { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, 267 | { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, 268 | ] 269 | 270 | [[package]] 271 | name = "pydantic-settings" 272 | version = "2.8.1" 273 | source = { registry = "https://pypi.org/simple" } 274 | dependencies = [ 275 | { name = "pydantic" }, 276 | { name = "python-dotenv" }, 277 | ] 278 | sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 } 279 | wheels = [ 280 | { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 }, 281 | ] 282 | 283 | [[package]] 284 | name = "pygments" 285 | version = "2.19.1" 286 | source = { registry = "https://pypi.org/simple" } 287 | sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } 288 | wheels = [ 289 | { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, 290 | ] 291 | 292 | [[package]] 293 | name = "python-dotenv" 294 | version = "1.0.1" 295 | source = { registry = "https://pypi.org/simple" } 296 | sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } 297 | wheels = [ 298 | { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, 299 | ] 300 | 301 | [[package]] 302 | name = "rich" 303 | version = "13.9.4" 304 | source = { registry = "https://pypi.org/simple" } 305 | dependencies = [ 306 | { name = "markdown-it-py" }, 307 | { name = "pygments" }, 308 | ] 309 | sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } 310 | wheels = [ 311 | { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, 312 | ] 313 | 314 | [[package]] 315 | name = "ruff" 316 | version = "0.9.10" 317 | source = { registry = "https://pypi.org/simple" } 318 | sdist = { url = "https://files.pythonhosted.org/packages/20/8e/fafaa6f15c332e73425d9c44ada85360501045d5ab0b81400076aff27cf6/ruff-0.9.10.tar.gz", hash = "sha256:9bacb735d7bada9cfb0f2c227d3658fc443d90a727b47f206fb33f52f3c0eac7", size = 3759776 } 319 | wheels = [ 320 | { url = "https://files.pythonhosted.org/packages/73/b2/af7c2cc9e438cbc19fafeec4f20bfcd72165460fe75b2b6e9a0958c8c62b/ruff-0.9.10-py3-none-linux_armv6l.whl", hash = "sha256:eb4d25532cfd9fe461acc83498361ec2e2252795b4f40b17e80692814329e42d", size = 10049494 }, 321 | { url = "https://files.pythonhosted.org/packages/6d/12/03f6dfa1b95ddd47e6969f0225d60d9d7437c91938a310835feb27927ca0/ruff-0.9.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:188a6638dab1aa9bb6228a7302387b2c9954e455fb25d6b4470cb0641d16759d", size = 10853584 }, 322 | { url = "https://files.pythonhosted.org/packages/02/49/1c79e0906b6ff551fb0894168763f705bf980864739572b2815ecd3c9df0/ruff-0.9.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5284dcac6b9dbc2fcb71fdfc26a217b2ca4ede6ccd57476f52a587451ebe450d", size = 10155692 }, 323 | { url = "https://files.pythonhosted.org/packages/5b/01/85e8082e41585e0e1ceb11e41c054e9e36fed45f4b210991052d8a75089f/ruff-0.9.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47678f39fa2a3da62724851107f438c8229a3470f533894b5568a39b40029c0c", size = 10369760 }, 324 | { url = "https://files.pythonhosted.org/packages/a1/90/0bc60bd4e5db051f12445046d0c85cc2c617095c0904f1aa81067dc64aea/ruff-0.9.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99713a6e2766b7a17147b309e8c915b32b07a25c9efd12ada79f217c9c778b3e", size = 9912196 }, 325 | { url = "https://files.pythonhosted.org/packages/66/ea/0b7e8c42b1ec608033c4d5a02939c82097ddcb0b3e393e4238584b7054ab/ruff-0.9.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524ee184d92f7c7304aa568e2db20f50c32d1d0caa235d8ddf10497566ea1a12", size = 11434985 }, 326 | { url = "https://files.pythonhosted.org/packages/d5/86/3171d1eff893db4f91755175a6e1163c5887be1f1e2f4f6c0c59527c2bfd/ruff-0.9.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:df92aeac30af821f9acf819fc01b4afc3dfb829d2782884f8739fb52a8119a16", size = 12155842 }, 327 | { url = "https://files.pythonhosted.org/packages/89/9e/700ca289f172a38eb0bca752056d0a42637fa17b81649b9331786cb791d7/ruff-0.9.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de42e4edc296f520bb84954eb992a07a0ec5a02fecb834498415908469854a52", size = 11613804 }, 328 | { url = "https://files.pythonhosted.org/packages/f2/92/648020b3b5db180f41a931a68b1c8575cca3e63cec86fd26807422a0dbad/ruff-0.9.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d257f95b65806104b6b1ffca0ea53f4ef98454036df65b1eda3693534813ecd1", size = 13823776 }, 329 | { url = "https://files.pythonhosted.org/packages/5e/a6/cc472161cd04d30a09d5c90698696b70c169eeba2c41030344194242db45/ruff-0.9.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60dec7201c0b10d6d11be00e8f2dbb6f40ef1828ee75ed739923799513db24c", size = 11302673 }, 330 | { url = "https://files.pythonhosted.org/packages/6c/db/d31c361c4025b1b9102b4d032c70a69adb9ee6fde093f6c3bf29f831c85c/ruff-0.9.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d838b60007da7a39c046fcdd317293d10b845001f38bcb55ba766c3875b01e43", size = 10235358 }, 331 | { url = "https://files.pythonhosted.org/packages/d1/86/d6374e24a14d4d93ebe120f45edd82ad7dcf3ef999ffc92b197d81cdc2a5/ruff-0.9.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ccaf903108b899beb8e09a63ffae5869057ab649c1e9231c05ae354ebc62066c", size = 9886177 }, 332 | { url = "https://files.pythonhosted.org/packages/00/62/a61691f6eaaac1e945a1f3f59f1eea9a218513139d5b6c2b8f88b43b5b8f/ruff-0.9.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f9567d135265d46e59d62dc60c0bfad10e9a6822e231f5b24032dba5a55be6b5", size = 10864747 }, 333 | { url = "https://files.pythonhosted.org/packages/ee/94/2c7065e1d92a8a8a46d46d9c3cf07b0aa7e0a1e0153d74baa5e6620b4102/ruff-0.9.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5f202f0d93738c28a89f8ed9eaba01b7be339e5d8d642c994347eaa81c6d75b8", size = 11360441 }, 334 | { url = "https://files.pythonhosted.org/packages/a7/8f/1f545ea6f9fcd7bf4368551fb91d2064d8f0577b3079bb3f0ae5779fb773/ruff-0.9.10-py3-none-win32.whl", hash = "sha256:bfb834e87c916521ce46b1788fbb8484966e5113c02df216680102e9eb960029", size = 10247401 }, 335 | { url = "https://files.pythonhosted.org/packages/4f/18/fb703603ab108e5c165f52f5b86ee2aa9be43bb781703ec87c66a5f5d604/ruff-0.9.10-py3-none-win_amd64.whl", hash = "sha256:f2160eeef3031bf4b17df74e307d4c5fb689a6f3a26a2de3f7ef4044e3c484f1", size = 11366360 }, 336 | { url = "https://files.pythonhosted.org/packages/35/85/338e603dc68e7d9994d5d84f24adbf69bae760ba5efd3e20f5ff2cec18da/ruff-0.9.10-py3-none-win_arm64.whl", hash = "sha256:5fd804c0327a5e5ea26615550e706942f348b197d5475ff34c19733aee4b2e69", size = 10436892 }, 337 | ] 338 | 339 | [[package]] 340 | name = "setuptools" 341 | version = "76.0.0" 342 | source = { registry = "https://pypi.org/simple" } 343 | sdist = { url = "https://files.pythonhosted.org/packages/32/d2/7b171caf085ba0d40d8391f54e1c75a1cda9255f542becf84575cfd8a732/setuptools-76.0.0.tar.gz", hash = "sha256:43b4ee60e10b0d0ee98ad11918e114c70701bc6051662a9a675a0496c1a158f4", size = 1349387 } 344 | wheels = [ 345 | { url = "https://files.pythonhosted.org/packages/37/66/d2d7e6ad554f3a7c7297c3f8ef6e22643ad3d35ef5c63bf488bc89f32f31/setuptools-76.0.0-py3-none-any.whl", hash = "sha256:199466a166ff664970d0ee145839f5582cb9bca7a0a3a2e795b6a9cb2308e9c6", size = 1236106 }, 346 | ] 347 | 348 | [[package]] 349 | name = "shellingham" 350 | version = "1.5.4" 351 | source = { registry = "https://pypi.org/simple" } 352 | sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } 353 | wheels = [ 354 | { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, 355 | ] 356 | 357 | [[package]] 358 | name = "sniffio" 359 | version = "1.3.1" 360 | source = { registry = "https://pypi.org/simple" } 361 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } 362 | wheels = [ 363 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, 364 | ] 365 | 366 | [[package]] 367 | name = "sse-starlette" 368 | version = "2.2.1" 369 | source = { registry = "https://pypi.org/simple" } 370 | dependencies = [ 371 | { name = "anyio" }, 372 | { name = "starlette" }, 373 | ] 374 | sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 } 375 | wheels = [ 376 | { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 }, 377 | ] 378 | 379 | [[package]] 380 | name = "starlette" 381 | version = "0.46.1" 382 | source = { registry = "https://pypi.org/simple" } 383 | dependencies = [ 384 | { name = "anyio" }, 385 | ] 386 | sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 } 387 | wheels = [ 388 | { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 }, 389 | ] 390 | 391 | [[package]] 392 | name = "tenacity" 393 | version = "9.0.0" 394 | source = { registry = "https://pypi.org/simple" } 395 | sdist = { url = "https://files.pythonhosted.org/packages/cd/94/91fccdb4b8110642462e653d5dcb27e7b674742ad68efd146367da7bdb10/tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b", size = 47421 } 396 | wheels = [ 397 | { url = "https://files.pythonhosted.org/packages/b6/cb/b86984bed139586d01532a587464b5805f12e397594f19f931c4c2fbfa61/tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539", size = 28169 }, 398 | ] 399 | 400 | [[package]] 401 | name = "typer" 402 | version = "0.15.2" 403 | source = { registry = "https://pypi.org/simple" } 404 | dependencies = [ 405 | { name = "click" }, 406 | { name = "rich" }, 407 | { name = "shellingham" }, 408 | { name = "typing-extensions" }, 409 | ] 410 | sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 } 411 | wheels = [ 412 | { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 }, 413 | ] 414 | 415 | [[package]] 416 | name = "typing-extensions" 417 | version = "4.12.2" 418 | source = { registry = "https://pypi.org/simple" } 419 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } 420 | wheels = [ 421 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, 422 | ] 423 | 424 | [[package]] 425 | name = "uvicorn" 426 | version = "0.34.0" 427 | source = { registry = "https://pypi.org/simple" } 428 | dependencies = [ 429 | { name = "click" }, 430 | { name = "h11" }, 431 | ] 432 | sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } 433 | wheels = [ 434 | { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, 435 | ] 436 | --------------------------------------------------------------------------------