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.
- {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/PKG-INFO +72 -46
- {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/README.md +71 -45
- {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/analytics/usage_metrics.py +7 -1
- github_pr_context_mcp-0.2.7/app/mcp_app.py +55 -0
- github_pr_context_mcp-0.2.7/app/routes/http.py +90 -0
- github_pr_context_mcp-0.2.7/app/state.py +174 -0
- github_pr_context_mcp-0.2.7/app/tools/admin.py +57 -0
- github_pr_context_mcp-0.2.7/app/tools/analysis.py +95 -0
- github_pr_context_mcp-0.2.7/app/tools/generation.py +125 -0
- github_pr_context_mcp-0.2.7/app/tools/indexing.py +241 -0
- {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/entrypoints/local/server.py +18 -3
- {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/github_pr_context_mcp.egg-info/PKG-INFO +72 -46
- {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/github_pr_context_mcp.egg-info/SOURCES.txt +6 -0
- {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/pyproject.toml +1 -1
- github_pr_context_mcp-0.2.5/app/mcp_app.py +0 -928
- {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/LICENSE +0 -0
- {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/analytics/__init__.py +0 -0
- {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/app/__init__.py +0 -0
- {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/auth/__init__.py +0 -0
- {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/auth/gmail_identity.py +0 -0
- {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/entrypoints/deployed/server.py +0 -0
- {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/fetcher/__init__.py +0 -0
- {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/fetcher/client.py +0 -0
- {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/fetcher/queries.py +0 -0
- {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/fetcher/transform.py +0 -0
- {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
- {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
- {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/github_pr_context_mcp.egg-info/requires.txt +0 -0
- {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
- {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/inference/__init__.py +0 -0
- {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/inference/providers.py +0 -0
- {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/inference/review.py +0 -0
- {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/setup.cfg +0 -0
- {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/storage/__init__.py +0 -0
- {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/storage/document_builder.py +0 -0
- {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/storage/encoder.py +0 -0
- {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/storage/vector_store.py +0 -0
- {github_pr_context_mcp-0.2.5 → github_pr_context_mcp-0.2.7}/tests/test_fixes.py +0 -0
- {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.
|
|
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
|

|
|
@@ -28,14 +30,53 @@ Dynamic: license-file
|
|
|
28
30
|

|
|
29
31
|
[](LICENSE)
|
|
30
32
|

|
|
33
|
+

|
|
34
|
+
<!-- [](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
|
-

|
|
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
|
-
##
|
|
188
|
+
## 📖 Documentation
|
|
156
189
|
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
- **
|
|
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
|

|
|
@@ -9,14 +11,53 @@
|
|
|
9
11
|

|
|
10
12
|
[](LICENSE)
|
|
11
13
|

|
|
14
|
+

|
|
15
|
+
<!-- [](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
|
-

|
|
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
|
-
##
|
|
169
|
+
## 📖 Documentation
|
|
137
170
|
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
- **
|
|
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
|
-
"
|
|
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
|
+
)
|