@better_vibe/mcp-server 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/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # BetterVibe MCP Server
2
+
3
+ Audit your code health directly from your AI coding agent (Cursor, Claude Desktop, Windsurf, Cline).
4
+
5
+ Just tell your agent: **"audit this repo"** — and it runs a full BetterVibe audit, returning your score, grade, and what to fix.
6
+
7
+ ## Setup
8
+
9
+ ### 1. Get an API key
10
+
11
+ Go to [bettervibe.app/dashboard/keys](https://www.bettervibe.app/dashboard/keys) and create a key.
12
+
13
+ ### 2. Add to your AI agent
14
+
15
+ #### Cursor
16
+
17
+ Add to `.cursor/mcp.json` in your project (or global `~/.cursor/mcp.json`):
18
+
19
+ ```json
20
+ {
21
+ "mcpServers": {
22
+ "bettervibe": {
23
+ "command": "npx",
24
+ "args": ["-y", "@bettervibe/mcp-server"],
25
+ "env": {
26
+ "BETTERVIBE_API_KEY": "bv_live_your_key_here"
27
+ }
28
+ }
29
+ }
30
+ }
31
+ ```
32
+
33
+ #### Claude Desktop
34
+
35
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
36
+
37
+ ```json
38
+ {
39
+ "mcpServers": {
40
+ "bettervibe": {
41
+ "command": "npx",
42
+ "args": ["-y", "@bettervibe/mcp-server"],
43
+ "env": {
44
+ "BETTERVIBE_API_KEY": "bv_live_your_key_here"
45
+ }
46
+ }
47
+ }
48
+ }
49
+ ```
50
+
51
+ #### Alternative: config file
52
+
53
+ Instead of setting the env var per-agent, create `~/.bettervibe`:
54
+
55
+ ```json
56
+ { "apiKey": "bv_live_your_key_here" }
57
+ ```
58
+
59
+ ### 3. Use it
60
+
61
+ Just talk to your AI agent:
62
+
63
+ - "Audit this repo"
64
+ - "What's my code health score?"
65
+ - "Audit this repo and fix any issues below 70"
66
+ - "Run a BetterVibe check before I push"
67
+
68
+ ## Available Tools
69
+
70
+ | Tool | Description |
71
+ |------|-------------|
72
+ | `audit_repo` | Full audit — score, grade, per-category breakdown, issues to fix |
73
+ | `get_score` | Quick check — just the score and grade |
74
+
75
+ ## Available Prompts
76
+
77
+ | Prompt | Description |
78
+ |--------|-------------|
79
+ | `audit-before-push` | Runs an audit and suggests fixes for anything below 70 |
80
+
81
+ ## How it works
82
+
83
+ 1. The MCP server auto-detects your GitHub repo from the local git remote
84
+ 2. Calls the BetterVibe API with your key
85
+ 3. Returns a structured audit result the AI agent can act on
86
+ 4. The agent can then suggest or apply fixes directly
87
+
88
+ ## Requirements
89
+
90
+ - Node.js 18+
91
+ - A GitHub repo (public, or private with GitHub App connected)
92
+ - A BetterVibe API key ($9.99/mo Pro, or 3 free audits)
93
+
94
+ ## Local development
95
+
96
+ ```bash
97
+ cd mcp-server
98
+ npm install
99
+ npm run build
100
+ BETTERVIBE_API_KEY=bv_live_... node dist/index.js
101
+ ```
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { getApiKey, detectRepoUrl, runAudit, formatAuditResult } from "./lib.js";
6
+ const server = new McpServer({
7
+ name: "bettervibe",
8
+ version: "1.0.0",
9
+ });
10
+ // Tool: audit_repo
11
+ server.registerTool("audit_repo", {
12
+ title: "Audit Code Health",
13
+ description: "Run a BetterVibe code health audit on a GitHub repo. Returns a 0-100 score, letter grade, and per-category breakdown (security, tests, architecture, secrets, duplication, CI/CD). If no repo URL is provided, auto-detects from the current git remote.",
14
+ inputSchema: {
15
+ repoUrl: z.string().optional().describe("GitHub repo URL (e.g. https://github.com/owner/repo). Auto-detected from git remote if omitted."),
16
+ deep: z.boolean().optional().describe("Run a Deep Audit with full tool suite (Semgrep, osv-scanner, secretlint, jscpd). Defaults to true."),
17
+ },
18
+ }, async ({ repoUrl, deep }) => {
19
+ const apiKey = getApiKey();
20
+ if (!apiKey) {
21
+ return {
22
+ content: [{ type: "text", text: "❌ No BetterVibe API key found. Set BETTERVIBE_API_KEY environment variable or create ~/.bettervibe with {\"apiKey\": \"bv_live_...\"}. Get a key at https://www.bettervibe.app/dashboard/keys" }],
23
+ isError: true,
24
+ };
25
+ }
26
+ const url = repoUrl || detectRepoUrl();
27
+ if (!url) {
28
+ return {
29
+ content: [{ type: "text", text: "❌ Could not detect repo URL. Either provide a repoUrl parameter or run this from a directory with a GitHub git remote." }],
30
+ isError: true,
31
+ };
32
+ }
33
+ const result = await runAudit(url, apiKey, deep !== false);
34
+ if (result.error) {
35
+ return { content: [{ type: "text", text: `❌ Audit failed: ${result.error}` }], isError: true };
36
+ }
37
+ return { content: [{ type: "text", text: formatAuditResult(result, url) }] };
38
+ });
39
+ // Tool: get_score
40
+ server.registerTool("get_score", {
41
+ title: "Get Code Health Score",
42
+ description: "Quick check — returns just the numeric score (0-100) and grade for a repo.",
43
+ inputSchema: {
44
+ repoUrl: z.string().optional().describe("GitHub repo URL. Auto-detected if omitted."),
45
+ },
46
+ }, async ({ repoUrl }) => {
47
+ const apiKey = getApiKey();
48
+ if (!apiKey) {
49
+ return { content: [{ type: "text", text: "❌ No API key. Set BETTERVIBE_API_KEY env var." }], isError: true };
50
+ }
51
+ const url = repoUrl || detectRepoUrl();
52
+ if (!url) {
53
+ return { content: [{ type: "text", text: "❌ Could not detect repo URL." }], isError: true };
54
+ }
55
+ const result = await runAudit(url, apiKey, true);
56
+ if (result.error) {
57
+ return { content: [{ type: "text", text: `❌ ${result.error}` }], isError: true };
58
+ }
59
+ return { content: [{ type: "text", text: `${result.score}/100 (${result.grade})` }] };
60
+ });
61
+ // Prompt: pre-push audit
62
+ server.registerPrompt("audit-before-push", {
63
+ title: "Audit Before Push",
64
+ description: "Run a code health audit and suggest fixes before pushing to GitHub.",
65
+ }, () => ({
66
+ messages: [
67
+ {
68
+ role: "user",
69
+ content: {
70
+ type: "text",
71
+ text: "Run a BetterVibe audit on this repo. If the score is below 70, list the top issues and suggest specific code fixes I can make right now before pushing.",
72
+ },
73
+ },
74
+ ],
75
+ }));
76
+ // Start
77
+ const transport = new StdioServerTransport();
78
+ await server.connect(transport);
package/dist/lib.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ /** Get API key from env or ~/.bettervibe config file */
2
+ export declare function getApiKey(): string | null;
3
+ /** Detect the GitHub repo URL from the local git remote */
4
+ export declare function detectRepoUrl(cwd?: string): string | null;
5
+ export type AuditResult = {
6
+ score: number;
7
+ grade: string;
8
+ categories: any[];
9
+ error?: string;
10
+ cached?: boolean;
11
+ };
12
+ /** Call the BetterVibe audit API */
13
+ export declare function runAudit(repoUrl: string, apiKey: string, deep: boolean, baseUrl?: string): Promise<AuditResult>;
14
+ /** Format an audit result for display in an AI agent */
15
+ export declare function formatAuditResult(result: AuditResult, repoUrl: string): string;
package/dist/lib.js ADDED
@@ -0,0 +1,74 @@
1
+ import { execSync } from "child_process";
2
+ import { readFileSync } from "fs";
3
+ import { join } from "path";
4
+ /** Get API key from env or ~/.bettervibe config file */
5
+ export function getApiKey() {
6
+ if (process.env.BETTERVIBE_API_KEY)
7
+ return process.env.BETTERVIBE_API_KEY;
8
+ try {
9
+ const home = process.env.HOME || process.env.USERPROFILE || "";
10
+ const configPath = join(home, ".bettervibe");
11
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
12
+ return config.apiKey || null;
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ /** Detect the GitHub repo URL from the local git remote */
19
+ export function detectRepoUrl(cwd) {
20
+ try {
21
+ const remote = execSync("git remote get-url origin", {
22
+ cwd: cwd || process.cwd(),
23
+ encoding: "utf-8",
24
+ timeout: 5000,
25
+ }).trim();
26
+ // Convert SSH to HTTPS
27
+ if (remote.startsWith("git@github.com:")) {
28
+ return `https://github.com/${remote.slice(15).replace(/\.git$/, "")}`;
29
+ }
30
+ if (remote.includes("github.com")) {
31
+ return remote.replace(/\.git$/, "");
32
+ }
33
+ return null;
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ /** Call the BetterVibe audit API */
40
+ export async function runAudit(repoUrl, apiKey, deep, baseUrl = "https://www.bettervibe.app") {
41
+ const res = await fetch(`${baseUrl}/api/audit`, {
42
+ method: "POST",
43
+ headers: {
44
+ "Content-Type": "application/json",
45
+ "X-API-Key": apiKey,
46
+ },
47
+ body: JSON.stringify({ repoUrl, deep }),
48
+ });
49
+ const data = await res.json();
50
+ if (!res.ok) {
51
+ return { score: 0, grade: "error", categories: [], error: data.error || `HTTP ${res.status}` };
52
+ }
53
+ return data;
54
+ }
55
+ /** Format an audit result for display in an AI agent */
56
+ export function formatAuditResult(result, repoUrl) {
57
+ const categoryLines = (result.categories || [])
58
+ .filter((c) => c.applicable !== false)
59
+ .map((c) => ` ${c.icon} ${c.name}: ${c.pct ?? 0}%`)
60
+ .join("\n");
61
+ const findings = (result.categories || [])
62
+ .flatMap((c) => (c.checkpoints || [])
63
+ .filter((cp) => cp.applicable && !cp.passed)
64
+ .map((cp) => ` ⚠️ ${cp.label}${cp.detail ? ` — ${cp.detail}` : ""}`))
65
+ .join("\n");
66
+ return `🛡️ BetterVibe Audit — ${result.score}/100 (${result.grade})${result.cached ? " (cached)" : ""}
67
+
68
+ 📊 Categories:
69
+ ${categoryLines}
70
+
71
+ ${findings ? `🔧 Issues to fix:\n${findings}` : "✅ No critical issues found."}
72
+
73
+ Repo: ${repoUrl}`;
74
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,146 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { getApiKey, detectRepoUrl, runAudit, formatAuditResult } from "./lib.js";
3
+ describe("getApiKey", () => {
4
+ const originalEnv = process.env;
5
+ beforeEach(() => {
6
+ process.env = { ...originalEnv };
7
+ });
8
+ afterEach(() => {
9
+ process.env = originalEnv;
10
+ });
11
+ it("returns BETTERVIBE_API_KEY from env", () => {
12
+ process.env.BETTERVIBE_API_KEY = "bv_live_test123";
13
+ expect(getApiKey()).toBe("bv_live_test123");
14
+ });
15
+ it("returns null when no env var and no config file", () => {
16
+ delete process.env.BETTERVIBE_API_KEY;
17
+ process.env.HOME = "/nonexistent";
18
+ expect(getApiKey()).toBeNull();
19
+ });
20
+ });
21
+ describe("detectRepoUrl", () => {
22
+ it("detects the repo URL from this repo's git remote", () => {
23
+ // We're running from within the better-vibe repo
24
+ const url = detectRepoUrl(process.cwd());
25
+ // It should be a github URL (SSH or HTTPS)
26
+ if (url) {
27
+ expect(url).toContain("github.com");
28
+ expect(url).toContain("better-vibe");
29
+ }
30
+ });
31
+ it("converts SSH remote to HTTPS", () => {
32
+ // Test the conversion logic by calling from a known dir
33
+ // (the current repo uses SSH or HTTPS — either way it should return HTTPS)
34
+ const url = detectRepoUrl(process.cwd());
35
+ if (url) {
36
+ expect(url.startsWith("https://")).toBe(true);
37
+ }
38
+ });
39
+ it("returns null for a non-git directory", () => {
40
+ const url = detectRepoUrl("/tmp");
41
+ expect(url).toBeNull();
42
+ });
43
+ });
44
+ describe("runAudit", () => {
45
+ it("calls the BetterVibe API with correct headers", async () => {
46
+ const mockFetch = vi.fn().mockResolvedValue({
47
+ ok: true,
48
+ json: () => Promise.resolve({ score: 85, grade: "A", categories: [] }),
49
+ });
50
+ vi.stubGlobal("fetch", mockFetch);
51
+ const result = await runAudit("https://github.com/test/repo", "bv_live_testkey", true, "https://example.com");
52
+ expect(mockFetch).toHaveBeenCalledWith("https://example.com/api/audit", expect.objectContaining({
53
+ method: "POST",
54
+ headers: {
55
+ "Content-Type": "application/json",
56
+ "X-API-Key": "bv_live_testkey",
57
+ },
58
+ body: JSON.stringify({ repoUrl: "https://github.com/test/repo", deep: true }),
59
+ }));
60
+ expect(result.score).toBe(85);
61
+ expect(result.grade).toBe("A");
62
+ vi.unstubAllGlobals();
63
+ });
64
+ it("returns error on non-200 response", async () => {
65
+ const mockFetch = vi.fn().mockResolvedValue({
66
+ ok: false,
67
+ status: 402,
68
+ json: () => Promise.resolve({ error: "Free audit used." }),
69
+ });
70
+ vi.stubGlobal("fetch", mockFetch);
71
+ const result = await runAudit("https://github.com/test/repo", "bv_live_testkey", true, "https://example.com");
72
+ expect(result.error).toBe("Free audit used.");
73
+ expect(result.score).toBe(0);
74
+ vi.unstubAllGlobals();
75
+ });
76
+ it("passes deep=false when requested", async () => {
77
+ const mockFetch = vi.fn().mockResolvedValue({
78
+ ok: true,
79
+ json: () => Promise.resolve({ score: 70, grade: "B", categories: [] }),
80
+ });
81
+ vi.stubGlobal("fetch", mockFetch);
82
+ await runAudit("https://github.com/test/repo", "bv_live_testkey", false, "https://example.com");
83
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
84
+ expect(body.deep).toBe(false);
85
+ vi.unstubAllGlobals();
86
+ });
87
+ });
88
+ describe("formatAuditResult", () => {
89
+ it("formats a successful audit with categories", () => {
90
+ const result = {
91
+ score: 75,
92
+ grade: "B — solid, a few gaps",
93
+ categories: [
94
+ { id: "testing", name: "Test Coverage", icon: "🧪", pct: 100, applicable: true, checkpoints: [] },
95
+ { id: "security", name: "Security", icon: "🔐", pct: 80, applicable: true, checkpoints: [] },
96
+ ],
97
+ };
98
+ const text = formatAuditResult(result, "https://github.com/test/repo");
99
+ expect(text).toContain("75/100");
100
+ expect(text).toContain("B — solid, a few gaps");
101
+ expect(text).toContain("🧪 Test Coverage: 100%");
102
+ expect(text).toContain("🔐 Security: 80%");
103
+ expect(text).toContain("https://github.com/test/repo");
104
+ expect(text).toContain("✅ No critical issues found.");
105
+ });
106
+ it("shows issues when checkpoints fail", () => {
107
+ const result = {
108
+ score: 45,
109
+ grade: "C — needs work",
110
+ categories: [
111
+ {
112
+ id: "security",
113
+ name: "Security",
114
+ icon: "🔐",
115
+ pct: 30,
116
+ applicable: true,
117
+ checkpoints: [
118
+ { id: "no-secrets", label: "No secrets in code", applicable: true, passed: false, detail: "3 secrets found" },
119
+ { id: "deps-secure", label: "Dependencies secure", applicable: true, passed: true, detail: "" },
120
+ ],
121
+ },
122
+ ],
123
+ };
124
+ const text = formatAuditResult(result, "https://github.com/test/repo");
125
+ expect(text).toContain("⚠️ No secrets in code — 3 secrets found");
126
+ expect(text).not.toContain("Dependencies secure"); // passed, shouldn't show
127
+ });
128
+ it("shows (cached) label when result is cached", () => {
129
+ const result = { score: 80, grade: "B", categories: [], cached: true };
130
+ const text = formatAuditResult(result, "https://github.com/test/repo");
131
+ expect(text).toContain("(cached)");
132
+ });
133
+ it("skips non-applicable categories", () => {
134
+ const result = {
135
+ score: 90,
136
+ grade: "A",
137
+ categories: [
138
+ { id: "testing", name: "Test Coverage", icon: "🧪", pct: 100, applicable: true, checkpoints: [] },
139
+ { id: "ci", name: "CI/CD", icon: "⚙️", pct: null, applicable: false, checkpoints: [] },
140
+ ],
141
+ };
142
+ const text = formatAuditResult(result, "https://github.com/test/repo");
143
+ expect(text).toContain("Test Coverage");
144
+ expect(text).not.toContain("CI/CD");
145
+ });
146
+ });
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@better_vibe/mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "BetterVibe MCP server — audit your code health from any AI coding agent (Cursor, Claude Desktop, Windsurf, Kiro)",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "bettervibe-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "start": "node dist/index.js",
17
+ "test": "vitest run",
18
+ "prepublishOnly": "npm run build"
19
+ },
20
+ "dependencies": {
21
+ "@modelcontextprotocol/sdk": "^1.12.0",
22
+ "zod": "^3.23.0"
23
+ },
24
+ "devDependencies": {
25
+ "typescript": "^5.5.0",
26
+ "vitest": "^4.1.10"
27
+ },
28
+ "keywords": ["mcp", "bettervibe", "code-audit", "vibe-coding", "cursor", "claude", "kiro", "windsurf"],
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/sumitvairagar/better-vibe",
33
+ "directory": "mcp-server"
34
+ },
35
+ "homepage": "https://www.bettervibe.app/dashboard/keys"
36
+ }