koalr-mcp 0.3.0 → 0.3.2
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 +21 -11
- package/dist/tools/ai-adoption.js +2 -2
- package/dist/tools/coverage.js +1 -1
- package/dist/tools/deploy-risk.js +1 -1
- package/dist/tools/developers.js +2 -2
- package/dist/tools/dora.js +2 -2
- package/dist/tools/incidents.js +1 -1
- package/dist/tools/organization.js +2 -2
- package/dist/tools/pull-requests.js +3 -3
- package/dist/tools/repositories.js +2 -2
- package/dist/tools/search.js +1 -1
- package/dist/tools/teams.js +3 -3
- package/package.json +1 -1
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: /
|
|
27
|
-
// Route: /mcp
|
|
28
|
-
|
|
36
|
+
// Route: /mcp?apiKey=koalr_xxx — Smithery 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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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 ===
|
|
37
|
-
|
|
38
|
-
mcpPath = '/mcp';
|
|
47
|
+
else if (pathParts.length === 2 && pathParts[1] === 'mcp') {
|
|
48
|
+
sessionApiKey = pathParts[0];
|
|
39
49
|
}
|
|
40
|
-
if (
|
|
50
|
+
if (isMcpEndpoint) {
|
|
41
51
|
if (req.method === 'POST') {
|
|
42
52
|
const sessionId = req.headers['mcp-session-id'] ?? undefined;
|
|
43
53
|
let transport;
|
|
@@ -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);
|
package/dist/tools/coverage.js
CHANGED
|
@@ -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,
|
package/dist/tools/developers.js
CHANGED
|
@@ -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);
|
package/dist/tools/dora.js
CHANGED
|
@@ -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);
|
package/dist/tools/incidents.js
CHANGED
|
@@ -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
|
});
|
package/dist/tools/search.js
CHANGED
|
@@ -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;
|
package/dist/tools/teams.js
CHANGED
|
@@ -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
|
});
|