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.
- {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/PKG-INFO +72 -48
- {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/README.md +71 -47
- {github_pr_context_mcp-0.2.6 → 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.6 → github_pr_context_mcp-0.2.7}/entrypoints/local/server.py +16 -1
- {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/github_pr_context_mcp.egg-info/PKG-INFO +72 -48
- {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/github_pr_context_mcp.egg-info/SOURCES.txt +6 -0
- {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/pyproject.toml +1 -1
- github_pr_context_mcp-0.2.6/app/mcp_app.py +0 -928
- {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/LICENSE +0 -0
- {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/analytics/__init__.py +0 -0
- {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/app/__init__.py +0 -0
- {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/auth/__init__.py +0 -0
- {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/auth/gmail_identity.py +0 -0
- {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/entrypoints/deployed/server.py +0 -0
- {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/fetcher/__init__.py +0 -0
- {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/fetcher/client.py +0 -0
- {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/fetcher/queries.py +0 -0
- {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/fetcher/transform.py +0 -0
- {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
- {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
- {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/github_pr_context_mcp.egg-info/requires.txt +0 -0
- {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
- {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/inference/__init__.py +0 -0
- {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/inference/providers.py +0 -0
- {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/inference/review.py +0 -0
- {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/setup.cfg +0 -0
- {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/storage/__init__.py +0 -0
- {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/storage/document_builder.py +0 -0
- {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/storage/encoder.py +0 -0
- {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/storage/vector_store.py +0 -0
- {github_pr_context_mcp-0.2.6 → github_pr_context_mcp-0.2.7}/tests/test_fixes.py +0 -0
- {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.
|
|
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,16 +30,53 @@ Dynamic: license-file
|
|
|
28
30
|

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

|
|
31
|
-

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

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

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

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

|
|
12
|
-

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

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