koalr-mcp 0.3.0 → 0.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/dist/index.js CHANGED
@@ -17,27 +17,37 @@ async function startHttp() {
17
17
  const transports = new Map();
18
18
  const httpServer = createHttpServer(async (req, res) => {
19
19
  const url = new URL(req.url ?? '/', `http://localhost:${port}`);
20
+ // CORS — required for browser-based MCP clients (Claude.ai web)
21
+ res.setHeader('Access-Control-Allow-Origin', '*');
22
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
23
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept, Mcp-Session-Id');
24
+ res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');
25
+ if (req.method === 'OPTIONS') {
26
+ res.writeHead(204);
27
+ res.end();
28
+ return;
29
+ }
20
30
  // Health check (no auth required)
21
31
  if (req.method === 'GET' && url.pathname === '/health') {
22
32
  res.writeHead(200, { 'Content-Type': 'application/json' });
23
33
  res.end(JSON.stringify({ status: 'ok', server: 'koalr-mcp' }));
24
34
  return;
25
35
  }
26
- // Route: /{apiKey}/mcpper-user hosted endpoint
27
- // Route: /mcp env-var key (for local/self-hosted use)
28
- let mcpPath = '/mcp';
36
+ // Route: /mcp?apiKey=koalr_xxxSmithery gateway style (query param)
37
+ // Route: /{apiKey}/mcp path-based per-user endpoint
38
+ // Route: /mcp — env-var key (local/self-hosted)
29
39
  let sessionApiKey;
30
40
  const pathParts = url.pathname.split('/').filter(Boolean);
31
- if (pathParts.length === 2 && pathParts[1] === 'mcp') {
32
- // /{apiKey}/mcp
33
- sessionApiKey = pathParts[0];
34
- mcpPath = `/${pathParts[0]}/mcp`;
41
+ const isMcpEndpoint = (pathParts.length === 1 && pathParts[0] === 'mcp') ||
42
+ (pathParts.length === 2 && pathParts[1] === 'mcp');
43
+ // API key precedence: query param > path segment > env var
44
+ if (url.searchParams.has('apiKey')) {
45
+ sessionApiKey = url.searchParams.get('apiKey') ?? undefined;
35
46
  }
36
- else if (pathParts.length === 1 && pathParts[0] === 'mcp') {
37
- // /mcp — use env key
38
- mcpPath = '/mcp';
47
+ else if (pathParts.length === 2 && pathParts[1] === 'mcp') {
48
+ sessionApiKey = pathParts[0];
39
49
  }
40
- if (url.pathname === mcpPath) {
50
+ if (isMcpEndpoint) {
41
51
  if (req.method === 'POST') {
42
52
  const sessionId = req.headers['mcp-session-id'] ?? undefined;
43
53
  let transport;
package/dist/server.js CHANGED
@@ -10,10 +10,35 @@ import { registerIncidentTools } from './tools/incidents.js';
10
10
  import { registerAiAdoptionTools } from './tools/ai-adoption.js';
11
11
  import { registerOrganizationTools } from './tools/organization.js';
12
12
  import { registerDeployRiskTools } from './tools/deploy-risk.js';
13
+ const SYSTEM_PROMPT = `You are connected to Koalr, a deployment intelligence platform for engineering teams.
14
+
15
+ ## What this server does
16
+ Koalr tracks pull requests, DORA metrics, deploy risk, test coverage, incidents, and developer productivity across GitHub-connected repositories. All tools are read-only.
17
+
18
+ ## When to use which tool
19
+ - **Broad overview**: start with get_org_health
20
+ - **Merge safety**: use score_pr_for_deploy_risk — pass the PR number, file list, and size
21
+ - **Delivery performance**: get_dora_summary (or get_dora_trend for week-over-week changes)
22
+ - **Stale/blocked work**: get_at_risk_prs then get_open_prs for detail
23
+ - **Team or repo drill-down**: list_teams → get_team, or list_repositories → get_repository
24
+ - **Individual developer**: get_developer (requires GitHub login) or list_top_contributors
25
+ - **Incidents**: list_recent_incidents returns MTTR and impact
26
+ - **Coverage**: get_coverage_summary by repository
27
+ - **AI tool usage**: get_ai_adoption_summary or get_ai_adoption_trend
28
+
29
+ ## Key parameters
30
+ - Most tools accept optional \`from\`/\`to\` ISO date strings to set the time window (default: last 30 days)
31
+ - Use \`teamId\` to scope metrics to a specific team — call list_teams first to find IDs
32
+ - Use \`repositoryId\` to scope metrics to a single repo — call list_repositories first
33
+
34
+ ## Risk score interpretation
35
+ score_pr_for_deploy_risk returns 0–100. 0–39 = low, 40–69 = medium, 70–89 = high, 90–100 = critical. Scores above 70 warrant a review before merging.`;
13
36
  export function createServer(apiKey) {
14
37
  const server = new McpServer({
15
38
  name: 'koalr',
16
- version: '0.3.0',
39
+ version: '0.3.2',
40
+ }, {
41
+ instructions: SYSTEM_PROMPT,
17
42
  });
18
43
  // Register all tool groups (apiKey is threaded through for hosted HTTP mode)
19
44
  registerDoraTools(server, apiKey);
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod/v3';
2
2
  import { apiGet, formatResult } from '../client.js';
3
3
  export function registerAiAdoptionTools(server, apiKey) {
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 () => {
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.', {}, { readOnlyHint: true }, async () => {
5
5
  const data = await apiGet('/api/v1/copilot', undefined, apiKey);
6
6
  return { content: [{ type: 'text', text: formatResult(data) }] };
7
7
  });
@@ -10,7 +10,7 @@ export function registerAiAdoptionTools(server, apiKey) {
10
10
  .number()
11
11
  .optional()
12
12
  .describe('Number of days to look back for trend data (default: 90)'),
13
- }, async ({ days }) => {
13
+ }, { readOnlyHint: true }, async ({ days }) => {
14
14
  const params = {};
15
15
  if (days)
16
16
  params['days'] = String(days);
@@ -6,7 +6,7 @@ export function registerCoverageTools(server, apiKey) {
6
6
  .string()
7
7
  .optional()
8
8
  .describe('Filter to a specific repository by ID. Omit to see all repositories.'),
9
- }, async ({ repositoryId }) => {
9
+ }, { readOnlyHint: true }, async ({ repositoryId }) => {
10
10
  const params = {};
11
11
  if (repositoryId)
12
12
  params['repositoryId'] = repositoryId;
@@ -24,7 +24,7 @@ export function registerDeployRiskTools(server, apiKey) {
24
24
  .boolean()
25
25
  .optional()
26
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, }) => {
27
+ }, { readOnlyHint: true }, async ({ owner, repo, prNumber, sha, title, body, authorLogin, additions, deletions, changedFiles, files, hasReview, hasApproval, }) => {
28
28
  const data = await apiPost('/api/v1/pr-risk/score', {
29
29
  repo: `${owner}/${repo}`,
30
30
  prNumber,
@@ -3,7 +3,7 @@ import { apiGet, formatResult } from '../client.js';
3
3
  export function registerDeveloperTools(server, apiKey) {
4
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
5
  login: z.string().describe('GitHub username / login of the developer (e.g. "jsmith")'),
6
- }, async ({ login }) => {
6
+ }, { readOnlyHint: true }, async ({ login }) => {
7
7
  const data = await apiGet('/developer-profiles', { login }, apiKey);
8
8
  return { content: [{ type: 'text', text: formatResult(data) }] };
9
9
  });
@@ -13,7 +13,7 @@ export function registerDeveloperTools(server, apiKey) {
13
13
  .number()
14
14
  .optional()
15
15
  .describe('Number of contributors to return (default: 10, max: 50)'),
16
- }, async ({ days, limit }) => {
16
+ }, { readOnlyHint: true }, async ({ days, limit }) => {
17
17
  const params = {};
18
18
  if (days)
19
19
  params['days'] = String(days);
@@ -11,7 +11,7 @@ export function registerDoraTools(server, apiKey) {
11
11
  .string()
12
12
  .optional()
13
13
  .describe('End date as ISO string (e.g. 2024-01-31). Defaults to today.'),
14
- }, async ({ teamId, from, to }) => {
14
+ }, { readOnlyHint: true }, async ({ teamId, from, to }) => {
15
15
  const params = {};
16
16
  if (teamId)
17
17
  params['teamId'] = teamId;
@@ -31,7 +31,7 @@ export function registerDoraTools(server, apiKey) {
31
31
  .optional()
32
32
  .describe('Number of days to look back (default: 90). Max recommended: 365.'),
33
33
  teamId: z.string().optional().describe('Team ID to filter metrics for a specific team'),
34
- }, async ({ metric, days, teamId }) => {
34
+ }, { readOnlyHint: true }, async ({ metric, days, teamId }) => {
35
35
  const params = { metric };
36
36
  if (days)
37
37
  params['days'] = String(days);
@@ -7,7 +7,7 @@ export function registerIncidentTools(server, apiKey) {
7
7
  .optional()
8
8
  .describe('Number of days to look back for incidents (default: 30)'),
9
9
  limit: z.number().optional().describe('Maximum number of incidents to return (default: 20)'),
10
- }, async ({ days, limit }) => {
10
+ }, { readOnlyHint: true }, async ({ days, limit }) => {
11
11
  const params = {};
12
12
  if (days) {
13
13
  const from = new Date(Date.now() - days * 86400000).toISOString();
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod/v3';
2
2
  import { apiGet, formatResult } from '../client.js';
3
3
  export function registerOrganizationTools(server, apiKey) {
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 () => {
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.', {}, { readOnlyHint: true }, async () => {
5
5
  const [doraData, flowData, teamsData] = await Promise.all([
6
6
  apiGet('/api/v1/metrics/dora', undefined, apiKey),
7
7
  apiGet('/api/v1/flow', undefined, apiKey),
@@ -20,7 +20,7 @@ export function registerOrganizationTools(server, apiKey) {
20
20
  .string()
21
21
  .optional()
22
22
  .describe('Team ID to filter well-being data. Omit for org-wide summary.'),
23
- }, async ({ teamId }) => {
23
+ }, { readOnlyHint: true }, async ({ teamId }) => {
24
24
  const params = {};
25
25
  if (teamId)
26
26
  params['teamId'] = teamId;
@@ -4,7 +4,7 @@ export function registerPullRequestTools(server, apiKey) {
4
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
5
  teamId: z.string().optional().describe('Team ID to filter to a specific team'),
6
6
  days: z.number().optional().describe('Number of days to look back (default: 30)'),
7
- }, async ({ teamId, days }) => {
7
+ }, { readOnlyHint: true }, async ({ teamId, days }) => {
8
8
  const params = {};
9
9
  if (teamId)
10
10
  params['teamId'] = teamId;
@@ -21,7 +21,7 @@ export function registerPullRequestTools(server, apiKey) {
21
21
  .boolean()
22
22
  .optional()
23
23
  .describe('If true, only return PRs that are large (>500 lines), old (>72h open), or have no reviewers'),
24
- }, async ({ repositoryId, highRiskOnly }) => {
24
+ }, { readOnlyHint: true }, async ({ repositoryId, highRiskOnly }) => {
25
25
  const params = { state: 'open' };
26
26
  if (repositoryId)
27
27
  params['repositoryId'] = repositoryId;
@@ -30,7 +30,7 @@ export function registerPullRequestTools(server, apiKey) {
30
30
  const data = await apiGet('/api/v1/pull-requests', params, apiKey);
31
31
  return { content: [{ type: 'text', text: formatResult(data) }] };
32
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 () => {
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.', {}, { readOnlyHint: true }, async () => {
34
34
  const params = {
35
35
  state: 'open',
36
36
  highRiskOnly: 'true',
@@ -6,7 +6,7 @@ export function registerRepositoryTools(server, apiKey) {
6
6
  .number()
7
7
  .optional()
8
8
  .describe('Maximum number of repositories to return (default: 50, max: 200)'),
9
- }, async ({ limit }) => {
9
+ }, { readOnlyHint: true }, async ({ limit }) => {
10
10
  const params = {};
11
11
  if (limit)
12
12
  params['limit'] = String(limit);
@@ -17,7 +17,7 @@ export function registerRepositoryTools(server, apiKey) {
17
17
  name: z
18
18
  .string()
19
19
  .describe('Repository name (e.g. "my-service") or full name with owner (e.g. "org/my-service")'),
20
- }, async ({ name }) => {
20
+ }, { readOnlyHint: true }, async ({ name }) => {
21
21
  const data = await apiGet('/repositories', { name }, apiKey);
22
22
  return { content: [{ type: 'text', text: formatResult(data) }] };
23
23
  });
@@ -7,7 +7,7 @@ export function registerSearchTools(server, apiKey) {
7
7
  .enum(['developer', 'repository', 'pr', 'team'])
8
8
  .optional()
9
9
  .describe('Limit results to a specific entity type. Omit to search all types.'),
10
- }, async ({ query, type }) => {
10
+ }, { readOnlyHint: true }, async ({ query, type }) => {
11
11
  const params = { q: query };
12
12
  if (type)
13
13
  params['type'] = type;
@@ -1,13 +1,13 @@
1
1
  import { z } from 'zod/v3';
2
2
  import { apiGet, formatResult } from '../client.js';
3
3
  export function registerTeamTools(server, apiKey) {
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 () => {
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.', {}, { readOnlyHint: true }, async () => {
5
5
  const data = await apiGet('/api/v1/teams', undefined, apiKey);
6
6
  return { content: [{ type: 'text', text: formatResult(data) }] };
7
7
  });
8
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
9
  teamId: z.string().describe('The team ID (use list_teams to find team IDs)'),
10
- }, async ({ teamId }) => {
10
+ }, { readOnlyHint: true }, async ({ teamId }) => {
11
11
  const [doraData, flowData] = await Promise.all([
12
12
  apiGet('/api/v1/metrics/dora', { teamId }, apiKey),
13
13
  apiGet('/api/v1/flow', { teamId }, apiKey),
@@ -21,7 +21,7 @@ export function registerTeamTools(server, apiKey) {
21
21
  });
22
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
23
  teamId: z.string().describe('The team ID (use list_teams to find team IDs)'),
24
- }, async ({ teamId }) => {
24
+ }, { readOnlyHint: true }, async ({ teamId }) => {
25
25
  const data = await apiGet(`/teams/${teamId}/members`, undefined, apiKey);
26
26
  return { content: [{ type: 'text', text: formatResult(data) }] };
27
27
  });
package/package.json CHANGED
@@ -1,15 +1,25 @@
1
1
  {
2
2
  "name": "koalr-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Koalr MCP server — connect engineering metrics to AI agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "keywords": [
8
- "koalr",
9
8
  "mcp",
9
+ "koalr",
10
10
  "engineering-metrics",
11
11
  "dora",
12
- "deploy-risk"
12
+ "deploy-risk",
13
+ "devops",
14
+ "github",
15
+ "pull-requests",
16
+ "code-review",
17
+ "developer-productivity",
18
+ "deployment",
19
+ "metrics",
20
+ "ai-agent",
21
+ "claude",
22
+ "model-context-protocol"
13
23
  ],
14
24
  "homepage": "https://docs.koalr.com/docs/features/mcp-server",
15
25
  "repository": {