├── .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 | [](https://github.com/code-yeongyu/perplexity-advanced-mcp)
6 | [](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 | [](https://mseep.ai/app/code-yeongyu-perplexity-advanced-mcp)
2 |
3 |
4 |
5 | # Perplexity Advanced MCP
6 |
7 | [](https://github.com/code-yeongyu/perplexity-advanced-mcp)
8 | [](https://pypi.org/project/perplexity-advanced-mcp)
9 | [](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 |
--------------------------------------------------------------------------------