github-pr-context-mcp 0.2.6__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.6 → github_pr_context_mcp-0.2.7}/PKG-INFO +72 -48
  2. {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/README.md +71 -47
  3. {github_pr_context_mcp-0.2.6 → 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.6 → github_pr_context_mcp-0.2.7}/entrypoints/local/server.py +16 -1
  12. {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/github_pr_context_mcp.egg-info/PKG-INFO +72 -48
  13. {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/github_pr_context_mcp.egg-info/SOURCES.txt +6 -0
  14. {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/pyproject.toml +1 -1
  15. github_pr_context_mcp-0.2.6/app/mcp_app.py +0 -928
  16. {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/LICENSE +0 -0
  17. {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/analytics/__init__.py +0 -0
  18. {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/app/__init__.py +0 -0
  19. {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/auth/__init__.py +0 -0
  20. {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/auth/gmail_identity.py +0 -0
  21. {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/entrypoints/deployed/server.py +0 -0
  22. {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/fetcher/__init__.py +0 -0
  23. {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/fetcher/client.py +0 -0
  24. {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/fetcher/queries.py +0 -0
  25. {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/fetcher/transform.py +0 -0
  26. {github_pr_context_mcp-0.2.6 → 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.6 → 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.6 → github_pr_context_mcp-0.2.7}/github_pr_context_mcp.egg-info/requires.txt +0 -0
  29. {github_pr_context_mcp-0.2.6 → 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.6 → github_pr_context_mcp-0.2.7}/inference/__init__.py +0 -0
  31. {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/inference/providers.py +0 -0
  32. {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/inference/review.py +0 -0
  33. {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/setup.cfg +0 -0
  34. {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/storage/__init__.py +0 -0
  35. {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/storage/document_builder.py +0 -0
  36. {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/storage/encoder.py +0 -0
  37. {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/storage/vector_store.py +0 -0
  38. {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/tests/test_fixes.py +0 -0
  39. {github_pr_context_mcp-0.2.6 → 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.6
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,16 +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)
31
- ![PyPI - Downloads](https://img.shields.io/pypi/dm/github-pr-context-mcp)
32
- ![Downloads](https://img.shields.io/badge/downloads-525-blue)
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) -->
33
35
 
34
36
  **Production-grade context layer for AI code review, grounded in your repository's real pull request history.**
35
37
 
36
38
 
37
- > Tracking unique users across **uvx**, **pipx**, and **local** sources. (Render hosting upcoming)
38
-
39
39
  </div>
40
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
+
41
80
  ---
42
81
 
43
82
  ## Overview
@@ -90,6 +129,24 @@ If your team has Hosted this MCP on Render, you do **NOT** need to `git clone` o
90
129
 
91
130
  ---
92
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
+
93
150
  ## Key Capabilities
94
151
 
95
152
  | Capability | What It Delivers |
@@ -102,20 +159,6 @@ If your team has Hosted this MCP on Render, you do **NOT** need to `git clone` o
102
159
  | Flexible storage modes | Permanent (disk) and temporary (in-memory) indexing options |
103
160
  | Portable inference layer | Switch LLM providers using environment configuration only |
104
161
 
105
- ---
106
-
107
- ## Demo
108
-
109
- ![demo](assets/demo.gif)
110
-
111
- Example workflow:
112
- - Ask the assistant to review a diff using repository history.
113
- - The server retrieves similar past review context.
114
- - The model returns grounded feedback aligned to team expectations.
115
-
116
- ## Usage Analytics
117
-
118
- 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**.
119
162
 
120
163
  ---
121
164
 
@@ -140,24 +183,17 @@ The server exposes 12 core tools for IDE agents and developers. For a deep dive
140
183
 
141
184
  ---
142
185
 
143
- ## Documentation
144
-
145
- Detailed guides are split into focused pages:
146
-
147
- - [Quick Start and Usage](docs/quickstart.md)
148
- - [LLM Configuration](docs/llm-configuration.md)
149
- - [Integrations](docs/integrations/index.md)
150
- - [Architecture and Tools](docs/architecture.md)
151
- - [Pipeline Deep Dive](docs/pipeline.md)
152
- - [Configuration Guide (Change Tokens/Settings)](docs/guides/configuration.md)
153
- - [Roadmap](docs/roadmap.md)
154
-
155
186
  ---
156
187
 
157
- ## Quick Links
188
+ ## 📖 Documentation
158
189
 
159
- - Access setup: [GitHub Token Guide](docs/GUIDE_GITHUB_TOKEN.md)
160
- - 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.
161
197
 
162
198
  ---
163
199
 
@@ -165,24 +201,12 @@ Detailed guides are split into focused pages:
165
201
 
166
202
  We want to hear from you—whether you are a solo developer or a team at a large company!
167
203
 
168
- ### 👤 For Individuals
169
204
  - **Feedback**: Please open an issue or start a discussion if you have ideas or encounter bugs.
170
- - **Show your support**: If this tool saves you time, give it a **Star ⭐**! It helps others find the project.
171
-
172
- ### 🏢 For Corporate & Teams
173
- - **Usage**: Is your team using this MCP server? Join our "Adopters" list by opening a PR to add your team's name.
174
- - **Corporate Feedback**: Open an issue with the `corporate-usage` label to tell us how this has improved your PR review workflow.
175
- - **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.
176
207
 
177
208
  ---
178
209
 
179
- ## 📜 Documentation & Guides
180
-
181
- - **Strategy & Best Practices**: [Tool Strategy & Selection Guide](docs/tools_strategy.md)
182
- - **Architecture**: [Architecture and Tools](docs/architecture.md)
183
- - **Pipeline**: [Pipeline Deep Dive](docs/pipeline.md)
184
- - **Usage**: [Quick Start and Usage](docs/quickstart.md)
185
-
186
210
  ## 🛠️ Troubleshooting
187
211
 
188
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,16 +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)
12
- ![PyPI - Downloads](https://img.shields.io/pypi/dm/github-pr-context-mcp)
13
- ![Downloads](https://img.shields.io/badge/downloads-525-blue)
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) -->
14
16
 
15
17
  **Production-grade context layer for AI code review, grounded in your repository's real pull request history.**
16
18
 
17
19
 
18
- > Tracking unique users across **uvx**, **pipx**, and **local** sources. (Render hosting upcoming)
19
-
20
20
  </div>
21
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
+
22
61
  ---
23
62
 
24
63
  ## Overview
@@ -71,6 +110,24 @@ If your team has Hosted this MCP on Render, you do **NOT** need to `git clone` o
71
110
 
72
111
  ---
73
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
+
74
131
  ## Key Capabilities
75
132
 
76
133
  | Capability | What It Delivers |
@@ -83,20 +140,6 @@ If your team has Hosted this MCP on Render, you do **NOT** need to `git clone` o
83
140
  | Flexible storage modes | Permanent (disk) and temporary (in-memory) indexing options |
84
141
  | Portable inference layer | Switch LLM providers using environment configuration only |
85
142
 
86
- ---
87
-
88
- ## Demo
89
-
90
- ![demo](assets/demo.gif)
91
-
92
- Example workflow:
93
- - Ask the assistant to review a diff using repository history.
94
- - The server retrieves similar past review context.
95
- - The model returns grounded feedback aligned to team expectations.
96
-
97
- ## Usage Analytics
98
-
99
- 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**.
100
143
 
101
144
  ---
102
145
 
@@ -121,24 +164,17 @@ The server exposes 12 core tools for IDE agents and developers. For a deep dive
121
164
 
122
165
  ---
123
166
 
124
- ## Documentation
125
-
126
- Detailed guides are split into focused pages:
127
-
128
- - [Quick Start and Usage](docs/quickstart.md)
129
- - [LLM Configuration](docs/llm-configuration.md)
130
- - [Integrations](docs/integrations/index.md)
131
- - [Architecture and Tools](docs/architecture.md)
132
- - [Pipeline Deep Dive](docs/pipeline.md)
133
- - [Configuration Guide (Change Tokens/Settings)](docs/guides/configuration.md)
134
- - [Roadmap](docs/roadmap.md)
135
-
136
167
  ---
137
168
 
138
- ## Quick Links
169
+ ## 📖 Documentation
139
170
 
140
- - Access setup: [GitHub Token Guide](docs/GUIDE_GITHUB_TOKEN.md)
141
- - 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.
142
178
 
143
179
  ---
144
180
 
@@ -146,24 +182,12 @@ Detailed guides are split into focused pages:
146
182
 
147
183
  We want to hear from you—whether you are a solo developer or a team at a large company!
148
184
 
149
- ### 👤 For Individuals
150
185
  - **Feedback**: Please open an issue or start a discussion if you have ideas or encounter bugs.
151
- - **Show your support**: If this tool saves you time, give it a **Star ⭐**! It helps others find the project.
152
-
153
- ### 🏢 For Corporate & Teams
154
- - **Usage**: Is your team using this MCP server? Join our "Adopters" list by opening a PR to add your team's name.
155
- - **Corporate Feedback**: Open an issue with the `corporate-usage` label to tell us how this has improved your PR review workflow.
156
- - **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.
157
188
 
158
189
  ---
159
190
 
160
- ## 📜 Documentation & Guides
161
-
162
- - **Strategy & Best Practices**: [Tool Strategy & Selection Guide](docs/tools_strategy.md)
163
- - **Architecture**: [Architecture and Tools](docs/architecture.md)
164
- - **Pipeline**: [Pipeline Deep Dive](docs/pipeline.md)
165
- - **Usage**: [Quick Start and Usage](docs/quickstart.md)
166
-
167
191
  ## 🛠️ Troubleshooting
168
192
 
169
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
+ )