github-pr-context-mcp 0.2.5__tar.gz → 0.2.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/PKG-INFO +72 -46
  2. {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/README.md +71 -45
  3. {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/analytics/usage_metrics.py +7 -1
  4. github_pr_context_mcp-0.2.7/app/mcp_app.py +55 -0
  5. github_pr_context_mcp-0.2.7/app/routes/http.py +90 -0
  6. github_pr_context_mcp-0.2.7/app/state.py +174 -0
  7. github_pr_context_mcp-0.2.7/app/tools/admin.py +57 -0
  8. github_pr_context_mcp-0.2.7/app/tools/analysis.py +95 -0
  9. github_pr_context_mcp-0.2.7/app/tools/generation.py +125 -0
  10. github_pr_context_mcp-0.2.7/app/tools/indexing.py +241 -0
  11. {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/entrypoints/local/server.py +18 -3
  12. {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/github_pr_context_mcp.egg-info/PKG-INFO +72 -46
  13. {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/github_pr_context_mcp.egg-info/SOURCES.txt +6 -0
  14. {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/pyproject.toml +1 -1
  15. github_pr_context_mcp-0.2.5/app/mcp_app.py +0 -928
  16. {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/LICENSE +0 -0
  17. {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/analytics/__init__.py +0 -0
  18. {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/app/__init__.py +0 -0
  19. {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/auth/__init__.py +0 -0
  20. {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/auth/gmail_identity.py +0 -0
  21. {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/entrypoints/deployed/server.py +0 -0
  22. {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/fetcher/__init__.py +0 -0
  23. {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/fetcher/client.py +0 -0
  24. {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/fetcher/queries.py +0 -0
  25. {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/fetcher/transform.py +0 -0
  26. {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/github_pr_context_mcp.egg-info/dependency_links.txt +0 -0
  27. {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/github_pr_context_mcp.egg-info/entry_points.txt +0 -0
  28. {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/github_pr_context_mcp.egg-info/requires.txt +0 -0
  29. {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/github_pr_context_mcp.egg-info/top_level.txt +0 -0
  30. {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/inference/__init__.py +0 -0
  31. {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/inference/providers.py +0 -0
  32. {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/inference/review.py +0 -0
  33. {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/setup.cfg +0 -0
  34. {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/storage/__init__.py +0 -0
  35. {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/storage/document_builder.py +0 -0
  36. {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/storage/encoder.py +0 -0
  37. {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/storage/vector_store.py +0 -0
  38. {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/tests/test_fixes.py +0 -0
  39. {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/tests/test_sqlite_auth.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-pr-context-mcp
3
- Version: 0.2.5
3
+ Version: 0.2.7
4
4
  Summary: GitHub PR Review Context MCP Server
5
5
  Author: Paarth Gala
6
6
  Requires-Python: >=3.10
@@ -19,6 +19,8 @@ Dynamic: license-file
19
19
 
20
20
  # GitHub PR Review Context MCP
21
21
 
22
+
23
+
22
24
  <div align="center">
23
25
 
24
26
  ![Python](https://img.shields.io/badge/Python-3.10%2B-blue?logo=python&logoColor=white)
@@ -28,14 +30,53 @@ Dynamic: license-file
28
30
  ![Inference](https://img.shields.io/badge/LLM-Multi--Provider-brightgreen)
29
31
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
30
32
  ![Status](https://img.shields.io/badge/Render%20Hosting-Upcoming-gray)
33
+ ![Downloads](https://img.shields.io/badge/downloads-750-blue)
34
+ <!-- [![Users](https://img.shields.io/endpoint?url=https://github-pr-context-mcp.onrender.com/usage/badge)](https://github-pr-context-mcp.onrender.com/usage) -->
31
35
 
32
36
  **Production-grade context layer for AI code review, grounded in your repository's real pull request history.**
33
37
 
34
38
 
35
- > Tracking unique users across **uvx**, **pipx**, and **local** sources. (Render hosting upcoming)
36
-
37
39
  </div>
38
40
 
41
+ ## 🚀 Quick Start
42
+
43
+ ### 🚀 Zero-Setup (uvx / pipx / npx)
44
+ The fastest way to use the server. No cloning required. Just run one of these commands directly in your terminal or use them in your IDE's MCP settings:
45
+
46
+ > [!TIP]
47
+ > **Don't clone this repo to get AI rules!**
48
+ > Once installed, run `generate_repo_rules` inside **YOUR** project to automatically create `.cursorrules` or `CLAUDE.md` tailored to your own team's PR history.
49
+
50
+ **Using uvx (Recommended for speed):**
51
+ ```bash
52
+ uvx github-pr-context-mcp
53
+ ```
54
+
55
+ **Using pipx (Recommended for stability):**
56
+ ```bash
57
+ pipx run github-pr-context-mcp
58
+ # Or install permanently:
59
+ pipx install github-pr-context-mcp
60
+ ```
61
+
62
+ **Using npx (Smithery bridge):**
63
+ ```bash
64
+ npx -y @smithery/cli run github-pr-context-mcp
65
+ ```
66
+
67
+ ---
68
+
69
+ ### ⚠️ Manual Installation (Git Clone / Advanced)
70
+ > [!WARNING]
71
+ > Running from a git clone is **only recommended for developers** contributing to this project. For general use, please use the `pipx` method above.
72
+
73
+ If you have cloned the repository for development:
74
+ 1. Create a virtual environment: `python -m venv .venv`
75
+ 2. Activate it and install: `pip install -e .`
76
+ 3. Run automatic setup: `python scripts/install_clients.py`
77
+
78
+ For full configuration (Cursor, Claude Desktop), see the [**Quick Start Guide**](docs/quickstart.md).
79
+
39
80
  ---
40
81
 
41
82
  ## Overview
@@ -88,6 +129,24 @@ If your team has Hosted this MCP on Render, you do **NOT** need to `git clone` o
88
129
 
89
130
  ---
90
131
 
132
+ > [!IMPORTANT]
133
+ > **🚀 USE THE OFFICIAL PACKAGE:** This project is now on PyPI.
134
+ > To ensure seamless updates and zero configuration friction, do **NOT** `git clone`.
135
+ >
136
+ > **Recommended Install:**
137
+ > ```bash
138
+ > pipx install github-pr-context-mcp
139
+ > ```
140
+ > Or run instantly with: `uvx github-pr-context-mcp`
141
+
142
+ <div align="center">
143
+ <img src="assets/mcp_tool_guide_premium_v2.png" width="800" alt="GitHub PR Context MCP Tools">
144
+ </div>
145
+
146
+ <br/>
147
+
148
+ ---
149
+
91
150
  ## Key Capabilities
92
151
 
93
152
  | Capability | What It Delivers |
@@ -100,20 +159,6 @@ If your team has Hosted this MCP on Render, you do **NOT** need to `git clone` o
100
159
  | Flexible storage modes | Permanent (disk) and temporary (in-memory) indexing options |
101
160
  | Portable inference layer | Switch LLM providers using environment configuration only |
102
161
 
103
- ---
104
-
105
- ## Demo
106
-
107
- ![demo](assets/demo.gif)
108
-
109
- Example workflow:
110
- - Ask the assistant to review a diff using repository history.
111
- - The server retrieves similar past review context.
112
- - The model returns grounded feedback aligned to team expectations.
113
-
114
- ## Usage Analytics
115
-
116
- To help us understand adoption, the MCP server collects privacy-first, anonymous telemetry on deployments. Future hosted deployments will expose HTTP endpoints (`/stats` and `/ping`) that publicly display the **number of unique users**.
117
162
 
118
163
  ---
119
164
 
@@ -138,24 +183,17 @@ The server exposes 12 core tools for IDE agents and developers. For a deep dive
138
183
 
139
184
  ---
140
185
 
141
- ## Documentation
142
-
143
- Detailed guides are split into focused pages:
144
-
145
- - [Quick Start and Usage](docs/quickstart.md)
146
- - [LLM Configuration](docs/llm-configuration.md)
147
- - [Integrations](docs/integrations/index.md)
148
- - [Architecture and Tools](docs/architecture.md)
149
- - [Pipeline Deep Dive](docs/pipeline.md)
150
- - [Configuration Guide (Change Tokens/Settings)](docs/guides/configuration.md)
151
- - [Roadmap](docs/roadmap.md)
152
-
153
186
  ---
154
187
 
155
- ## Quick Links
188
+ ## 📖 Documentation
156
189
 
157
- - Access setup: [GitHub Token Guide](docs/GUIDE_GITHUB_TOKEN.md)
158
- - Client connection: [Integrations](docs/integrations/index.md)
190
+ Detailed guides for deep dives and specific configurations:
191
+
192
+ - 🛠️ [**Quick Start & Usage**](docs/quickstart.md) — Setup and basic commands.
193
+ - ⚙️ [**LLM Configuration**](docs/llm-configuration.md) — Switching between OpenAI, Anthropic, Gemini, and Cerebras.
194
+ - 🧩 [**Tool Strategy & Selection Guide**](docs/tools_strategy.md) — When to use which tool (for humans and agents).
195
+ - 🏗️ [**Architecture & Pipeline**](docs/architecture.md) — How the RAG engine and indexing work.
196
+ - 🔌 [**Integrations**](docs/integrations/index.md) — Connecting to Cursor, Claude Desktop, and more.
159
197
 
160
198
  ---
161
199
 
@@ -163,24 +201,12 @@ Detailed guides are split into focused pages:
163
201
 
164
202
  We want to hear from you—whether you are a solo developer or a team at a large company!
165
203
 
166
- ### 👤 For Individuals
167
204
  - **Feedback**: Please open an issue or start a discussion if you have ideas or encounter bugs.
168
- - **Show your support**: If this tool saves you time, give it a **Star ⭐**! It helps others find the project.
169
-
170
- ### 🏢 For Corporate & Teams
171
- - **Usage**: Is your team using this MCP server? Join our "Adopters" list by opening a PR to add your team's name.
172
- - **Corporate Feedback**: Open an issue with the `corporate-usage` label to tell us how this has improved your PR review workflow.
173
- - **Custom Integration**: Need help deploying this to your private cloud? Reach out via GitHub Discussions.
205
+ - **Star ⭐**: If this tool saves you time, give it a star! It helps others find the project.
206
+ - **Corporate**: Is your team using this? Join our "Adopters" list by opening a PR to add your team's name.
174
207
 
175
208
  ---
176
209
 
177
- ## 📜 Documentation & Guides
178
-
179
- - **Strategy & Best Practices**: [Tool Strategy & Selection Guide](docs/tools_strategy.md)
180
- - **Architecture**: [Architecture and Tools](docs/architecture.md)
181
- - **Pipeline**: [Pipeline Deep Dive](docs/pipeline.md)
182
- - **Usage**: [Quick Start and Usage](docs/quickstart.md)
183
-
184
210
  ## 🛠️ Troubleshooting
185
211
 
186
212
  - **"command not found"**: Use absolute paths in your configuration. Run `github-pr-context-mcp config` to get your exact path.
@@ -1,5 +1,7 @@
1
1
  # GitHub PR Review Context MCP
2
2
 
3
+
4
+
3
5
  <div align="center">
4
6
 
5
7
  ![Python](https://img.shields.io/badge/Python-3.10%2B-blue?logo=python&logoColor=white)
@@ -9,14 +11,53 @@
9
11
  ![Inference](https://img.shields.io/badge/LLM-Multi--Provider-brightgreen)
10
12
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
11
13
  ![Status](https://img.shields.io/badge/Render%20Hosting-Upcoming-gray)
14
+ ![Downloads](https://img.shields.io/badge/downloads-750-blue)
15
+ <!-- [![Users](https://img.shields.io/endpoint?url=https://github-pr-context-mcp.onrender.com/usage/badge)](https://github-pr-context-mcp.onrender.com/usage) -->
12
16
 
13
17
  **Production-grade context layer for AI code review, grounded in your repository's real pull request history.**
14
18
 
15
19
 
16
- > Tracking unique users across **uvx**, **pipx**, and **local** sources. (Render hosting upcoming)
17
-
18
20
  </div>
19
21
 
22
+ ## 🚀 Quick Start
23
+
24
+ ### 🚀 Zero-Setup (uvx / pipx / npx)
25
+ The fastest way to use the server. No cloning required. Just run one of these commands directly in your terminal or use them in your IDE's MCP settings:
26
+
27
+ > [!TIP]
28
+ > **Don't clone this repo to get AI rules!**
29
+ > Once installed, run `generate_repo_rules` inside **YOUR** project to automatically create `.cursorrules` or `CLAUDE.md` tailored to your own team's PR history.
30
+
31
+ **Using uvx (Recommended for speed):**
32
+ ```bash
33
+ uvx github-pr-context-mcp
34
+ ```
35
+
36
+ **Using pipx (Recommended for stability):**
37
+ ```bash
38
+ pipx run github-pr-context-mcp
39
+ # Or install permanently:
40
+ pipx install github-pr-context-mcp
41
+ ```
42
+
43
+ **Using npx (Smithery bridge):**
44
+ ```bash
45
+ npx -y @smithery/cli run github-pr-context-mcp
46
+ ```
47
+
48
+ ---
49
+
50
+ ### ⚠️ Manual Installation (Git Clone / Advanced)
51
+ > [!WARNING]
52
+ > Running from a git clone is **only recommended for developers** contributing to this project. For general use, please use the `pipx` method above.
53
+
54
+ If you have cloned the repository for development:
55
+ 1. Create a virtual environment: `python -m venv .venv`
56
+ 2. Activate it and install: `pip install -e .`
57
+ 3. Run automatic setup: `python scripts/install_clients.py`
58
+
59
+ For full configuration (Cursor, Claude Desktop), see the [**Quick Start Guide**](docs/quickstart.md).
60
+
20
61
  ---
21
62
 
22
63
  ## Overview
@@ -69,6 +110,24 @@ If your team has Hosted this MCP on Render, you do **NOT** need to `git clone` o
69
110
 
70
111
  ---
71
112
 
113
+ > [!IMPORTANT]
114
+ > **🚀 USE THE OFFICIAL PACKAGE:** This project is now on PyPI.
115
+ > To ensure seamless updates and zero configuration friction, do **NOT** `git clone`.
116
+ >
117
+ > **Recommended Install:**
118
+ > ```bash
119
+ > pipx install github-pr-context-mcp
120
+ > ```
121
+ > Or run instantly with: `uvx github-pr-context-mcp`
122
+
123
+ <div align="center">
124
+ <img src="assets/mcp_tool_guide_premium_v2.png" width="800" alt="GitHub PR Context MCP Tools">
125
+ </div>
126
+
127
+ <br/>
128
+
129
+ ---
130
+
72
131
  ## Key Capabilities
73
132
 
74
133
  | Capability | What It Delivers |
@@ -81,20 +140,6 @@ If your team has Hosted this MCP on Render, you do **NOT** need to `git clone` o
81
140
  | Flexible storage modes | Permanent (disk) and temporary (in-memory) indexing options |
82
141
  | Portable inference layer | Switch LLM providers using environment configuration only |
83
142
 
84
- ---
85
-
86
- ## Demo
87
-
88
- ![demo](assets/demo.gif)
89
-
90
- Example workflow:
91
- - Ask the assistant to review a diff using repository history.
92
- - The server retrieves similar past review context.
93
- - The model returns grounded feedback aligned to team expectations.
94
-
95
- ## Usage Analytics
96
-
97
- To help us understand adoption, the MCP server collects privacy-first, anonymous telemetry on deployments. Future hosted deployments will expose HTTP endpoints (`/stats` and `/ping`) that publicly display the **number of unique users**.
98
143
 
99
144
  ---
100
145
 
@@ -119,24 +164,17 @@ The server exposes 12 core tools for IDE agents and developers. For a deep dive
119
164
 
120
165
  ---
121
166
 
122
- ## Documentation
123
-
124
- Detailed guides are split into focused pages:
125
-
126
- - [Quick Start and Usage](docs/quickstart.md)
127
- - [LLM Configuration](docs/llm-configuration.md)
128
- - [Integrations](docs/integrations/index.md)
129
- - [Architecture and Tools](docs/architecture.md)
130
- - [Pipeline Deep Dive](docs/pipeline.md)
131
- - [Configuration Guide (Change Tokens/Settings)](docs/guides/configuration.md)
132
- - [Roadmap](docs/roadmap.md)
133
-
134
167
  ---
135
168
 
136
- ## Quick Links
169
+ ## 📖 Documentation
137
170
 
138
- - Access setup: [GitHub Token Guide](docs/GUIDE_GITHUB_TOKEN.md)
139
- - Client connection: [Integrations](docs/integrations/index.md)
171
+ Detailed guides for deep dives and specific configurations:
172
+
173
+ - 🛠️ [**Quick Start & Usage**](docs/quickstart.md) — Setup and basic commands.
174
+ - ⚙️ [**LLM Configuration**](docs/llm-configuration.md) — Switching between OpenAI, Anthropic, Gemini, and Cerebras.
175
+ - 🧩 [**Tool Strategy & Selection Guide**](docs/tools_strategy.md) — When to use which tool (for humans and agents).
176
+ - 🏗️ [**Architecture & Pipeline**](docs/architecture.md) — How the RAG engine and indexing work.
177
+ - 🔌 [**Integrations**](docs/integrations/index.md) — Connecting to Cursor, Claude Desktop, and more.
140
178
 
141
179
  ---
142
180
 
@@ -144,24 +182,12 @@ Detailed guides are split into focused pages:
144
182
 
145
183
  We want to hear from you—whether you are a solo developer or a team at a large company!
146
184
 
147
- ### 👤 For Individuals
148
185
  - **Feedback**: Please open an issue or start a discussion if you have ideas or encounter bugs.
149
- - **Show your support**: If this tool saves you time, give it a **Star ⭐**! It helps others find the project.
150
-
151
- ### 🏢 For Corporate & Teams
152
- - **Usage**: Is your team using this MCP server? Join our "Adopters" list by opening a PR to add your team's name.
153
- - **Corporate Feedback**: Open an issue with the `corporate-usage` label to tell us how this has improved your PR review workflow.
154
- - **Custom Integration**: Need help deploying this to your private cloud? Reach out via GitHub Discussions.
186
+ - **Star ⭐**: If this tool saves you time, give it a star! It helps others find the project.
187
+ - **Corporate**: Is your team using this? Join our "Adopters" list by opening a PR to add your team's name.
155
188
 
156
189
  ---
157
190
 
158
- ## 📜 Documentation & Guides
159
-
160
- - **Strategy & Best Practices**: [Tool Strategy & Selection Guide](docs/tools_strategy.md)
161
- - **Architecture**: [Architecture and Tools](docs/architecture.md)
162
- - **Pipeline**: [Pipeline Deep Dive](docs/pipeline.md)
163
- - **Usage**: [Quick Start and Usage](docs/quickstart.md)
164
-
165
191
  ## 🛠️ Troubleshooting
166
192
 
167
193
  - **"command not found"**: Use absolute paths in your configuration. Run `github-pr-context-mcp config` to get your exact path.
@@ -178,7 +178,13 @@ class UsageMetricsStore:
178
178
  return {
179
179
  "tracked_since": tracked_since_val,
180
180
  "total_tool_calls": total_calls_val,
181
- "total_unique_users": total_unique,
181
+ "metrics": {
182
+ "total_unique_users": total_unique,
183
+ "active_cli_users": total_ping_users,
184
+ "authenticated_users": total_auth_users,
185
+ "github_clones": github_clones_val,
186
+ "github_downloads": github_downloads_val
187
+ },
182
188
  "users_by_mode": users_by_mode,
183
189
  "top_tools": top_tools,
184
190
  "daily": daily_series,
@@ -0,0 +1,55 @@
1
+ import os
2
+ import threading
3
+ from mcp.server.fastmcp import FastMCP
4
+
5
+ from app.state import (
6
+ build_auth_settings,
7
+ token_verifier,
8
+ build_transport_security,
9
+ usage_store
10
+ )
11
+ from app.tools.indexing import register_indexing_tools
12
+ from app.tools.analysis import register_analysis_tools
13
+ from app.tools.generation import register_generation_tools
14
+ from app.tools.admin import register_admin_tools
15
+ from app.routes.http import register_http_routes
16
+
17
+ # Initialize FastMCP
18
+ mcp = FastMCP(
19
+ "github-pr-review-context",
20
+ host=os.getenv("HOST", "0.0.0.0"),
21
+ port=int(os.getenv("PORT", "8000")),
22
+ streamable_http_path=os.getenv("MCP_HTTP_PATH", "/mcp"),
23
+ auth=build_auth_settings(),
24
+ token_verifier=token_verifier,
25
+ transport_security=build_transport_security(),
26
+ )
27
+
28
+ # Register Tools
29
+ register_indexing_tools(mcp)
30
+ register_analysis_tools(mcp)
31
+ register_generation_tools(mcp)
32
+ register_admin_tools(mcp)
33
+
34
+ # Register HTTP Routes
35
+ register_http_routes(mcp)
36
+
37
+ # Background Sync Loop (GitHub Traffic)
38
+ def _github_sync_loop():
39
+ repo = os.getenv("GITHUB_TRAFFIC_REPO")
40
+ token = os.getenv("GITHUB_TOKEN")
41
+ if not repo or not token or not usage_store:
42
+ return
43
+
44
+ owner, name = repo.split("/", 1)
45
+ print(f"Starting GitHub traffic sync for {repo}...", flush=True)
46
+ while True:
47
+ try:
48
+ usage_store.sync_github_traffic(owner, name, token)
49
+ except Exception as e:
50
+ print(f"GitHub traffic sync failed: {e}", flush=True)
51
+ import time
52
+ time.sleep(3600 * 12) # Sync every 12 hours
53
+
54
+ if os.getenv("GITHUB_TRAFFIC_REPO"):
55
+ threading.Thread(target=_github_sync_loop, daemon=True).start()
@@ -0,0 +1,90 @@
1
+ from starlette.requests import Request
2
+ from starlette.responses import JSONResponse
3
+ from mcp.server.auth.middleware.auth_context import get_access_token
4
+ from app.state import (
5
+ identity_store,
6
+ usage_store,
7
+ current_user_email,
8
+ current_user_settings,
9
+ validate_admin_token
10
+ )
11
+
12
+ def register_http_routes(mcp):
13
+ @mcp.custom_route("/settings", methods=["PUT"], include_in_schema=False)
14
+ async def update_settings_route(request: Request):
15
+ access_token = get_access_token()
16
+ if access_token is None or identity_store is None:
17
+ return JSONResponse({"error": "unauthorized"}, status_code=401)
18
+
19
+ try:
20
+ payload = await request.json()
21
+ except Exception:
22
+ return JSONResponse({"error": "invalid_json"}, status_code=400)
23
+
24
+ settings = payload.get("settings") if isinstance(payload, dict) else None
25
+ if not isinstance(settings, dict):
26
+ return JSONResponse({"error": "settings must be an object"}, status_code=400)
27
+
28
+ try:
29
+ updated = identity_store.update_user_settings(access_token.client_id, settings)
30
+ except ValueError as exc:
31
+ return JSONResponse({"error": str(exc)}, status_code=400)
32
+
33
+ return JSONResponse({"email": access_token.client_id, "settings": updated})
34
+
35
+ @mcp.custom_route("/whoami", methods=["GET"], include_in_schema=False)
36
+ async def whoami_route(_: Request):
37
+ access_token = get_access_token()
38
+ if access_token is None:
39
+ return JSONResponse({"error": "unauthorized"}, status_code=401)
40
+ user_settings = current_user_settings()
41
+ return JSONResponse({
42
+ "email": access_token.client_id,
43
+ "scopes": access_token.scopes,
44
+ "has_custom_github_token": bool(user_settings.get("github_token")),
45
+ "has_custom_llm": any(user_settings.get(k) for k in ("llm_provider", "llm_model", "llm_api_key", "llm_base_url")),
46
+ })
47
+
48
+ @mcp.custom_route("/healthz", methods=["GET"], include_in_schema=False)
49
+ async def healthz(_: Request):
50
+ return JSONResponse({"status": "ok"})
51
+
52
+ @mcp.custom_route("/ping", methods=["POST"], include_in_schema=False)
53
+ async def ping(request: Request):
54
+ """Anonymous telemetry ping from CLI (uvx/pipx)."""
55
+ if usage_store is None:
56
+ return JSONResponse({"enabled": False}, status_code=503)
57
+ try:
58
+ payload = await request.json()
59
+ uid = payload.get("id")
60
+ mode = payload.get("mode", "unknown")
61
+ if not uid:
62
+ return JSONResponse({"error": "invalid_id"}, status_code=400)
63
+ usage_store.record_ping(uid, mode)
64
+ return JSONResponse({"ok": True})
65
+ except Exception:
66
+ return JSONResponse({"error": "invalid_request"}, status_code=400)
67
+
68
+ @mcp.custom_route("/usage", methods=["GET"], include_in_schema=False)
69
+ async def usage(request: Request):
70
+ if usage_store is None:
71
+ return JSONResponse({"enabled": False}, status_code=503)
72
+ token = request.query_params.get("token")
73
+ if not validate_admin_token(token):
74
+ return JSONResponse({"error": "unauthorized"}, status_code=401)
75
+ return JSONResponse(usage_store.summary())
76
+
77
+ @mcp.custom_route("/usage/badge", methods=["GET"], include_in_schema=False)
78
+ async def usage_badge(_: Request):
79
+ """Returns a Shields.io compatible JSON for the user count."""
80
+ if usage_store is None:
81
+ return JSONResponse({"schemaVersion": 1, "label": "users", "message": "off", "color": "gray"})
82
+ summary = usage_store.summary()
83
+ count = summary.get("metrics", {}).get("total_unique_users", 0)
84
+ return JSONResponse({
85
+ "schemaVersion": 1,
86
+ "label": "users",
87
+ "message": str(count),
88
+ "color": "blueviolet",
89
+ "style": "flat-square",
90
+ })
@@ -0,0 +1,174 @@
1
+ import hmac
2
+ import os
3
+ import re
4
+ import sys
5
+ from mcp.server.fastmcp import Context
6
+ from mcp.server.auth.middleware.auth_context import get_access_token
7
+ from mcp.server.auth.provider import AccessToken
8
+ from mcp.server.auth.settings import AuthSettings
9
+ from mcp.server.transport_security import TransportSecuritySettings
10
+ from urllib.parse import urlparse
11
+
12
+ from auth import GmailIdentityStore, GmailTokenVerifier
13
+ from analytics import UsageMetricsStore
14
+ from storage import (
15
+ repo_is_indexed_permanently,
16
+ repo_is_indexed_temporarily,
17
+ )
18
+
19
+ # --- Configuration Constants ---
20
+ USAGE_TRACKING_ENABLED = os.getenv("USAGE_TRACKING_ENABLED", "true").strip().lower() in {"1", "true", "yes", "on"}
21
+ AUTH_REQUIRED = os.getenv("AUTH_REQUIRED", "false").strip().lower() in {"1", "true", "yes", "on"}
22
+ REGISTRATION_SECRET = os.getenv("REGISTRATION_SECRET", "").strip()
23
+ MCP_PUBLIC_URL = os.getenv("MCP_PUBLIC_URL", "").strip()
24
+ AUTH_REGISTRY_PATH = os.getenv("AUTH_REGISTRY_PATH", "./chroma_db/auth_registry.json")
25
+ USAGE_METRICS_TOKEN = os.getenv("USAGE_METRICS_TOKEN", "").strip()
26
+ USAGE_STATS_PATH = os.getenv("USAGE_STATS_PATH", "./chroma_db/usage_stats.json")
27
+
28
+ # --- Globals ---
29
+ identity_store = GmailIdentityStore(AUTH_REGISTRY_PATH) if AUTH_REQUIRED else None
30
+ token_verifier = GmailTokenVerifier(identity_store) if identity_store else None
31
+ usage_store = UsageMetricsStore(USAGE_STATS_PATH) if USAGE_TRACKING_ENABLED else None
32
+
33
+ # Stateful per connected client/session
34
+ _sessions: dict[str, dict] = {}
35
+
36
+ # --- Helper Functions ---
37
+ def normalize_repo(repo: str | None) -> str:
38
+ """Strict validation for GitHub repository identifiers (owner/name)."""
39
+ if not repo:
40
+ raise ValueError("Repository identifier is required (e.g. 'owner/repo').")
41
+
42
+ # Handle full URLs
43
+ if repo.endswith(".git"):
44
+ repo = repo[:-4]
45
+ match = re.search(r"(?:github\.com/)?([^/]+/[^/]+)", repo)
46
+ if match:
47
+ repo = match.group(1).split("#")[0].split("?")[0]
48
+
49
+ if not re.fullmatch(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$", repo):
50
+ raise ValueError(f"Invalid repository format: '{repo}'. Expected 'owner/repo'.")
51
+
52
+ return repo
53
+
54
+ def normalize_namespace(namespace: str | None) -> str | None:
55
+ if namespace is None:
56
+ return None
57
+ ns = namespace.strip()
58
+ return ns or None
59
+
60
+ def current_user_email() -> str | None:
61
+ access_token = get_access_token()
62
+ if isinstance(access_token, AccessToken):
63
+ return normalize_namespace(access_token.client_id)
64
+ return None
65
+
66
+ def current_user_settings() -> dict:
67
+ store = identity_store
68
+ if not store:
69
+ return {}
70
+ email = current_user_email()
71
+ if not email:
72
+ return {}
73
+ return store.get_user_settings(email)
74
+
75
+ def llm_settings(user_settings: dict[str, str]) -> dict[str, str]:
76
+ llm: dict[str, str] = {}
77
+ for key in ("llm_provider", "llm_model", "llm_api_key", "llm_base_url"):
78
+ value = user_settings.get(key)
79
+ if value:
80
+ llm[key] = value
81
+ return llm
82
+
83
+ def repo_state_key(repo_key: str, namespace: str | None) -> str:
84
+ ns = normalize_namespace(namespace) or "_default"
85
+ return f"{ns}::{repo_key}"
86
+
87
+ def session_id(ctx: Context) -> str:
88
+ return current_user_email() or ctx.client_id or f"session-{id(ctx.session)}"
89
+
90
+ def get_state(ctx: Context) -> dict:
91
+ sid = session_id(ctx)
92
+ if sid not in _sessions:
93
+ configured_ns = normalize_namespace(os.getenv("MCP_NAMESPACE", ""))
94
+ _sessions[sid] = {
95
+ "active_repo": None,
96
+ "active_namespace": configured_ns or current_user_email() or normalize_namespace(ctx.client_id),
97
+ "storage_types": {},
98
+ }
99
+ return _sessions[sid]
100
+
101
+ def resolve_namespace(requested_namespace: str | None, state: dict) -> str | None:
102
+ current_email = current_user_email()
103
+ if AUTH_REQUIRED:
104
+ if not current_email:
105
+ raise ValueError("Unauthorized: missing identity when AUTH_REQUIRED is true.")
106
+ return normalize_namespace(current_email)
107
+ return normalize_namespace(requested_namespace if requested_namespace is not None else state.get("active_namespace"))
108
+
109
+ def resolve_repo(repo: str | None, state: dict) -> str:
110
+ if repo:
111
+ return normalize_repo(repo)
112
+ active = state.get("active_repo")
113
+ if not active:
114
+ raise ValueError("No repo specified and no active repo set. Use ensure_repo_ready first, or pass repo explicitly.")
115
+ return normalize_repo(active)
116
+
117
+ def is_temporary(repo_key: str, namespace: str | None, state: dict) -> bool:
118
+ key = repo_state_key(repo_key, namespace)
119
+ known = state["storage_types"].get(key)
120
+ if known is not None:
121
+ return known == "temporary"
122
+ return repo_is_indexed_temporarily(repo_key, namespace=namespace)
123
+
124
+ def namespace_text(namespace: str | None) -> str:
125
+ if namespace:
126
+ return f"\nNamespace: {namespace}"
127
+ return ""
128
+
129
+ def usage_user_id(ctx: Context, namespace: str | None) -> str:
130
+ current_email = current_user_email()
131
+ if current_email:
132
+ return f"email:{current_email}"
133
+ if namespace:
134
+ return f"ns:{namespace}"
135
+ if ctx.client_id:
136
+ return f"client:{ctx.client_id}"
137
+ return session_id(ctx)
138
+
139
+ def track_usage(ctx: Context, namespace: str | None, tool_name: str) -> None:
140
+ if usage_store is None:
141
+ return
142
+ usage_store.record_event(usage_user_id(ctx, namespace), tool_name)
143
+
144
+ def validate_admin_token(admin_token: str | None) -> bool:
145
+ if not USAGE_METRICS_TOKEN:
146
+ return True
147
+ return hmac.compare_digest(admin_token or "", USAGE_METRICS_TOKEN)
148
+
149
+ def build_auth_settings() -> AuthSettings | None:
150
+ if not AUTH_REQUIRED:
151
+ return None
152
+ if not MCP_PUBLIC_URL:
153
+ raise ValueError("MCP_PUBLIC_URL is required when AUTH_REQUIRED=true")
154
+ if not REGISTRATION_SECRET:
155
+ raise ValueError("REGISTRATION_SECRET is required when AUTH_REQUIRED=true")
156
+ public_url = MCP_PUBLIC_URL.rstrip("/")
157
+ return AuthSettings(
158
+ issuer_url=public_url,
159
+ resource_server_url=public_url,
160
+ service_documentation_url=os.getenv("AUTH_SERVICE_DOC_URL", public_url),
161
+ required_scopes=["identity:gmail"],
162
+ )
163
+
164
+ def build_transport_security() -> TransportSecuritySettings | None:
165
+ if not AUTH_REQUIRED or not MCP_PUBLIC_URL:
166
+ return None
167
+ parsed = urlparse(MCP_PUBLIC_URL)
168
+ host = parsed.netloc
169
+ origin = f"{parsed.scheme}://{parsed.netloc}"
170
+ return TransportSecuritySettings(
171
+ enable_dns_rebinding_protection=True,
172
+ allowed_hosts=[host],
173
+ allowed_origins=[origin],
174
+ )