projectpulse-mcp 1.3.0 → 1.4.0
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.
- package/README.md +109 -74
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/tools/compare-repos.d.ts +21 -0
- package/dist/tools/compare-repos.d.ts.map +1 -0
- package/dist/tools/compare-repos.js +81 -0
- package/dist/tools/compare-repos.js.map +1 -0
- package/dist/tools/compare-repos.test.d.ts +2 -0
- package/dist/tools/compare-repos.test.d.ts.map +1 -0
- package/dist/tools/compare-repos.test.js +138 -0
- package/dist/tools/compare-repos.test.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,117 +1,152 @@
|
|
|
1
|
-
|
|
1
|
+
<div id="top" align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<img src="https://raw.githubusercontent.com/alexbypa/github-projectpulse-mcp/main/docs/images/code-scanning-example.png" width="80%" alt="ProjectPulse MCP Logo" style="border-radius: 12px; box-shadow: 0 8px 24px rgba(106, 13, 173, 0.2); margin-bottom: 20px;"/>
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
# ⚡ ProjectPulse MCP
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**Monitor, score, and compare GitHub repository health directly from your AI agent conversations.**
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
- **Ping**: Simple connectivity test tool.
|
|
9
|
+
[](https://www.npmjs.com/package/projectpulse-mcp)
|
|
10
|
+
[](https://opensource.org/licenses/MIT)
|
|
11
|
+
[](https://www.typescriptlang.org/)
|
|
12
|
+
[](https://modelcontextprotocol.io)
|
|
13
|
+
[](https://vitest.dev/)
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
<p align="center">
|
|
16
|
+
<a href="#-overview">Overview</a> •
|
|
17
|
+
<a href="#-features">Features</a> •
|
|
18
|
+
<a href="#-tools-reference">Tools</a> •
|
|
19
|
+
<a href="#-quick-start">Quick Start</a> •
|
|
20
|
+
<a href="#-configuration">Configuration</a> •
|
|
21
|
+
<a href="#-license">License</a>
|
|
22
|
+
</p>
|
|
17
23
|
|
|
18
|
-
|
|
24
|
+
---
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
- **Lead Time for Changes** — median time from PR creation to merge (in hours)
|
|
22
|
-
- **Change Failure Rate** — percentage of CI workflow runs that fail
|
|
23
|
-
- **Mean Time to Recovery (MTTR)** — median time between a CI failure and the next success on the same branch
|
|
26
|
+
</div>
|
|
24
27
|
|
|
25
|
-
|
|
28
|
+
## 🌟 Overview
|
|
26
29
|
|
|
27
|
-
|
|
30
|
+
**ProjectPulse MCP** is a comprehensive [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server engineered for AI assistants (Claude Desktop, Cursor, Windsurf, Claude Code CLI). It equips LLMs with real-time capabilities to evaluate codebase maintainability, monitor CI/CD reliability, audit security vulnerabilities, track DORA engineering metrics, and rank multiple repositories side-by-side.
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
---
|
|
30
33
|
|
|
31
|
-
##
|
|
34
|
+
## 🚀 Features
|
|
32
35
|
|
|
33
|
-
###
|
|
36
|
+
### 🔍 1. Multi-Repository Benchmark & Comparison
|
|
37
|
+
* **`compare_repos`**: Compares 2 to 5 repositories simultaneously and returns a ranked JSON score.
|
|
38
|
+
* **Competition Ranking**: Employs standard competition ranking (1224 scheme) with full tie-breaking.
|
|
39
|
+
* **Fault Tolerance**: Isolates repository errors (e.g. 404s) so remaining repositories are scored seamlessly.
|
|
34
40
|
|
|
35
|
-
|
|
41
|
+
### 📊 2. Algorithmic Health Scoring (Grade A–F)
|
|
42
|
+
* **`get_health_score`**: Weighted composite metric across 5 dimensions:
|
|
43
|
+
- **CI/CD Reliability (25%)**: Evaluates pass rate across recent workflow runs.
|
|
44
|
+
- **Security & Dependabot (25%)**: Audits open critical, high, medium, and low security advisories.
|
|
45
|
+
- **Codebase Freshness (20%)**: Tracks commit recency and push activity.
|
|
46
|
+
- **Community & Activity (15%)**: Measures open issue resolution and fork engagement.
|
|
47
|
+
- **Maintenance Standards (15%)**: Assesses licensing, descriptions, and active status.
|
|
36
48
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
### ⏱️ 3. Proxy DORA Metrics
|
|
50
|
+
* **`get_dora_metrics`**: Computes core DevOps Research & Assessment metrics over a configurable window (7–90 days):
|
|
51
|
+
- **Deployment Frequency**: Weekly release throughput.
|
|
52
|
+
- **Lead Time for Changes**: Median PR duration from opening to merge.
|
|
53
|
+
- **Change Failure Rate**: Proportion of failed workflow executions.
|
|
54
|
+
- **Mean Time to Recovery (MTTR)**: Median duration between failure and subsequent branch recovery.
|
|
55
|
+
|
|
56
|
+
### 🛡️ 4. Security & Code Quality Audits
|
|
57
|
+
* **`analyze_dependencies`**: Detailed Dependabot alert breakdowns with severity filtering.
|
|
58
|
+
* **`analyze_code_scanning`**: CodeQL static analysis and security scanning summaries.
|
|
59
|
+
* **`check_ci_status`**: Live inspection of recent GitHub Actions pipeline conclusions.
|
|
60
|
+
* **`get_repo_health`**: Raw telemetry (stars, forks, languages, licenses, default branches).
|
|
61
|
+
|
|
62
|
+
---
|
|
40
63
|
|
|
41
|
-
|
|
64
|
+
## 🛠️ Tools Reference
|
|
42
65
|
|
|
43
|
-
|
|
66
|
+
| Tool Name | Description | Input Schema |
|
|
67
|
+
| :--- | :--- | :--- |
|
|
68
|
+
| **`compare_repos`** | Compares and ranks 2 to 5 GitHub repositories by health score using standard competition ranking. | `repos: Array<{ owner: string, repo: string }>` *(min: 2, max: 5)* |
|
|
69
|
+
| **`get_health_score`** | Calculates a 0–100 weighted health score with letter grade (A–F), category breakdown, and suggestions. | `owner: string`, `repo: string` |
|
|
70
|
+
| **`get_dora_metrics`** | Calculates DORA proxy metrics (Deployment Frequency, Lead Time, Change Failure Rate, MTTR). | `owner: string`, `repo: string`, `days?: number` *(7–90, default: 30)* |
|
|
71
|
+
| **`get_repo_health`** | Retrieves core repository metadata (stars, open issues, language, license, activity). | `owner: string`, `repo: string` |
|
|
72
|
+
| **`analyze_dependencies`** | Lists Dependabot security advisories grouped by severity level. | `owner: string`, `repo: string`, `severity?: "critical" \| "high" \| "medium" \| "low"` |
|
|
73
|
+
| **`analyze_code_scanning`** | Fetches CodeQL and static analysis scanning alerts. | `owner: string`, `repo: string` |
|
|
74
|
+
| **`check_ci_status`** | Fetches recent GitHub Actions workflow runs and conclusions. | `owner: string`, `repo: string`, `limit?: number` *(default: 10)* |
|
|
75
|
+
| **`ping`** | Simple connectivity and latency validation check. | `message: string` |
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 📦 Quick Start
|
|
44
80
|
|
|
45
|
-
|
|
81
|
+
### Option A: Claude Desktop Setup
|
|
82
|
+
|
|
83
|
+
Add the server to your `claude_desktop_config.json`:
|
|
84
|
+
* **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
85
|
+
* **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
46
86
|
|
|
47
87
|
```json
|
|
48
88
|
{
|
|
49
89
|
"mcpServers": {
|
|
50
90
|
"projectpulse": {
|
|
51
91
|
"command": "npx",
|
|
52
|
-
"args": [
|
|
53
|
-
"-y",
|
|
54
|
-
"projectpulse-mcp"
|
|
55
|
-
],
|
|
92
|
+
"args": ["-y", "projectpulse-mcp"],
|
|
56
93
|
"env": {
|
|
57
|
-
"GITHUB_TOKEN": "
|
|
94
|
+
"GITHUB_TOKEN": "ghp_your_github_personal_access_token"
|
|
58
95
|
}
|
|
59
96
|
}
|
|
60
97
|
}
|
|
61
98
|
}
|
|
62
99
|
```
|
|
63
100
|
|
|
64
|
-
###
|
|
101
|
+
### Option B: Claude Code CLI
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
claude mcp add projectpulse -- npx projectpulse-mcp
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Option C: Cursor & Other MCP Clients (stdio transport)
|
|
108
|
+
* **Command:** `npx`
|
|
109
|
+
* **Args:** `-y projectpulse-mcp`
|
|
110
|
+
* **Environment Variable:** `GITHUB_TOKEN=your_token_here`
|
|
65
111
|
|
|
66
|
-
|
|
112
|
+
---
|
|
67
113
|
|
|
68
|
-
|
|
69
|
-
- **Command**: `npx`
|
|
70
|
-
- **Arguments**: `-y projectpulse-mcp`
|
|
71
|
-
- **Environment Variables**: Add `GITHUB_TOKEN` with your GitHub Personal Access Token.
|
|
114
|
+
## 🔑 Configuration & GitHub Tokens
|
|
72
115
|
|
|
73
|
-
|
|
116
|
+
To prevent GitHub API rate-limiting and allow reading security advisories (Dependabot/CodeQL), generate a GitHub Personal Access Token (PAT):
|
|
74
117
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
| `get_health_score` | Calculate a 0-100 health score for a GitHub repository with grade (A-F), category breakdown, and improvement suggestions. | `owner` (string), `repo` (string) |
|
|
78
|
-
| `get_repo_health` | Get health information for a GitHub repository (stars, issues, language, license, etc.). | `owner` (string), `repo` (string) |
|
|
79
|
-
| `analyze_dependencies` | Get Dependabot alerts for a GitHub repository. | `owner` (string), `repo` (string), `severity` (optional enum: critical, high, medium, low) |
|
|
80
|
-
| `check_ci_status` | Get recent CI/CD workflow runs for a GitHub repository. | `owner` (string), `repo` (string), `limit` (optional number, default: 10) |
|
|
81
|
-
| `analyze_code_scanning` | Get Code Scanning alerts for a GitHub repository with CodeQL. | `owner` (string), `repo` (string) |
|
|
82
|
-
| `get_dora_metrics` | Calculate DORA proxy metrics (deployment frequency, lead time, change failure rate, MTTR) for a GitHub repository. | `owner` (string), `repo` (string), `days` (optional number, default: 30, range: 7-90) |
|
|
83
|
-
| `ping` | Simple connectivity check that responds with a pong. | `message` (string) |
|
|
84
|
-
|
|
85
|
-
## Configuration
|
|
86
|
-
|
|
87
|
-
### GITHUB_TOKEN setup
|
|
88
|
-
|
|
89
|
-
To use the GitHub API without severe rate limits and to access protected data (like security alerts), you must provide a GitHub Personal Access Token (PAT).
|
|
90
|
-
|
|
91
|
-
**Option A: Fine-grained Personal Access Token (Recommended)**
|
|
92
|
-
1. Go to **Settings** > **Developer settings** > **Personal access tokens** > **Fine-grained tokens**.
|
|
93
|
-
2. Click **Generate new token**.
|
|
94
|
-
3. Under **Repository access**, select the repositories you want to analyze.
|
|
95
|
-
4. Under **Repository permissions**, grant **Read-only** access to:
|
|
118
|
+
1. Navigate to **GitHub** > **Settings** > **Developer settings** > **Personal access tokens** > **Fine-grained tokens**.
|
|
119
|
+
2. Grant **Read-only** permissions to:
|
|
96
120
|
- `Code scanning alerts`
|
|
97
121
|
- `Dependabot alerts`
|
|
98
|
-
- `
|
|
122
|
+
- `Actions`
|
|
123
|
+
- `Metadata` (default)
|
|
124
|
+
3. Set your token as the `GITHUB_TOKEN` environment variable in your `.env` file or MCP client configuration.
|
|
99
125
|
|
|
100
|
-
|
|
101
|
-
Generate a classic token with at least the `repo` and `security_events` scopes.
|
|
126
|
+
---
|
|
102
127
|
|
|
103
|
-
|
|
128
|
+
## 🧪 Development & Testing
|
|
104
129
|
|
|
105
|
-
|
|
106
|
-
|
|
130
|
+
```bash
|
|
131
|
+
# Install dependencies
|
|
132
|
+
npm install
|
|
133
|
+
|
|
134
|
+
# Run comprehensive test suite (58 unit tests)
|
|
135
|
+
npm test
|
|
136
|
+
|
|
137
|
+
# Type-check TypeScript codebase
|
|
138
|
+
npm run lint
|
|
107
139
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
```env
|
|
111
|
-
GITHUB_TOKEN=ghp_your_token_here
|
|
140
|
+
# Build production bundle
|
|
141
|
+
npm run build
|
|
112
142
|
```
|
|
113
|
-
The MCP server automatically loads environment variables from `.env` files on startup.
|
|
114
143
|
|
|
115
|
-
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 📄 License
|
|
147
|
+
|
|
148
|
+
Distributed under the **MIT License**. See [`LICENSE`](./LICENSE) for more information.
|
|
116
149
|
|
|
117
|
-
|
|
150
|
+
<div align="center">
|
|
151
|
+
<sub>Built with ❤️ by <a href="https://github.com/alexbypa">Alessandro Chiodo</a></sub>
|
|
152
|
+
</div>
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { registerAnalyzeDependencies } from "./tools/analyze-dependencies.js";
|
|
|
7
7
|
import { registerAnalyzeCodeScanning } from "./tools/analyze-code-scanning.js";
|
|
8
8
|
import { registerGetHealthScore } from "./tools/get-health-score.js";
|
|
9
9
|
import { registerGetDoraMetrics } from "./tools/get-dora-metrics.js";
|
|
10
|
+
import { registerCompareRepos } from "./tools/compare-repos.js";
|
|
10
11
|
const server = new McpServer({ name: "projectpulse-mcp", version: "0.1.0" });
|
|
11
12
|
registerCheckCiStatus(server);
|
|
12
13
|
registerGetRepoHealth(server);
|
|
@@ -14,6 +15,7 @@ registerAnalyzeDependencies(server);
|
|
|
14
15
|
registerAnalyzeCodeScanning(server);
|
|
15
16
|
registerGetHealthScore(server);
|
|
16
17
|
registerGetDoraMetrics(server);
|
|
18
|
+
registerCompareRepos(server);
|
|
17
19
|
server.registerTool("ping", {
|
|
18
20
|
description: "This is a simple ping, response with pong",
|
|
19
21
|
inputSchema: z.object({
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAA;AAChF,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,EAAE,2BAA2B,EAAE,MAAM,iCAAiC,CAAC;AAC9E,OAAO,EAAE,2BAA2B,EAAE,MAAM,kCAAkC,CAAC;AAC/E,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;AAE7E,qBAAqB,CAAC,MAAM,CAAC,CAAC;AAC9B,qBAAqB,CAAC,MAAM,CAAC,CAAC;AAC9B,2BAA2B,CAAC,MAAM,CAAC,CAAC;AACpC,2BAA2B,CAAC,MAAM,CAAC,CAAC;AACpC,sBAAsB,CAAC,MAAM,CAAC,CAAC;AAC/B,sBAAsB,CAAC,MAAM,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAA;AAChF,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,EAAE,2BAA2B,EAAE,MAAM,iCAAiC,CAAC;AAC9E,OAAO,EAAE,2BAA2B,EAAE,MAAM,kCAAkC,CAAC;AAC/E,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;AAE7E,qBAAqB,CAAC,MAAM,CAAC,CAAC;AAC9B,qBAAqB,CAAC,MAAM,CAAC,CAAC;AAC9B,2BAA2B,CAAC,MAAM,CAAC,CAAC;AACpC,2BAA2B,CAAC,MAAM,CAAC,CAAC;AACpC,sBAAsB,CAAC,MAAM,CAAC,CAAC;AAC/B,sBAAsB,CAAC,MAAM,CAAC,CAAC;AAC/B,oBAAoB,CAAC,MAAM,CAAC,CAAC;AAE7B,MAAM,CAAC,YAAY,CACf,MAAM,EAAE;IACR,WAAW,EAAE,2CAA2C;IACxD,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC;QAClB,OAAO,EAAE,CAAC;aACL,MAAM,EAAE;aACR,QAAQ,CAAC,2CAA2C,CAAC;KAC7D,CAAC;CACL,EACG,KAAK,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE;IAClB,OAAO;QACH,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,OAAO,EAAE,EAAE,CAAC;KACvD,CAAC;AACN,CAAC,CACJ,CAAA;AAED,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAA;AAC5C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
export declare const inputSchema: z.ZodObject<{
|
|
4
|
+
repos: z.ZodArray<z.ZodObject<{
|
|
5
|
+
owner: z.ZodString;
|
|
6
|
+
repo: z.ZodString;
|
|
7
|
+
}, z.core.$strip>>;
|
|
8
|
+
}, z.core.$strip>;
|
|
9
|
+
export declare function executeCompareRepos({ repos }: {
|
|
10
|
+
repos: {
|
|
11
|
+
owner: string;
|
|
12
|
+
repo: string;
|
|
13
|
+
}[];
|
|
14
|
+
}): Promise<{
|
|
15
|
+
content: {
|
|
16
|
+
type: "text";
|
|
17
|
+
text: string;
|
|
18
|
+
}[];
|
|
19
|
+
}>;
|
|
20
|
+
export declare function registerCompareRepos(server: McpServer): void;
|
|
21
|
+
//# sourceMappingURL=compare-repos.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compare-repos.d.ts","sourceRoot":"","sources":["../../src/tools/compare-repos.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIpE,eAAO,MAAM,WAAW;;;;;iBAWtB,CAAC;AAEH,wBAAsB,mBAAmB,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;KAAE,EAAE,CAAA;CAAE;;;;;GAoEjG;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAS5D"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { executeGetHealthScore } from "./get-health-score.js";
|
|
3
|
+
export const inputSchema = z.object({
|
|
4
|
+
repos: z.array(z.object({
|
|
5
|
+
owner: z.string().describe("GitHub repository owner"),
|
|
6
|
+
repo: z.string().describe("GitHub repository name")
|
|
7
|
+
})).min(2, {
|
|
8
|
+
message: "At least 2 repositories are required"
|
|
9
|
+
}).max(5, {
|
|
10
|
+
message: "At most 5 repositories are allowed"
|
|
11
|
+
})
|
|
12
|
+
});
|
|
13
|
+
export async function executeCompareRepos({ repos }) {
|
|
14
|
+
const reposResults = await Promise.all(repos.map(async (repo) => {
|
|
15
|
+
try {
|
|
16
|
+
const result = await executeGetHealthScore(repo);
|
|
17
|
+
const report = JSON.parse(result.content[0].text);
|
|
18
|
+
return {
|
|
19
|
+
owner: repo.owner,
|
|
20
|
+
repo: repo.repo,
|
|
21
|
+
health: report,
|
|
22
|
+
error: null,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
return {
|
|
27
|
+
owner: repo.owner,
|
|
28
|
+
repo: repo.repo,
|
|
29
|
+
health: null,
|
|
30
|
+
error: error.message || String(error),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}));
|
|
34
|
+
const successes = reposResults
|
|
35
|
+
.filter((r) => r.health !== null)
|
|
36
|
+
.sort((a, b) => b.health.score - a.health.score);
|
|
37
|
+
const failures = reposResults
|
|
38
|
+
.filter(r => r.health === null)
|
|
39
|
+
.map(r => ({
|
|
40
|
+
owner: r.owner,
|
|
41
|
+
repo: r.repo,
|
|
42
|
+
rank: 0,
|
|
43
|
+
health: null,
|
|
44
|
+
error: r.error
|
|
45
|
+
}));
|
|
46
|
+
const rankedSuccesses = [];
|
|
47
|
+
let currentRank = 1;
|
|
48
|
+
for (let i = 0; i < successes.length; i++) {
|
|
49
|
+
const current = successes[i];
|
|
50
|
+
if (i > 0) {
|
|
51
|
+
const prev = successes[i - 1];
|
|
52
|
+
if (current.health.score < prev.health.score) {
|
|
53
|
+
currentRank = i + 1;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
rankedSuccesses.push({
|
|
57
|
+
owner: current.owner,
|
|
58
|
+
repo: current.repo,
|
|
59
|
+
rank: currentRank,
|
|
60
|
+
health: current.health,
|
|
61
|
+
error: null
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const comparedRepos = [...rankedSuccesses, ...failures];
|
|
65
|
+
const comparison = {
|
|
66
|
+
compared_at: new Date().toISOString(),
|
|
67
|
+
repos: comparedRepos
|
|
68
|
+
};
|
|
69
|
+
return {
|
|
70
|
+
content: [
|
|
71
|
+
{ type: "text", text: JSON.stringify(comparison, null, 2) }
|
|
72
|
+
]
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function registerCompareRepos(server) {
|
|
76
|
+
server.registerTool("compare_repos", {
|
|
77
|
+
description: "Compare health scores of multiple GitHub repositories, returning ranked JSON with category breakdown and ranking for each repository",
|
|
78
|
+
inputSchema,
|
|
79
|
+
}, executeCompareRepos);
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=compare-repos.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compare-repos.js","sourceRoot":"","sources":["../../src/tools/compare-repos.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compare-repos.test.d.ts","sourceRoot":"","sources":["../../src/tools/compare-repos.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { executeCompareRepos, inputSchema } from './compare-repos.js';
|
|
3
|
+
import { executeGetHealthScore } from './get-health-score.js';
|
|
4
|
+
vi.mock('./get-health-score.js');
|
|
5
|
+
describe('executeCompareRepos', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.resetAllMocks();
|
|
8
|
+
});
|
|
9
|
+
it('should retrieve scores, rank repositories correctly including ties, and handle failures gracefully', async () => {
|
|
10
|
+
// Mock executeGetHealthScore behavior:
|
|
11
|
+
// repo-a: 85 score
|
|
12
|
+
// repo-b: 85 score (tie with repo-a)
|
|
13
|
+
// repo-c: 70 score (should be rank 3 in standard competition ranking)
|
|
14
|
+
// repo-fail: fails
|
|
15
|
+
vi.mocked(executeGetHealthScore).mockImplementation(async (repo) => {
|
|
16
|
+
if (repo.repo === 'repo-a') {
|
|
17
|
+
return {
|
|
18
|
+
content: [{
|
|
19
|
+
type: 'text',
|
|
20
|
+
text: JSON.stringify({
|
|
21
|
+
repo: 'test/repo-a',
|
|
22
|
+
score: 85,
|
|
23
|
+
grade: 'B',
|
|
24
|
+
breakdown: {},
|
|
25
|
+
suggestions: [],
|
|
26
|
+
checkedAt: '2023-10-01T00:00:00Z'
|
|
27
|
+
})
|
|
28
|
+
}]
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (repo.repo === 'repo-b') {
|
|
32
|
+
return {
|
|
33
|
+
content: [{
|
|
34
|
+
type: 'text',
|
|
35
|
+
text: JSON.stringify({
|
|
36
|
+
repo: 'test/repo-b',
|
|
37
|
+
score: 85,
|
|
38
|
+
grade: 'B',
|
|
39
|
+
breakdown: {},
|
|
40
|
+
suggestions: [],
|
|
41
|
+
checkedAt: '2023-10-01T00:00:00Z'
|
|
42
|
+
})
|
|
43
|
+
}]
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (repo.repo === 'repo-c') {
|
|
47
|
+
return {
|
|
48
|
+
content: [{
|
|
49
|
+
type: 'text',
|
|
50
|
+
text: JSON.stringify({
|
|
51
|
+
repo: 'test/repo-c',
|
|
52
|
+
score: 70,
|
|
53
|
+
grade: 'C',
|
|
54
|
+
breakdown: {},
|
|
55
|
+
suggestions: [],
|
|
56
|
+
checkedAt: '2023-10-01T00:00:00Z'
|
|
57
|
+
})
|
|
58
|
+
}]
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
throw new Error('API Rate Limit or Repository not found');
|
|
62
|
+
});
|
|
63
|
+
const repos = [
|
|
64
|
+
{ owner: 'test', repo: 'repo-a' },
|
|
65
|
+
{ owner: 'test', repo: 'repo-fail' },
|
|
66
|
+
{ owner: 'test', repo: 'repo-b' },
|
|
67
|
+
{ owner: 'test', repo: 'repo-c' }
|
|
68
|
+
];
|
|
69
|
+
const result = await executeCompareRepos({ repos });
|
|
70
|
+
expect(result.content).toHaveLength(1);
|
|
71
|
+
expect(result.content[0].type).toBe('text');
|
|
72
|
+
const comparison = JSON.parse(result.content[0].text);
|
|
73
|
+
expect(comparison.compared_at).toBeDefined();
|
|
74
|
+
expect(comparison.repos).toHaveLength(4);
|
|
75
|
+
// Verify sorting order: success ordered by score desc, then failures
|
|
76
|
+
expect(comparison.repos[0].repo).toMatch(/repo-a|repo-b/);
|
|
77
|
+
expect(comparison.repos[1].repo).toMatch(/repo-a|repo-b/);
|
|
78
|
+
expect(comparison.repos[2].repo).toBe('repo-c');
|
|
79
|
+
expect(comparison.repos[3].repo).toBe('repo-fail');
|
|
80
|
+
const repoA = comparison.repos.find((r) => r.repo === 'repo-a');
|
|
81
|
+
const repoB = comparison.repos.find((r) => r.repo === 'repo-b');
|
|
82
|
+
const repoC = comparison.repos.find((r) => r.repo === 'repo-c');
|
|
83
|
+
const repoFail = comparison.repos.find((r) => r.repo === 'repo-fail');
|
|
84
|
+
expect(repoA.rank).toBe(1);
|
|
85
|
+
expect(repoA.health.score).toBe(85);
|
|
86
|
+
expect(repoA.error).toBeNull();
|
|
87
|
+
expect(repoB.rank).toBe(1);
|
|
88
|
+
expect(repoB.health.score).toBe(85);
|
|
89
|
+
expect(repoB.error).toBeNull();
|
|
90
|
+
// repo-c should be rank 3 (since 2 repos share rank 1)
|
|
91
|
+
expect(repoC.rank).toBe(3);
|
|
92
|
+
expect(repoC.health.score).toBe(70);
|
|
93
|
+
expect(repoC.error).toBeNull();
|
|
94
|
+
// repo-fail should be rank 0 with error details
|
|
95
|
+
expect(repoFail.rank).toBe(0);
|
|
96
|
+
expect(repoFail.health).toBeNull();
|
|
97
|
+
expect(repoFail.error).toContain('API Rate Limit');
|
|
98
|
+
});
|
|
99
|
+
describe('inputSchema validation', () => {
|
|
100
|
+
it('should validate correctly with 2 to 5 repos', () => {
|
|
101
|
+
const valid = inputSchema.safeParse({
|
|
102
|
+
repos: [
|
|
103
|
+
{ owner: 'owner1', repo: 'repo1' },
|
|
104
|
+
{ owner: 'owner2', repo: 'repo2' }
|
|
105
|
+
]
|
|
106
|
+
});
|
|
107
|
+
expect(valid.success).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
it('should fail validation with less than 2 repos', () => {
|
|
110
|
+
const invalid = inputSchema.safeParse({
|
|
111
|
+
repos: [
|
|
112
|
+
{ owner: 'owner1', repo: 'repo1' }
|
|
113
|
+
]
|
|
114
|
+
});
|
|
115
|
+
expect(invalid.success).toBe(false);
|
|
116
|
+
if (!invalid.success) {
|
|
117
|
+
expect(invalid.error.issues[0].message).toBe('At least 2 repositories are required');
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
it('should fail validation with more than 5 repos', () => {
|
|
121
|
+
const invalid = inputSchema.safeParse({
|
|
122
|
+
repos: [
|
|
123
|
+
{ owner: 'owner1', repo: 'repo1' },
|
|
124
|
+
{ owner: 'owner2', repo: 'repo2' },
|
|
125
|
+
{ owner: 'owner3', repo: 'repo3' },
|
|
126
|
+
{ owner: 'owner4', repo: 'repo4' },
|
|
127
|
+
{ owner: 'owner5', repo: 'repo5' },
|
|
128
|
+
{ owner: 'owner6', repo: 'repo6' }
|
|
129
|
+
]
|
|
130
|
+
});
|
|
131
|
+
expect(invalid.success).toBe(false);
|
|
132
|
+
if (!invalid.success) {
|
|
133
|
+
expect(invalid.error.issues[0].message).toBe('At most 5 repositories are allowed');
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
//# sourceMappingURL=compare-repos.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compare-repos.test.js","sourceRoot":"","sources":["../../src/tools/compare-repos.test.ts"],"names":[],"mappings":"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"}
|