koalr-mcp 0.1.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,171 @@
1
+ # koalr-mcp
2
+
3
+ Koalr MCP server — connect your engineering metrics to AI agents like Claude Code, Cursor, and others.
4
+
5
+ ## Quick Start
6
+
7
+ ### Generate an API Key
8
+
9
+ 1. Go to [app.koalr.com/settings/api-keys](https://app.koalr.com/settings/api-keys)
10
+ 2. Create a new API key with the scopes you need
11
+ 3. Copy the key (shown once) — it starts with `koalr_`
12
+
13
+ ### Claude Desktop
14
+
15
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
16
+
17
+ ```json
18
+ {
19
+ "mcpServers": {
20
+ "koalr": {
21
+ "command": "npx",
22
+ "args": ["-y", "koalr-mcp"],
23
+ "env": {
24
+ "KOALR_API_KEY": "koalr_your_key_here"
25
+ }
26
+ }
27
+ }
28
+ }
29
+ ```
30
+
31
+ ### Cursor
32
+
33
+ Add to your Cursor MCP settings (`~/.cursor/mcp.json`):
34
+
35
+ ```json
36
+ {
37
+ "mcpServers": {
38
+ "koalr": {
39
+ "command": "npx",
40
+ "args": ["-y", "koalr-mcp"],
41
+ "env": {
42
+ "KOALR_API_KEY": "koalr_your_key_here"
43
+ }
44
+ }
45
+ }
46
+ }
47
+ ```
48
+
49
+ ### Claude Code (CLI)
50
+
51
+ ```bash
52
+ # Add to your project's MCP config
53
+ claude mcp add koalr \
54
+ -e KOALR_API_KEY=koalr_your_key_here \
55
+ -- npx -y koalr-mcp
56
+ ```
57
+
58
+ ## Environment Variables
59
+
60
+ | Variable | Description | Default |
61
+ | --------------- | ---------------------------------------------------- | ----------------------- |
62
+ | `KOALR_API_KEY` | Your Koalr API key (required). Starts with `koalr_`. | — |
63
+ | `KOALR_API_URL` | Koalr API base URL (for self-hosted or local dev) | `http://localhost:3001` |
64
+ | `MCP_TRANSPORT` | Transport mode: `stdio` (default) or `http` | `stdio` |
65
+ | `PORT` | Port for HTTP transport mode | `3010` |
66
+
67
+ ## Available Tools
68
+
69
+ ### Organization
70
+
71
+ | Tool | Description |
72
+ | ------------------------ | ----------------------------------------------------------------- |
73
+ | `get_org_health` | Comprehensive org health: DORA tier, cycle time, teams, incidents |
74
+ | `get_well_being_summary` | Developer well-being: focus time, meeting load, burnout signals |
75
+
76
+ ### DORA Metrics
77
+
78
+ | Tool | Description |
79
+ | ------------------ | ------------------------------------------------------ |
80
+ | `get_dora_summary` | Deploy frequency, lead time, change failure rate, MTTR |
81
+ | `get_dora_trend` | Weekly trend for any DORA metric |
82
+
83
+ ### Pull Requests
84
+
85
+ | Tool | Description |
86
+ | ----------------- | ----------------------------------------------- |
87
+ | `get_pr_summary` | Cycle time, throughput, review health metrics |
88
+ | `get_open_prs` | Currently open PRs with age and risk indicators |
89
+ | `get_at_risk_prs` | PRs at risk of being long-running or blocked |
90
+
91
+ ### Teams
92
+
93
+ | Tool | Description |
94
+ | ------------------- | ------------------------------------ |
95
+ | `list_teams` | All teams with IDs and member counts |
96
+ | `get_team` | Team-level DORA and flow metrics |
97
+ | `list_team_members` | Team roster with GitHub logins |
98
+
99
+ ### Repositories
100
+
101
+ | Tool | Description |
102
+ | ------------------- | ------------------------------------------------------------ |
103
+ | `list_repositories` | All repos with health scores |
104
+ | `get_repository` | Repo metrics: deployment frequency, cycle time, contributors |
105
+
106
+ ### Developers
107
+
108
+ | Tool | Description |
109
+ | ----------------------- | ------------------------------------------------ |
110
+ | `get_developer` | Individual developer metrics and recent activity |
111
+ | `list_top_contributors` | Most active contributors by commits and PRs |
112
+
113
+ ### Search
114
+
115
+ | Tool | Description |
116
+ | -------- | ------------------------------------------------ |
117
+ | `search` | Search developers, repos, PRs, and teams by name |
118
+
119
+ ### Coverage
120
+
121
+ | Tool | Description |
122
+ | ---------------------- | -------------------------------------- |
123
+ | `get_coverage_summary` | Test coverage by repository with trend |
124
+
125
+ ### Incidents
126
+
127
+ | Tool | Description |
128
+ | ----------------------- | -------------------------------------------------- |
129
+ | `list_recent_incidents` | Recent incidents from PagerDuty/OpsGenie with MTTR |
130
+
131
+ ### AI Adoption
132
+
133
+ | Tool | Description |
134
+ | ------------------------- | --------------------------------------- |
135
+ | `get_ai_adoption_summary` | GitHub Copilot and Cursor usage metrics |
136
+ | `get_ai_adoption_trend` | AI tool adoption trend over time |
137
+
138
+ ## Example Prompts
139
+
140
+ Once connected, you can ask your AI agent things like:
141
+
142
+ - "What is our team's DORA performance tier this month?"
143
+ - "Show me all PRs that have been open for more than 3 days"
144
+ - "Which developers have the highest PR throughput on the backend team?"
145
+ - "How is our AI coding tool adoption trending?"
146
+ - "What was our MTTR for incidents last quarter?"
147
+ - "Find the auth-service repository and show me its deployment frequency"
148
+
149
+ ## HTTP Transport (Remote Hosting)
150
+
151
+ For hosting the MCP server remotely (e.g., at `mcp.koalr.com`):
152
+
153
+ ```bash
154
+ MCP_TRANSPORT=http PORT=3010 KOALR_API_KEY=koalr_... node dist/index.js
155
+ ```
156
+
157
+ The server exposes `POST /mcp` and `GET /mcp` endpoints following the MCP Streamable HTTP transport spec.
158
+
159
+ ## Local Development
160
+
161
+ ```bash
162
+ # From repo root
163
+ pnpm install
164
+ pnpm exec turbo build --filter=koalr-mcp
165
+
166
+ # Run in stdio mode (test with MCP Inspector)
167
+ KOALR_API_KEY=koalr_... node apps/mcp/dist/index.js
168
+
169
+ # Run in HTTP mode
170
+ MCP_TRANSPORT=http KOALR_API_KEY=koalr_... node apps/mcp/dist/index.js
171
+ ```
package/dist/client.js ADDED
@@ -0,0 +1,42 @@
1
+ const API_URL = process.env['KOALR_API_URL'] ?? 'https://api.koalr.com';
2
+ const API_KEY = process.env['KOALR_API_KEY'] ?? '';
3
+ export async function apiGet(path, params) {
4
+ const url = new URL(`${API_URL}${path}`);
5
+ if (params) {
6
+ Object.entries(params).forEach(([k, v]) => {
7
+ if (v !== undefined && v !== null && v !== '') {
8
+ url.searchParams.set(k, v);
9
+ }
10
+ });
11
+ }
12
+ const res = await fetch(url.toString(), {
13
+ headers: {
14
+ Authorization: `Bearer ${API_KEY}`,
15
+ 'Content-Type': 'application/json',
16
+ },
17
+ });
18
+ if (!res.ok) {
19
+ const body = await res.text().catch(() => '');
20
+ throw new Error(`Koalr API ${res.status} at ${path}: ${body}`);
21
+ }
22
+ return res.json();
23
+ }
24
+ export async function apiPost(path, body) {
25
+ const url = new URL(`${API_URL}${path}`);
26
+ const res = await fetch(url.toString(), {
27
+ method: 'POST',
28
+ headers: {
29
+ Authorization: `Bearer ${API_KEY}`,
30
+ 'Content-Type': 'application/json',
31
+ },
32
+ body: JSON.stringify(body),
33
+ });
34
+ if (!res.ok) {
35
+ const text = await res.text().catch(() => '');
36
+ throw new Error(`Koalr API ${res.status} at ${path}: ${text}`);
37
+ }
38
+ return res.json();
39
+ }
40
+ export function formatResult(data) {
41
+ return JSON.stringify(data, null, 2);
42
+ }
package/dist/index.js ADDED
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
4
+ import { createServer } from './server.js';
5
+ import { createServer as createHttpServer } from 'node:http';
6
+ import { randomUUID } from 'node:crypto';
7
+ const mode = process.env['MCP_TRANSPORT'] ?? 'stdio';
8
+ async function startStdio() {
9
+ const server = createServer();
10
+ const transport = new StdioServerTransport();
11
+ await server.connect(transport);
12
+ process.stderr.write('Koalr MCP server running on stdio\n');
13
+ }
14
+ async function startHttp() {
15
+ const port = parseInt(process.env['PORT'] ?? '3010', 10);
16
+ // Map of sessionId -> transport for stateful connections
17
+ const transports = new Map();
18
+ const httpServer = createHttpServer(async (req, res) => {
19
+ const url = new URL(req.url ?? '/', `http://localhost:${port}`);
20
+ // Health check
21
+ if (req.method === 'GET' && url.pathname === '/health') {
22
+ res.writeHead(200, { 'Content-Type': 'application/json' });
23
+ res.end(JSON.stringify({ status: 'ok', server: 'koalr-mcp' }));
24
+ return;
25
+ }
26
+ // MCP endpoint
27
+ if (url.pathname === '/mcp') {
28
+ if (req.method === 'POST') {
29
+ // Check for existing session or create new one
30
+ const sessionId = req.headers['mcp-session-id'] ?? undefined;
31
+ let transport;
32
+ if (sessionId && transports.has(sessionId)) {
33
+ transport = transports.get(sessionId);
34
+ }
35
+ else if (!sessionId) {
36
+ // New session
37
+ const newSessionId = randomUUID();
38
+ transport = new StreamableHTTPServerTransport({
39
+ sessionIdGenerator: () => newSessionId,
40
+ });
41
+ transports.set(newSessionId, transport);
42
+ transport.onclose = () => {
43
+ transports.delete(newSessionId);
44
+ };
45
+ const mcpServer = createServer();
46
+ await mcpServer.connect(transport);
47
+ }
48
+ if (!transport) {
49
+ res.writeHead(400, { 'Content-Type': 'application/json' });
50
+ res.end(JSON.stringify({ error: 'Invalid or missing session ID' }));
51
+ return;
52
+ }
53
+ await transport.handleRequest(req, res);
54
+ return;
55
+ }
56
+ if (req.method === 'GET' || req.method === 'DELETE') {
57
+ const sessionId = req.headers['mcp-session-id'] ?? undefined;
58
+ const transport = sessionId ? transports.get(sessionId) : undefined;
59
+ if (!transport) {
60
+ res.writeHead(404, { 'Content-Type': 'application/json' });
61
+ res.end(JSON.stringify({ error: 'Session not found' }));
62
+ return;
63
+ }
64
+ await transport.handleRequest(req, res);
65
+ return;
66
+ }
67
+ }
68
+ res.writeHead(404, { 'Content-Type': 'application/json' });
69
+ res.end(JSON.stringify({ error: 'Not found' }));
70
+ });
71
+ httpServer.listen(port, () => {
72
+ process.stderr.write(`Koalr MCP server running on http://localhost:${port}/mcp\n`);
73
+ });
74
+ }
75
+ if (mode === 'http') {
76
+ startHttp().catch((err) => {
77
+ process.stderr.write(`Fatal: ${String(err)}\n`);
78
+ process.exit(1);
79
+ });
80
+ }
81
+ else {
82
+ startStdio().catch((err) => {
83
+ process.stderr.write(`Fatal: ${String(err)}\n`);
84
+ process.exit(1);
85
+ });
86
+ }
package/dist/server.js ADDED
@@ -0,0 +1,31 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { registerDoraTools } from './tools/dora.js';
3
+ import { registerPullRequestTools } from './tools/pull-requests.js';
4
+ import { registerTeamTools } from './tools/teams.js';
5
+ import { registerRepositoryTools } from './tools/repositories.js';
6
+ import { registerDeveloperTools } from './tools/developers.js';
7
+ import { registerSearchTools } from './tools/search.js';
8
+ import { registerCoverageTools } from './tools/coverage.js';
9
+ import { registerIncidentTools } from './tools/incidents.js';
10
+ import { registerAiAdoptionTools } from './tools/ai-adoption.js';
11
+ import { registerOrganizationTools } from './tools/organization.js';
12
+ import { registerDeployRiskTools } from './tools/deploy-risk.js';
13
+ export function createServer() {
14
+ const server = new McpServer({
15
+ name: 'koalr',
16
+ version: '0.1.0',
17
+ });
18
+ // Register all tool groups
19
+ registerDoraTools(server);
20
+ registerPullRequestTools(server);
21
+ registerTeamTools(server);
22
+ registerRepositoryTools(server);
23
+ registerDeveloperTools(server);
24
+ registerSearchTools(server);
25
+ registerCoverageTools(server);
26
+ registerIncidentTools(server);
27
+ registerAiAdoptionTools(server);
28
+ registerOrganizationTools(server);
29
+ registerDeployRiskTools(server);
30
+ return server;
31
+ }
@@ -0,0 +1,20 @@
1
+ import { z } from 'zod/v3';
2
+ import { apiGet, formatResult } from '../client.js';
3
+ export function registerAiAdoptionTools(server) {
4
+ server.tool('get_ai_adoption_summary', 'Get AI coding tool adoption metrics including GitHub Copilot acceptance rate, Cursor active users, AI-generated code percentage, and suggestions per developer. Use this to understand how the team is using AI coding assistants.', {}, async () => {
5
+ const data = await apiGet('/api/v1/copilot');
6
+ return { content: [{ type: 'text', text: formatResult(data) }] };
7
+ });
8
+ server.tool('get_ai_adoption_trend', 'Get the trend of AI tool adoption over time showing weekly active users, acceptance rates, and code attribution percentages. Use this to track whether AI tool adoption is growing or declining.', {
9
+ days: z
10
+ .number()
11
+ .optional()
12
+ .describe('Number of days to look back for trend data (default: 90)'),
13
+ }, async ({ days }) => {
14
+ const params = {};
15
+ if (days)
16
+ params['days'] = String(days);
17
+ const data = await apiGet('/ai-analytics/trend', params);
18
+ return { content: [{ type: 'text', text: formatResult(data) }] };
19
+ });
20
+ }
@@ -0,0 +1,16 @@
1
+ import { z } from 'zod/v3';
2
+ import { apiGet, formatResult } from '../client.js';
3
+ export function registerCoverageTools(server) {
4
+ server.tool('get_coverage_summary', 'Get test coverage summary per repository showing overall coverage percentage, lines covered, and coverage trend. Use this to understand testing health and identify repositories with low coverage.', {
5
+ repositoryId: z
6
+ .string()
7
+ .optional()
8
+ .describe('Filter to a specific repository by ID. Omit to see all repositories.'),
9
+ }, async ({ repositoryId }) => {
10
+ const params = {};
11
+ if (repositoryId)
12
+ params['repositoryId'] = repositoryId;
13
+ const data = await apiGet('/coverage', params);
14
+ return { content: [{ type: 'text', text: formatResult(data) }] };
15
+ });
16
+ }
@@ -0,0 +1,44 @@
1
+ import { z } from 'zod/v3';
2
+ import { apiPost, formatResult } from '../client.js';
3
+ export function registerDeployRiskTools(server) {
4
+ server.tool('score_pr_for_deploy_risk', "Score a specific pull request for deployment risk using Koalr's 36-signal model. Returns a 0-100 risk score with a detailed factor breakdown covering change entropy, DDL migration detection, author file expertise, PR size, CODEOWNERS violations, and more. Use this to answer \"How risky is this PR?\" or \"Should we merge this before the release?\".", {
5
+ owner: z.string().describe('GitHub repository owner (org or user). Example: "acme".'),
6
+ repo: z.string().describe('Repository name. Example: "api-service".'),
7
+ prNumber: z.number().describe('Pull request number.'),
8
+ sha: z.string().describe('Head commit SHA.'),
9
+ title: z.string().describe('PR title.'),
10
+ body: z.string().optional().describe('PR description body (used to detect risky phrases).'),
11
+ authorLogin: z.string().optional().describe('GitHub login of the PR author.'),
12
+ additions: z.number().describe('Lines added.'),
13
+ deletions: z.number().describe('Lines deleted.'),
14
+ changedFiles: z.number().describe('Number of files changed.'),
15
+ files: z
16
+ .array(z.string())
17
+ .optional()
18
+ .describe('List of changed file paths. Used for DDL detection, CODEOWNERS analysis, and entropy calculation. More accurate results when provided.'),
19
+ hasReview: z
20
+ .boolean()
21
+ .optional()
22
+ .describe('Whether the PR has at least one review (any type).'),
23
+ hasApproval: z
24
+ .boolean()
25
+ .optional()
26
+ .describe('Whether the PR has at least one approving review.'),
27
+ }, async ({ owner, repo, prNumber, sha, title, body, authorLogin, additions, deletions, changedFiles, files, hasReview, hasApproval, }) => {
28
+ const data = await apiPost('/api/v1/pr-risk/score', {
29
+ repo: `${owner}/${repo}`,
30
+ prNumber,
31
+ sha,
32
+ title,
33
+ body: body ?? null,
34
+ authorLogin,
35
+ additions,
36
+ deletions,
37
+ changedFiles,
38
+ files: files ?? [],
39
+ hasReview: hasReview ?? false,
40
+ hasApproval: hasApproval ?? false,
41
+ });
42
+ return { content: [{ type: 'text', text: formatResult(data) }] };
43
+ });
44
+ }
@@ -0,0 +1,25 @@
1
+ import { z } from 'zod/v3';
2
+ import { apiGet, formatResult } from '../client.js';
3
+ export function registerDeveloperTools(server) {
4
+ server.tool('get_developer', "Get metrics for an individual developer including their PR throughput, review activity, average cycle time, and code contributions. Use this when asked about a specific engineer's activity or productivity.", {
5
+ login: z.string().describe('GitHub username / login of the developer (e.g. "jsmith")'),
6
+ }, async ({ login }) => {
7
+ const data = await apiGet('/developer-profiles', { login });
8
+ return { content: [{ type: 'text', text: formatResult(data) }] };
9
+ });
10
+ server.tool('list_top_contributors', 'List the most active contributors ranked by commits and PRs merged over a time window. Use this to identify key contributors, bus-factor risks, or to recognize top performers.', {
11
+ days: z.number().optional().describe('Time window in days to measure activity (default: 30)'),
12
+ limit: z
13
+ .number()
14
+ .optional()
15
+ .describe('Number of contributors to return (default: 10, max: 50)'),
16
+ }, async ({ days, limit }) => {
17
+ const params = {};
18
+ if (days)
19
+ params['days'] = String(days);
20
+ if (limit)
21
+ params['limit'] = String(limit);
22
+ const data = await apiGet('/developer-profiles/top-contributors', params);
23
+ return { content: [{ type: 'text', text: formatResult(data) }] };
24
+ });
25
+ }
@@ -0,0 +1,43 @@
1
+ import { z } from 'zod/v3';
2
+ import { apiGet, formatResult } from '../client.js';
3
+ export function registerDoraTools(server) {
4
+ server.tool('get_dora_summary', 'Get DORA metrics (deploy frequency, lead time for changes, change failure rate, MTTR) for the organization or a specific team. Use this to understand overall engineering delivery performance and reliability.', {
5
+ teamId: z.string().optional().describe('Team ID to filter metrics for a specific team'),
6
+ from: z
7
+ .string()
8
+ .optional()
9
+ .describe('Start date as ISO string (e.g. 2024-01-01). Defaults to 30 days ago.'),
10
+ to: z
11
+ .string()
12
+ .optional()
13
+ .describe('End date as ISO string (e.g. 2024-01-31). Defaults to today.'),
14
+ }, async ({ teamId, from, to }) => {
15
+ const params = {};
16
+ if (teamId)
17
+ params['teamId'] = teamId;
18
+ if (from)
19
+ params['from'] = from;
20
+ if (to)
21
+ params['to'] = to;
22
+ const data = await apiGet('/api/v1/metrics/dora', params);
23
+ return { content: [{ type: 'text', text: formatResult(data) }] };
24
+ });
25
+ server.tool('get_dora_trend', 'Get DORA metric trend over time to see how deployment frequency, lead time, change failure rate, or MTTR has changed week over week. Useful for spotting regressions or improvements.', {
26
+ metric: z
27
+ .enum(['deploy_frequency', 'lead_time', 'change_failure_rate', 'mttr'])
28
+ .describe('Which DORA metric to trend: deploy_frequency, lead_time, change_failure_rate, or mttr'),
29
+ days: z
30
+ .number()
31
+ .optional()
32
+ .describe('Number of days to look back (default: 90). Max recommended: 365.'),
33
+ teamId: z.string().optional().describe('Team ID to filter metrics for a specific team'),
34
+ }, async ({ metric, days, teamId }) => {
35
+ const params = { metric };
36
+ if (days)
37
+ params['days'] = String(days);
38
+ if (teamId)
39
+ params['teamId'] = teamId;
40
+ const data = await apiGet('/api/v1/metrics/dora/trend', params);
41
+ return { content: [{ type: 'text', text: formatResult(data) }] };
42
+ });
43
+ }
@@ -0,0 +1,21 @@
1
+ import { z } from 'zod/v3';
2
+ import { apiGet, formatResult } from '../client.js';
3
+ export function registerIncidentTools(server) {
4
+ server.tool('list_recent_incidents', 'List recent production incidents from PagerDuty or OpsGenie with their severity, MTTR (mean time to recovery), and affected services. Use this to understand reliability posture or investigate a recent outage.', {
5
+ days: z
6
+ .number()
7
+ .optional()
8
+ .describe('Number of days to look back for incidents (default: 30)'),
9
+ limit: z.number().optional().describe('Maximum number of incidents to return (default: 20)'),
10
+ }, async ({ days, limit }) => {
11
+ const params = {};
12
+ if (days) {
13
+ const from = new Date(Date.now() - days * 86400000).toISOString();
14
+ params['from'] = from;
15
+ }
16
+ if (limit)
17
+ params['take'] = String(limit);
18
+ const data = await apiGet('/api/v1/incidents', params);
19
+ return { content: [{ type: 'text', text: formatResult(data) }] };
20
+ });
21
+ }
@@ -0,0 +1,30 @@
1
+ import { z } from 'zod/v3';
2
+ import { apiGet, formatResult } from '../client.js';
3
+ export function registerOrganizationTools(server) {
4
+ server.tool('get_org_health', 'Get a comprehensive organization health snapshot: DORA performance tier (Elite/High/Medium/Low), cycle time percentile vs industry benchmarks, test coverage percentage, number of active teams, and incident rate. Use this as the first tool to get a high-level picture of engineering health.', {}, async () => {
5
+ const [doraData, flowData, teamsData] = await Promise.all([
6
+ apiGet('/api/v1/metrics/dora'),
7
+ apiGet('/api/v1/flow'),
8
+ apiGet('/api/v1/teams'),
9
+ ]);
10
+ const result = {
11
+ dora: doraData,
12
+ flow: flowData,
13
+ teams: teamsData,
14
+ _note: 'Use get_dora_summary, get_pr_summary, and list_teams for more detailed breakdowns.',
15
+ };
16
+ return { content: [{ type: 'text', text: formatResult(result) }] };
17
+ });
18
+ server.tool('get_well_being_summary', 'Get team well-being scores across pillars: focus time (uninterrupted deep work hours), meeting load (percentage of time in meetings), context switching (task interruptions per day), and burnout risk indicators. Use this to understand developer experience and identify teams under stress.', {
19
+ teamId: z
20
+ .string()
21
+ .optional()
22
+ .describe('Team ID to filter well-being data. Omit for org-wide summary.'),
23
+ }, async ({ teamId }) => {
24
+ const params = {};
25
+ if (teamId)
26
+ params['teamId'] = teamId;
27
+ const data = await apiGet('/wellbeing/summary', params);
28
+ return { content: [{ type: 'text', text: formatResult(data) }] };
29
+ });
30
+ }
@@ -0,0 +1,41 @@
1
+ import { z } from 'zod/v3';
2
+ import { apiGet, formatResult } from '../client.js';
3
+ export function registerPullRequestTools(server) {
4
+ server.tool('get_pr_summary', 'Get pull request metrics including cycle time (time from first commit to merge), throughput (PRs merged per week), review health (time to first review, reviewer distribution), and PR size trends. Use this to assess code review efficiency.', {
5
+ teamId: z.string().optional().describe('Team ID to filter to a specific team'),
6
+ days: z.number().optional().describe('Number of days to look back (default: 30)'),
7
+ }, async ({ teamId, days }) => {
8
+ const params = {};
9
+ if (teamId)
10
+ params['teamId'] = teamId;
11
+ if (days) {
12
+ const from = new Date(Date.now() - days * 86400000).toISOString();
13
+ params['from'] = from;
14
+ }
15
+ const data = await apiGet('/api/v1/review-metrics', params);
16
+ return { content: [{ type: 'text', text: formatResult(data) }] };
17
+ });
18
+ server.tool('get_open_prs', 'List currently open pull requests with their age in hours, size (additions + deletions), and reviewer assignments. Use this to identify stale or large PRs that may be blocking the team.', {
19
+ repositoryId: z.string().optional().describe('Filter to a specific repository by ID'),
20
+ highRiskOnly: z
21
+ .boolean()
22
+ .optional()
23
+ .describe('If true, only return PRs that are large (>500 lines), old (>72h open), or have no reviewers'),
24
+ }, async ({ repositoryId, highRiskOnly }) => {
25
+ const params = { state: 'open' };
26
+ if (repositoryId)
27
+ params['repositoryId'] = repositoryId;
28
+ if (highRiskOnly)
29
+ params['highRiskOnly'] = 'true';
30
+ const data = await apiGet('/api/v1/pull-requests', params);
31
+ return { content: [{ type: 'text', text: formatResult(data) }] };
32
+ });
33
+ server.tool('get_at_risk_prs', 'Get pull requests at risk of becoming long-running or blocked. These are PRs that have been open for more than 3 days, have no reviews, or are very large. Use this to prompt engineering leads to take action on blocked work.', {}, async () => {
34
+ const params = {
35
+ state: 'open',
36
+ highRiskOnly: 'true',
37
+ };
38
+ const data = await apiGet('/api/v1/pull-requests', params);
39
+ return { content: [{ type: 'text', text: formatResult(data) }] };
40
+ });
41
+ }
@@ -0,0 +1,24 @@
1
+ import { z } from 'zod/v3';
2
+ import { apiGet, formatResult } from '../client.js';
3
+ export function registerRepositoryTools(server) {
4
+ server.tool('list_repositories', 'List all repositories connected to Koalr with their health scores, activity levels, and basic stats. Use this to get an overview of the codebase landscape or find repository IDs for filtering.', {
5
+ limit: z
6
+ .number()
7
+ .optional()
8
+ .describe('Maximum number of repositories to return (default: 50, max: 200)'),
9
+ }, async ({ limit }) => {
10
+ const params = {};
11
+ if (limit)
12
+ params['limit'] = String(limit);
13
+ const data = await apiGet('/repositories', params);
14
+ return { content: [{ type: 'text', text: formatResult(data) }] };
15
+ });
16
+ server.tool('get_repository', 'Get detailed metrics for a specific repository including deployment frequency, PR cycle time, contributor count, and code health indicators. Use this when asked about a specific codebase.', {
17
+ name: z
18
+ .string()
19
+ .describe('Repository name (e.g. "my-service") or full name with owner (e.g. "org/my-service")'),
20
+ }, async ({ name }) => {
21
+ const data = await apiGet('/repositories', { name });
22
+ return { content: [{ type: 'text', text: formatResult(data) }] };
23
+ });
24
+ }
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod/v3';
2
+ import { apiGet, formatResult } from '../client.js';
3
+ export function registerSearchTools(server) {
4
+ server.tool('search', 'Search across all Koalr entities: developers (by name or GitHub login), repositories (by name), pull requests (by title or branch), and teams (by name). Use this when you need to find an entity before using a more specific tool.', {
5
+ query: z.string().describe('Search query text (e.g. "payments", "alice", "auth-service")'),
6
+ type: z
7
+ .enum(['developer', 'repository', 'pr', 'team'])
8
+ .optional()
9
+ .describe('Limit results to a specific entity type. Omit to search all types.'),
10
+ }, async ({ query, type }) => {
11
+ const params = { q: query };
12
+ if (type)
13
+ params['type'] = type;
14
+ const data = await apiGet('/search', params);
15
+ return { content: [{ type: 'text', text: formatResult(data) }] };
16
+ });
17
+ }
@@ -0,0 +1,28 @@
1
+ import { z } from 'zod/v3';
2
+ import { apiGet, formatResult } from '../client.js';
3
+ export function registerTeamTools(server) {
4
+ server.tool('list_teams', 'List all engineering teams in the organization with their member counts and slugs. Use this to discover team IDs needed for filtering other metrics tools.', {}, async () => {
5
+ const data = await apiGet('/api/v1/teams');
6
+ return { content: [{ type: 'text', text: formatResult(data) }] };
7
+ });
8
+ server.tool('get_team', "Get details and metrics for a specific team including DORA performance, cycle time, and member count. Use this when asked about a specific team's engineering health.", {
9
+ teamId: z.string().describe('The team ID (use list_teams to find team IDs)'),
10
+ }, async ({ teamId }) => {
11
+ const [doraData, flowData] = await Promise.all([
12
+ apiGet('/api/v1/metrics/dora', { teamId }),
13
+ apiGet('/api/v1/flow', { teamId }),
14
+ ]);
15
+ const result = {
16
+ teamId,
17
+ dora: doraData,
18
+ flow: flowData,
19
+ };
20
+ return { content: [{ type: 'text', text: formatResult(result) }] };
21
+ });
22
+ server.tool('list_team_members', 'List all members of a specific team with their GitHub logins and roles. Use this to understand team composition or find developer logins for the get_developer tool.', {
23
+ teamId: z.string().describe('The team ID (use list_teams to find team IDs)'),
24
+ }, async ({ teamId }) => {
25
+ const data = await apiGet(`/teams/${teamId}/members`);
26
+ return { content: [{ type: 'text', text: formatResult(data) }] };
27
+ });
28
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "koalr-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Koalr MCP server — connect engineering metrics to AI agents",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "koalr",
9
+ "mcp",
10
+ "engineering-metrics",
11
+ "dora",
12
+ "deploy-risk"
13
+ ],
14
+ "homepage": "https://docs.koalr.com/docs/features/mcp-server",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/koalr/koalr-app"
18
+ },
19
+ "bin": {
20
+ "koalr-mcp": "./dist/index.js"
21
+ },
22
+ "files": [
23
+ "dist/**/*"
24
+ ],
25
+ "scripts": {
26
+ "build": "tsc",
27
+ "dev": "tsx src/index.ts",
28
+ "start": "node dist/index.js",
29
+ "lint": "eslint src",
30
+ "typecheck": "tsc --noEmit",
31
+ "prepublishOnly": "pnpm build"
32
+ },
33
+ "dependencies": {
34
+ "@modelcontextprotocol/sdk": "^1.0.0",
35
+ "zod": "^4.0.0"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^20",
39
+ "tsx": "^4.0.0",
40
+ "typescript": "^5.9.3"
41
+ },
42
+ "engines": {
43
+ "node": ">=20"
44
+ }
45
+ }