projectpulse-mcp 1.0.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/LICENSE +21 -0
- package/README.md +73 -0
- package/bin/projectpulse-mcp.js +2 -0
- package/dist/github/client.d.ts +3 -0
- package/dist/github/client.d.ts.map +1 -0
- package/dist/github/client.js +10 -0
- package/dist/github/client.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/scoring/health-score.d.ts +16 -0
- package/dist/scoring/health-score.d.ts.map +1 -0
- package/dist/scoring/health-score.js +80 -0
- package/dist/scoring/health-score.js.map +1 -0
- package/dist/scoring/health-score.test.d.ts +2 -0
- package/dist/scoring/health-score.test.d.ts.map +1 -0
- package/dist/scoring/health-score.test.js +209 -0
- package/dist/scoring/health-score.test.js.map +1 -0
- package/dist/tools/analyze-code-scanning.d.ts +12 -0
- package/dist/tools/analyze-code-scanning.d.ts.map +1 -0
- package/dist/tools/analyze-code-scanning.js +44 -0
- package/dist/tools/analyze-code-scanning.js.map +1 -0
- package/dist/tools/analyze-code-scanning.test.d.ts +2 -0
- package/dist/tools/analyze-code-scanning.test.d.ts.map +1 -0
- package/dist/tools/analyze-code-scanning.test.js +62 -0
- package/dist/tools/analyze-code-scanning.test.js.map +1 -0
- package/dist/tools/analyze-dependencies.d.ts +13 -0
- package/dist/tools/analyze-dependencies.d.ts.map +1 -0
- package/dist/tools/analyze-dependencies.js +31 -0
- package/dist/tools/analyze-dependencies.js.map +1 -0
- package/dist/tools/analyze-dependencies.test.d.ts +2 -0
- package/dist/tools/analyze-dependencies.test.d.ts.map +1 -0
- package/dist/tools/analyze-dependencies.test.js +65 -0
- package/dist/tools/analyze-dependencies.test.js.map +1 -0
- package/dist/tools/check-ci-status.d.ts +13 -0
- package/dist/tools/check-ci-status.d.ts.map +1 -0
- package/dist/tools/check-ci-status.js +32 -0
- package/dist/tools/check-ci-status.js.map +1 -0
- package/dist/tools/check-ci-status.test.d.ts +2 -0
- package/dist/tools/check-ci-status.test.d.ts.map +1 -0
- package/dist/tools/check-ci-status.test.js +90 -0
- package/dist/tools/check-ci-status.test.js.map +1 -0
- package/dist/tools/get-health-score.d.ts +12 -0
- package/dist/tools/get-health-score.d.ts.map +1 -0
- package/dist/tools/get-health-score.js +47 -0
- package/dist/tools/get-health-score.js.map +1 -0
- package/dist/tools/get-health-score.test.d.ts +2 -0
- package/dist/tools/get-health-score.test.d.ts.map +1 -0
- package/dist/tools/get-health-score.test.js +93 -0
- package/dist/tools/get-health-score.test.js.map +1 -0
- package/dist/tools/get-repo-health.d.ts +12 -0
- package/dist/tools/get-repo-health.d.ts.map +1 -0
- package/dist/tools/get-repo-health.js +49 -0
- package/dist/tools/get-repo-health.js.map +1 -0
- package/dist/tools/get-repo-health.test.d.ts +2 -0
- package/dist/tools/get-repo-health.test.d.ts.map +1 -0
- package/dist/tools/get-repo-health.test.js +80 -0
- package/dist/tools/get-repo-health.test.js.map +1 -0
- package/dist/types/health.d.ts +16 -0
- package/dist/types/health.d.ts.map +1 -0
- package/dist/types/health.js +2 -0
- package/dist/types/health.js.map +1 -0
- package/dist/vitest.config.d.ts +3 -0
- package/dist/vitest.config.d.ts.map +1 -0
- package/dist/vitest.config.js +7 -0
- package/dist/vitest.config.js.map +1 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alessandro Chiodo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# projectpulse-mcp
|
|
2
|
+
|
|
3
|
+
GitHub repository health monitoring via MCP.
|
|
4
|
+
|
|
5
|
+
This Model Context Protocol (MCP) server allows AI assistants to analyze the health, security, and maintenance status of GitHub repositories directly from your conversations.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Repository Health Score**: Calculates a comprehensive 0-100 score based on CI status, freshness, security, and community metrics.
|
|
10
|
+
- **Basic Repository Info**: Fetches general metadata like stars, forks, language, and license.
|
|
11
|
+
- **Dependency Analysis**: Lists Dependabot alerts with severity filtering.
|
|
12
|
+
- **CI/CD Status**: Checks recent GitHub Actions workflow runs and their conclusions.
|
|
13
|
+
- **Code Scanning**: Retrieves CodeQL and other code scanning alerts.
|
|
14
|
+
- **Ping**: Simple connectivity test tool.
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
### Installation
|
|
19
|
+
|
|
20
|
+
You can run `projectpulse-mcp` directly using `npx`:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx projectpulse-mcp
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Claude Desktop Configuration
|
|
27
|
+
|
|
28
|
+
To use this with Claude Desktop, add the following configuration to your `claude_desktop_config.json` file (usually located at `%APPDATA%\Claude\claude_desktop_config.json` on Windows or `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"mcpServers": {
|
|
33
|
+
"projectpulse": {
|
|
34
|
+
"command": "npx",
|
|
35
|
+
"args": [
|
|
36
|
+
"-y",
|
|
37
|
+
"projectpulse-mcp"
|
|
38
|
+
],
|
|
39
|
+
"env": {
|
|
40
|
+
"GITHUB_TOKEN": "your-github-personal-access-token"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Tools
|
|
48
|
+
|
|
49
|
+
| Tool Name | Description | Input Schema |
|
|
50
|
+
| --- | --- | --- |
|
|
51
|
+
| `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) |
|
|
52
|
+
| `get_repo_health` | Get health information for a GitHub repository (stars, issues, language, license, etc.). | `owner` (string), `repo` (string) |
|
|
53
|
+
| `analyze_dependencies` | Get Dependabot alerts for a GitHub repository. | `owner` (string), `repo` (string), `severity` (optional enum: critical, high, medium, low) |
|
|
54
|
+
| `check_ci_status` | Get recent CI/CD workflow runs for a GitHub repository. | `owner` (string), `repo` (string), `limit` (optional number, default: 10) |
|
|
55
|
+
| `analyze_code_scanning` | Get Code Scanning alerts for a GitHub repository with CodeQL. | `owner` (string), `repo` (string) |
|
|
56
|
+
| `ping` | Simple connectivity check that responds with a pong. | `message` (string) |
|
|
57
|
+
|
|
58
|
+
## Configuration
|
|
59
|
+
|
|
60
|
+
### GITHUB_TOKEN setup
|
|
61
|
+
|
|
62
|
+
To use the GitHub API without severe rate limits and to access private repositories or security alerts, you must provide a GitHub Personal Access Token (PAT).
|
|
63
|
+
|
|
64
|
+
1. Go to your GitHub account settings: **Settings** > **Developer settings** > **Personal access tokens** > **Tokens (classic)**.
|
|
65
|
+
2. Click **Generate new token (classic)**.
|
|
66
|
+
3. Grant it at least the following scopes:
|
|
67
|
+
- `repo` (Full control of private repositories)
|
|
68
|
+
- `security_events` (Read and write security events)
|
|
69
|
+
4. Copy the token and set it as an environment variable named `GITHUB_TOKEN` in your MCP client configuration (e.g., Claude Desktop config).
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
MIT License. See the `LICENSE` file for details.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/github/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAIxC,wBAAgB,UAAU,IAAI,OAAO,CAMpC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Octokit } from "@octokit/rest";
|
|
2
|
+
let octokitInstance = null;
|
|
3
|
+
export function getOctokit() {
|
|
4
|
+
if (!octokitInstance) {
|
|
5
|
+
const token = process.env.GITHUB_TOKEN;
|
|
6
|
+
octokitInstance = new Octokit({ auth: token });
|
|
7
|
+
}
|
|
8
|
+
return octokitInstance;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/github/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAExC,IAAI,eAAe,GAAmB,IAAI,CAAC;AAE3C,MAAM,UAAU,UAAU;IACtB,IAAI,CAAC,eAAe,EAAE,CAAC;QACnB,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC;QACvC,eAAe,GAAG,IAAI,OAAO,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACnD,CAAC;IACD,OAAO,eAAe,CAAC;AAC3B,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { registerCheckCiStatus } from "./tools/check-ci-status.js";
|
|
5
|
+
import { registerGetRepoHealth } from "./tools/get-repo-health.js";
|
|
6
|
+
import { registerAnalyzeDependencies } from "./tools/analyze-dependencies.js";
|
|
7
|
+
import { registerAnalyzeCodeScanning } from "./tools/analyze-code-scanning.js";
|
|
8
|
+
import { registerGetHealthScore } from "./tools/get-health-score.js";
|
|
9
|
+
const server = new McpServer({ name: "projectpulse-mcp", version: "0.1.0" });
|
|
10
|
+
registerCheckCiStatus(server);
|
|
11
|
+
registerGetRepoHealth(server);
|
|
12
|
+
registerAnalyzeDependencies(server);
|
|
13
|
+
registerAnalyzeCodeScanning(server);
|
|
14
|
+
registerGetHealthScore(server);
|
|
15
|
+
server.registerTool("ping", {
|
|
16
|
+
description: "This is a simple ping, response with pong",
|
|
17
|
+
inputSchema: z.object({
|
|
18
|
+
message: z
|
|
19
|
+
.string()
|
|
20
|
+
.describe("This is a simple ping, response with pong")
|
|
21
|
+
}),
|
|
22
|
+
}, async ({ message }) => {
|
|
23
|
+
return {
|
|
24
|
+
content: [{ type: "text", text: `pong ${message}` }]
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
const transport = new StdioServerTransport();
|
|
28
|
+
await server.connect(transport);
|
|
29
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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;AAErE,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;AAE/B,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,16 @@
|
|
|
1
|
+
import { Grade, CategoryScore, HealthReport } from "../types/health.js";
|
|
2
|
+
export declare function scoreToGrade(score: number): Grade;
|
|
3
|
+
export declare function calculateCiScore(runs: Array<{
|
|
4
|
+
conclusion: string | null;
|
|
5
|
+
}>): CategoryScore;
|
|
6
|
+
export declare function calculateFreshnessScore(pushedAt: string): CategoryScore;
|
|
7
|
+
export declare function calculateSecurityScore(alertCounts: {
|
|
8
|
+
critical: number;
|
|
9
|
+
high: number;
|
|
10
|
+
medium: number;
|
|
11
|
+
low: number;
|
|
12
|
+
}): CategoryScore;
|
|
13
|
+
export declare function calculateCommunityScore(openIssues: number, forks: number): CategoryScore;
|
|
14
|
+
export declare function calculateMaintenanceScore(hasLicense: boolean, hasDescription: boolean, archived: boolean): CategoryScore;
|
|
15
|
+
export declare function calculateHealthScore(repo: string, categories: Record<string, CategoryScore>): HealthReport;
|
|
16
|
+
//# sourceMappingURL=health-score.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health-score.d.ts","sourceRoot":"","sources":["../../src/scoring/health-score.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAUxE,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,CAMjD;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,KAAK,CAAC;IAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,GAAG,aAAa,CAK1F;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,CAQvE;AAED,wBAAgB,sBAAsB,CAAC,WAAW,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,aAAa,CASlI;AAED,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,aAAa,CAQxF;AAED,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,GAAG,aAAa,CAMxH;AAED,wBAAgB,oBAAoB,CAChC,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,GAC1C,YAAY,CAQd"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const WEIGHTS = {
|
|
2
|
+
ci: 0.25,
|
|
3
|
+
freshness: 0.20,
|
|
4
|
+
community: 0.15,
|
|
5
|
+
security: 0.25,
|
|
6
|
+
maintenance: 0.15,
|
|
7
|
+
};
|
|
8
|
+
export function scoreToGrade(score) {
|
|
9
|
+
if (score >= 80)
|
|
10
|
+
return 'A';
|
|
11
|
+
if (score >= 60)
|
|
12
|
+
return 'B';
|
|
13
|
+
if (score >= 40)
|
|
14
|
+
return 'C';
|
|
15
|
+
if (score >= 20)
|
|
16
|
+
return 'D';
|
|
17
|
+
return 'F';
|
|
18
|
+
}
|
|
19
|
+
export function calculateCiScore(runs) {
|
|
20
|
+
const passed = runs.filter(r => r.conclusion === "success").length;
|
|
21
|
+
const total = runs.length;
|
|
22
|
+
const score = total === 0 ? 0 : Math.round((passed / total) * 100);
|
|
23
|
+
return { score, weight: WEIGHTS.ci, detail: `${passed}/${total} runs passed` };
|
|
24
|
+
}
|
|
25
|
+
export function calculateFreshnessScore(pushedAt) {
|
|
26
|
+
const daysAgo = (Date.now() - new Date(pushedAt).getTime()) / (1000 * 60 * 60 * 24);
|
|
27
|
+
let score;
|
|
28
|
+
if (daysAgo < 7)
|
|
29
|
+
score = 100;
|
|
30
|
+
else if (daysAgo < 30)
|
|
31
|
+
score = 70;
|
|
32
|
+
else if (daysAgo < 90)
|
|
33
|
+
score = 40;
|
|
34
|
+
else
|
|
35
|
+
score = 10;
|
|
36
|
+
return { score, weight: WEIGHTS.freshness, detail: `Last push ${Math.round(daysAgo)} days ago` };
|
|
37
|
+
}
|
|
38
|
+
export function calculateSecurityScore(alertCounts) {
|
|
39
|
+
const { critical, high, medium, low } = alertCounts;
|
|
40
|
+
let score = 100;
|
|
41
|
+
score -= critical * 25;
|
|
42
|
+
score -= high * 15;
|
|
43
|
+
score -= medium * 5;
|
|
44
|
+
score -= low * 2;
|
|
45
|
+
score = Math.max(0, score);
|
|
46
|
+
return { score, weight: WEIGHTS.security, detail: `Alerts: ${critical} critical, ${high} high, ${medium} medium, ${low} low` };
|
|
47
|
+
}
|
|
48
|
+
export function calculateCommunityScore(openIssues, forks) {
|
|
49
|
+
let score = 50;
|
|
50
|
+
if (forks > 10)
|
|
51
|
+
score += 25;
|
|
52
|
+
else if (forks > 0)
|
|
53
|
+
score += 10;
|
|
54
|
+
if (openIssues > 50)
|
|
55
|
+
score -= 25;
|
|
56
|
+
else if (openIssues > 20)
|
|
57
|
+
score -= 10;
|
|
58
|
+
score = Math.max(0, Math.min(100, score));
|
|
59
|
+
return { score, weight: WEIGHTS.community, detail: `${openIssues} open issues, ${forks} forks` };
|
|
60
|
+
}
|
|
61
|
+
export function calculateMaintenanceScore(hasLicense, hasDescription, archived) {
|
|
62
|
+
let score = 0;
|
|
63
|
+
if (hasLicense)
|
|
64
|
+
score += 35;
|
|
65
|
+
if (hasDescription)
|
|
66
|
+
score += 35;
|
|
67
|
+
if (!archived)
|
|
68
|
+
score += 30;
|
|
69
|
+
return { score, weight: WEIGHTS.maintenance, detail: `License: ${hasLicense}, Description: ${hasDescription}, Active: ${!archived}` };
|
|
70
|
+
}
|
|
71
|
+
export function calculateHealthScore(repo, categories) {
|
|
72
|
+
const values = Object.values(categories);
|
|
73
|
+
const score = Math.round(values.reduce((sum, cat) => sum + cat.score * cat.weight, 0));
|
|
74
|
+
const grade = scoreToGrade(score);
|
|
75
|
+
const suggestions = values
|
|
76
|
+
.filter(cat => cat.score < 50)
|
|
77
|
+
.map(cat => `Improve ${cat.detail} (score: ${cat.score}/100)`);
|
|
78
|
+
return { repo, score, grade, breakdown: categories, suggestions, checkedAt: new Date().toISOString() };
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=health-score.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health-score.js","sourceRoot":"","sources":["../../src/scoring/health-score.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health-score.test.d.ts","sourceRoot":"","sources":["../../src/scoring/health-score.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { scoreToGrade, calculateCiScore, calculateSecurityScore, calculateFreshnessScore, calculateCommunityScore, calculateMaintenanceScore, calculateHealthScore } from './health-score.js';
|
|
3
|
+
describe('scoreToGrade', () => {
|
|
4
|
+
// qui dentro scrivi i test case
|
|
5
|
+
// ogni it() testa un caso specifico
|
|
6
|
+
it('should return A for score 85', () => {
|
|
7
|
+
expect(scoreToGrade(85)).toBe('A');
|
|
8
|
+
});
|
|
9
|
+
it('should return B for score 65', () => {
|
|
10
|
+
expect(scoreToGrade(65)).toBe('B');
|
|
11
|
+
});
|
|
12
|
+
it('should return C for score 45', () => {
|
|
13
|
+
expect(scoreToGrade(45)).toBe('C');
|
|
14
|
+
});
|
|
15
|
+
it('should return D for score 25', () => {
|
|
16
|
+
expect(scoreToGrade(25)).toBe('D');
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
describe('calculateCiScore', () => {
|
|
20
|
+
it('should return 100 for all successful runs', () => {
|
|
21
|
+
expect(calculateCiScore([{
|
|
22
|
+
conclusion: 'success'
|
|
23
|
+
}])).toEqual({
|
|
24
|
+
score: 100,
|
|
25
|
+
weight: 0.25,
|
|
26
|
+
detail: '1/1 runs passed'
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
it('should return 50 for 1 failed run', () => {
|
|
30
|
+
expect(calculateCiScore([{
|
|
31
|
+
conclusion: 'failure',
|
|
32
|
+
}, {
|
|
33
|
+
conclusion: 'success'
|
|
34
|
+
}])).toEqual({
|
|
35
|
+
score: 50,
|
|
36
|
+
weight: 0.25,
|
|
37
|
+
detail: '1/2 runs passed'
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
it('should return 0 for no runs', () => {
|
|
41
|
+
expect(calculateCiScore([])).toEqual({
|
|
42
|
+
score: 0,
|
|
43
|
+
weight: 0.25,
|
|
44
|
+
detail: '0/0 runs passed'
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe('calculateSecurityScore', () => {
|
|
49
|
+
it('should return 100 with no alerts', () => {
|
|
50
|
+
expect(calculateSecurityScore({
|
|
51
|
+
critical: 0,
|
|
52
|
+
high: 0,
|
|
53
|
+
medium: 0,
|
|
54
|
+
low: 0
|
|
55
|
+
})).toEqual({
|
|
56
|
+
score: 100,
|
|
57
|
+
weight: 0.25,
|
|
58
|
+
detail: 'Alerts: 0 critical, 0 high, 0 medium, 0 low'
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
it('should return 50 for 2 critical alerts', () => {
|
|
62
|
+
expect(calculateSecurityScore({
|
|
63
|
+
critical: 2, // -50
|
|
64
|
+
high: 0,
|
|
65
|
+
medium: 0,
|
|
66
|
+
low: 0
|
|
67
|
+
})).toEqual({
|
|
68
|
+
score: 50,
|
|
69
|
+
weight: 0.25,
|
|
70
|
+
detail: 'Alerts: 2 critical, 0 high, 0 medium, 0 low'
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
it('should calculate 44 for a mix of severities', () => {
|
|
74
|
+
expect(calculateSecurityScore({
|
|
75
|
+
critical: 1, // -25
|
|
76
|
+
high: 1, // -15
|
|
77
|
+
medium: 2, // -10
|
|
78
|
+
low: 3 // -6
|
|
79
|
+
})).toEqual({
|
|
80
|
+
score: 44, // 100 - 25 - 15 - 10 - 6 = 44
|
|
81
|
+
weight: 0.25,
|
|
82
|
+
detail: 'Alerts: 1 critical, 1 high, 2 medium, 3 low'
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
it('should bound score to 0 (overflow)', () => {
|
|
86
|
+
expect(calculateSecurityScore({
|
|
87
|
+
critical: 5, // -125
|
|
88
|
+
high: 0,
|
|
89
|
+
medium: 0,
|
|
90
|
+
low: 0
|
|
91
|
+
})).toEqual({
|
|
92
|
+
score: 0,
|
|
93
|
+
weight: 0.25,
|
|
94
|
+
detail: 'Alerts: 5 critical, 0 high, 0 medium, 0 low'
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe('calculateFreshnessScore', () => {
|
|
99
|
+
it('should return 100 for push within 7 days', () => {
|
|
100
|
+
const date = new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString();
|
|
101
|
+
expect(calculateFreshnessScore(date)).toEqual({
|
|
102
|
+
score: 100,
|
|
103
|
+
weight: 0.20,
|
|
104
|
+
detail: 'Last push 5 days ago'
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
it('should return 70 for push within 30 days', () => {
|
|
108
|
+
const date = new Date(Date.now() - 15 * 24 * 60 * 60 * 1000).toISOString();
|
|
109
|
+
expect(calculateFreshnessScore(date)).toEqual({
|
|
110
|
+
score: 70,
|
|
111
|
+
weight: 0.20,
|
|
112
|
+
detail: 'Last push 15 days ago'
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
it('should return 40 for push within 90 days', () => {
|
|
116
|
+
const date = new Date(Date.now() - 60 * 24 * 60 * 60 * 1000).toISOString();
|
|
117
|
+
expect(calculateFreshnessScore(date)).toEqual({
|
|
118
|
+
score: 40,
|
|
119
|
+
weight: 0.20,
|
|
120
|
+
detail: 'Last push 60 days ago'
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
it('should return 10 for push older than 90 days', () => {
|
|
124
|
+
const date = new Date(Date.now() - 100 * 24 * 60 * 60 * 1000).toISOString();
|
|
125
|
+
expect(calculateFreshnessScore(date)).toEqual({
|
|
126
|
+
score: 10,
|
|
127
|
+
weight: 0.20,
|
|
128
|
+
detail: 'Last push 100 days ago'
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
describe('calculateCommunityScore', () => {
|
|
133
|
+
it('should return 50 for no forks and no issues', () => {
|
|
134
|
+
expect(calculateCommunityScore(0, 0)).toEqual({
|
|
135
|
+
score: 50,
|
|
136
|
+
weight: 0.15,
|
|
137
|
+
detail: '0 open issues, 0 forks'
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
it('should increase score for forks', () => {
|
|
141
|
+
expect(calculateCommunityScore(0, 5)).toEqual({
|
|
142
|
+
score: 60, // 50 + 10
|
|
143
|
+
weight: 0.15,
|
|
144
|
+
detail: '0 open issues, 5 forks'
|
|
145
|
+
});
|
|
146
|
+
expect(calculateCommunityScore(0, 15)).toEqual({
|
|
147
|
+
score: 75, // 50 + 25
|
|
148
|
+
weight: 0.15,
|
|
149
|
+
detail: '0 open issues, 15 forks'
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
it('should decrease score for many open issues', () => {
|
|
153
|
+
expect(calculateCommunityScore(25, 0)).toEqual({
|
|
154
|
+
score: 40, // 50 - 10
|
|
155
|
+
weight: 0.15,
|
|
156
|
+
detail: '25 open issues, 0 forks'
|
|
157
|
+
});
|
|
158
|
+
expect(calculateCommunityScore(55, 0)).toEqual({
|
|
159
|
+
score: 25, // 50 - 25
|
|
160
|
+
weight: 0.15,
|
|
161
|
+
detail: '55 open issues, 0 forks'
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
describe('calculateMaintenanceScore', () => {
|
|
166
|
+
it('should return 100 for perfect maintenance', () => {
|
|
167
|
+
expect(calculateMaintenanceScore(true, true, false)).toEqual({
|
|
168
|
+
score: 100,
|
|
169
|
+
weight: 0.15,
|
|
170
|
+
detail: 'License: true, Description: true, Active: true'
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
it('should return 0 for poor maintenance', () => {
|
|
174
|
+
expect(calculateMaintenanceScore(false, false, true)).toEqual({
|
|
175
|
+
score: 0,
|
|
176
|
+
weight: 0.15,
|
|
177
|
+
detail: 'License: false, Description: false, Active: false'
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
describe('calculateHealthScore', () => {
|
|
182
|
+
it('should calculate final health report and grade', () => {
|
|
183
|
+
const report = calculateHealthScore('test/repo', {
|
|
184
|
+
ci: { score: 100, weight: 0.25, detail: '1/1 runs passed' },
|
|
185
|
+
freshness: { score: 70, weight: 0.20, detail: 'Last push 15 days ago' },
|
|
186
|
+
community: { score: 75, weight: 0.15, detail: '10 open issues, 15 forks' },
|
|
187
|
+
security: { score: 100, weight: 0.25, detail: 'Alerts: 0 critical' },
|
|
188
|
+
maintenance: { score: 100, weight: 0.15, detail: 'Active' },
|
|
189
|
+
});
|
|
190
|
+
expect(report).toEqual({
|
|
191
|
+
repo: 'test/repo',
|
|
192
|
+
score: 90,
|
|
193
|
+
grade: 'A',
|
|
194
|
+
breakdown: expect.any(Object),
|
|
195
|
+
suggestions: [],
|
|
196
|
+
checkedAt: expect.any(String)
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
it('should generate suggestions for categories with score < 50', () => {
|
|
200
|
+
const report = calculateHealthScore('test/repo', {
|
|
201
|
+
ci: { score: 0, weight: 0.25, detail: '0/1 runs passed' },
|
|
202
|
+
freshness: { score: 100, weight: 0.20, detail: 'Last push 1 days ago' },
|
|
203
|
+
});
|
|
204
|
+
expect(report.suggestions).toEqual([
|
|
205
|
+
'Improve 0/1 runs passed (score: 0/100)'
|
|
206
|
+
]);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
//# sourceMappingURL=health-score.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health-score.test.js","sourceRoot":"","sources":["../../src/scoring/health-score.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
export declare function executeAnalyzeCodeScanning({ owner, repo }: {
|
|
3
|
+
owner: string;
|
|
4
|
+
repo: string;
|
|
5
|
+
}): Promise<{
|
|
6
|
+
content: {
|
|
7
|
+
type: "text";
|
|
8
|
+
text: string;
|
|
9
|
+
}[];
|
|
10
|
+
}>;
|
|
11
|
+
export declare function registerAnalyzeCodeScanning(server: McpServer): void;
|
|
12
|
+
//# sourceMappingURL=analyze-code-scanning.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyze-code-scanning.d.ts","sourceRoot":"","sources":["../../src/tools/analyze-code-scanning.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAQpE,wBAAsB,0BAA0B,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;;;;;GA8BhG;AAED,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CASnE"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getOctokit } from "../github/client.js";
|
|
3
|
+
const inputSchema = z.object({
|
|
4
|
+
owner: z.string().describe("GitHub repository owner"),
|
|
5
|
+
repo: z.string().describe("GitHub repository name")
|
|
6
|
+
});
|
|
7
|
+
export async function executeAnalyzeCodeScanning({ owner, repo }) {
|
|
8
|
+
try {
|
|
9
|
+
const { data } = await getOctokit().codeScanning.listAlertsForRepo({ owner, repo });
|
|
10
|
+
const result = data.map((alert) => ({
|
|
11
|
+
rule_id: alert.rule.id,
|
|
12
|
+
severity: alert.rule.severity,
|
|
13
|
+
rule_description: alert.rule.description,
|
|
14
|
+
state: alert.state,
|
|
15
|
+
most_recent_instance_path: alert.most_recent_instance.location?.path,
|
|
16
|
+
most_recent_instance_start_line: alert.most_recent_instance.location?.start_line,
|
|
17
|
+
html_url: alert.html_url
|
|
18
|
+
}));
|
|
19
|
+
return {
|
|
20
|
+
content: [{
|
|
21
|
+
type: "text",
|
|
22
|
+
text: JSON.stringify(result, null, 2)
|
|
23
|
+
}]
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error.status === 404) {
|
|
28
|
+
return {
|
|
29
|
+
content: [{
|
|
30
|
+
type: "text",
|
|
31
|
+
text: "Code scanning not configured on this repo"
|
|
32
|
+
}]
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function registerAnalyzeCodeScanning(server) {
|
|
39
|
+
server.registerTool("analyze_code_scanning", {
|
|
40
|
+
description: "Get Code Scanning alerts for a GitHub repository with CodeQL",
|
|
41
|
+
inputSchema
|
|
42
|
+
}, executeAnalyzeCodeScanning);
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=analyze-code-scanning.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyze-code-scanning.js","sourceRoot":"","sources":["../../src/tools/analyze-code-scanning.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEjD,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC;IACzB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,yBAAyB,CAAC;IACrD,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,wBAAwB,CAAC;CACtD,CAAC,CAAC;AAEH,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAAC,EAAE,KAAK,EAAE,IAAI,EAAmC;IAC7F,IAAI,CAAC;QACD,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,UAAU,EAAE,CAAC,YAAY,CAAC,iBAAiB,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACpF,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YAChC,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE;YACtB,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,QAAQ;YAC7B,gBAAgB,EAAE,KAAK,CAAC,IAAI,CAAC,WAAW;YACxC,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,yBAAyB,EAAE,KAAK,CAAC,oBAAoB,CAAC,QAAQ,EAAE,IAAI;YACpE,+BAA+B,EAAE,KAAK,CAAC,oBAAoB,CAAC,QAAQ,EAAE,UAAU;YAChF,QAAQ,EAAE,KAAK,CAAC,QAAQ;SAC3B,CAAC,CAAC,CAAC;QACJ,OAAO;YACH,OAAO,EAAE,CAAC;oBACN,IAAI,EAAE,MAAe;oBACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;iBACxC,CAAC;SACL,CAAC;IAEN,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACb,IAAK,KAA6B,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAChD,OAAO;gBACH,OAAO,EAAE,CAAC;wBACN,IAAI,EAAE,MAAe;wBACrB,IAAI,EAAE,2CAA2C;qBACpD,CAAC;aACL,CAAC;QACN,CAAC;QACD,MAAM,KAAK,CAAC;IAChB,CAAC;AACL,CAAC;AAED,MAAM,UAAU,2BAA2B,CAAC,MAAiB;IACzD,MAAM,CAAC,YAAY,CACf,uBAAuB,EACvB;QACI,WAAW,EAAE,8DAA8D;QAC3E,WAAW;KACd,EACD,0BAA0B,CAC7B,CAAC;AACN,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyze-code-scanning.test.d.ts","sourceRoot":"","sources":["../../src/tools/analyze-code-scanning.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { getOctokit } from '../github/client.js';
|
|
3
|
+
import { executeAnalyzeCodeScanning } from './analyze-code-scanning.js';
|
|
4
|
+
vi.mock('../github/client.js');
|
|
5
|
+
describe('executeAnalyzeCodeScanning', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.resetAllMocks();
|
|
8
|
+
});
|
|
9
|
+
it('should return code scanning alerts', async () => {
|
|
10
|
+
const mockAlerts = [
|
|
11
|
+
{
|
|
12
|
+
rule: { id: 'js/sql-injection', severity: 'error', description: 'SQL Injection' },
|
|
13
|
+
state: 'open',
|
|
14
|
+
most_recent_instance: { location: { path: 'src/db.ts', start_line: 10 } },
|
|
15
|
+
html_url: 'https://github.com/test/repo/security/code-scanning/1'
|
|
16
|
+
}
|
|
17
|
+
];
|
|
18
|
+
const mockListAlerts = vi.fn().mockResolvedValue({ data: mockAlerts });
|
|
19
|
+
vi.mocked(getOctokit).mockReturnValue({
|
|
20
|
+
codeScanning: { listAlertsForRepo: mockListAlerts }
|
|
21
|
+
});
|
|
22
|
+
const result = await executeAnalyzeCodeScanning({ owner: 'test', repo: 'repo' });
|
|
23
|
+
expect(mockListAlerts).toHaveBeenCalledWith({ owner: 'test', repo: 'repo' });
|
|
24
|
+
expect(result.content).toHaveLength(1);
|
|
25
|
+
expect(result.content[0].type).toBe('text');
|
|
26
|
+
const parsedText = JSON.parse(result.content[0].text);
|
|
27
|
+
expect(parsedText).toHaveLength(1);
|
|
28
|
+
expect(parsedText[0]).toEqual({
|
|
29
|
+
rule_id: 'js/sql-injection',
|
|
30
|
+
severity: 'error',
|
|
31
|
+
rule_description: 'SQL Injection',
|
|
32
|
+
state: 'open',
|
|
33
|
+
most_recent_instance_path: 'src/db.ts',
|
|
34
|
+
most_recent_instance_start_line: 10,
|
|
35
|
+
html_url: 'https://github.com/test/repo/security/code-scanning/1'
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
it('should handle 404 when code scanning is not configured', async () => {
|
|
39
|
+
const notFoundError = new Error('Not Found');
|
|
40
|
+
notFoundError.status = 404;
|
|
41
|
+
const mockListAlerts = vi.fn().mockRejectedValue(notFoundError);
|
|
42
|
+
vi.mocked(getOctokit).mockReturnValue({
|
|
43
|
+
codeScanning: { listAlertsForRepo: mockListAlerts }
|
|
44
|
+
});
|
|
45
|
+
const result = await executeAnalyzeCodeScanning({ owner: 'test', repo: 'repo' });
|
|
46
|
+
expect(result.content).toHaveLength(1);
|
|
47
|
+
expect(result.content[0].type).toBe('text');
|
|
48
|
+
expect(result.content[0].text).toBe("Code scanning not configured on this repo");
|
|
49
|
+
});
|
|
50
|
+
it('should throw error for non-404 API failures', async () => {
|
|
51
|
+
const serverError = new Error('Server Error');
|
|
52
|
+
serverError.status = 500;
|
|
53
|
+
const mockListAlerts = vi.fn().mockRejectedValue(serverError);
|
|
54
|
+
vi.mocked(getOctokit).mockReturnValue({
|
|
55
|
+
codeScanning: { listAlertsForRepo: mockListAlerts }
|
|
56
|
+
});
|
|
57
|
+
await expect(executeAnalyzeCodeScanning({ owner: 'test', repo: 'repo' }))
|
|
58
|
+
.rejects
|
|
59
|
+
.toThrow('Server Error');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
//# sourceMappingURL=analyze-code-scanning.test.js.map
|