@prodbeam/mcp 0.1.1 → 0.2.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/CHANGELOG.md +29 -0
- package/README.md +626 -67
- package/dist/auth/app-config.d.ts +8 -0
- package/dist/auth/app-config.d.ts.map +1 -0
- package/dist/auth/app-config.js +32 -0
- package/dist/auth/app-config.js.map +1 -0
- package/dist/auth/auth-provider.d.ts +9 -0
- package/dist/auth/auth-provider.d.ts.map +1 -0
- package/dist/auth/auth-provider.js +173 -0
- package/dist/auth/auth-provider.js.map +1 -0
- package/dist/auth/github-device-flow.d.ts +5 -0
- package/dist/auth/github-device-flow.d.ts.map +1 -0
- package/dist/auth/github-device-flow.js +139 -0
- package/dist/auth/github-device-flow.js.map +1 -0
- package/dist/auth/jira-oauth-flow.d.ts +19 -0
- package/dist/auth/jira-oauth-flow.d.ts.map +1 -0
- package/dist/auth/jira-oauth-flow.js +210 -0
- package/dist/auth/jira-oauth-flow.js.map +1 -0
- package/dist/auth/token-store.d.ts +7 -0
- package/dist/auth/token-store.d.ts.map +1 -0
- package/dist/auth/token-store.js +74 -0
- package/dist/auth/token-store.js.map +1 -0
- package/dist/auth/types.d.ts +51 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +2 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/cli.js +403 -64
- package/dist/cli.js.map +1 -1
- package/dist/clients/github-client.d.ts +2 -2
- package/dist/clients/github-client.d.ts.map +1 -1
- package/dist/clients/github-client.js +14 -4
- package/dist/clients/github-client.js.map +1 -1
- package/dist/clients/jira-client.d.ts +9 -3
- package/dist/clients/jira-client.d.ts.map +1 -1
- package/dist/clients/jira-client.js +53 -10
- package/dist/clients/jira-client.js.map +1 -1
- package/dist/clients/types.d.ts +21 -0
- package/dist/clients/types.d.ts.map +1 -1
- package/dist/commands/auth.d.ts +4 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +254 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +45 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/config/credentials.d.ts +2 -0
- package/dist/config/credentials.d.ts.map +1 -1
- package/dist/config/credentials.js +6 -0
- package/dist/config/credentials.js.map +1 -1
- package/dist/generators/metrics-calculator.d.ts.map +1 -1
- package/dist/generators/metrics-calculator.js +28 -0
- package/dist/generators/metrics-calculator.js.map +1 -1
- package/dist/generators/report-generator.d.ts +2 -1
- package/dist/generators/report-generator.d.ts.map +1 -1
- package/dist/generators/report-generator.js +565 -131
- package/dist/generators/report-generator.js.map +1 -1
- package/dist/index.js +275 -89
- package/dist/index.js.map +1 -1
- package/dist/insights/content-insights.d.ts +46 -0
- package/dist/insights/content-insights.d.ts.map +1 -0
- package/dist/insights/content-insights.js +193 -0
- package/dist/insights/content-insights.js.map +1 -0
- package/dist/orchestrator/data-fetcher.d.ts +3 -1
- package/dist/orchestrator/data-fetcher.d.ts.map +1 -1
- package/dist/orchestrator/data-fetcher.js +15 -0
- package/dist/orchestrator/data-fetcher.js.map +1 -1
- package/dist/types/github.d.ts +3 -0
- package/dist/types/github.d.ts.map +1 -1
- package/dist/types/jira.d.ts +9 -0
- package/dist/types/jira.d.ts.map +1 -1
- package/dist/types/retrospective.d.ts +15 -0
- package/dist/types/retrospective.d.ts.map +1 -1
- package/dist/types/weekly.d.ts +7 -0
- package/dist/types/weekly.d.ts.map +1 -1
- package/dist/validators.d.ts +6 -6
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,24 +1,54 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
-
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
-
import { resolveGitHubCredentials, resolveJiraCredentials } from './config/credentials.js';
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
6
5
|
import { readTeamConfig, writeTeamConfig, teamConfigExists, createDefaultConfig, } from './config/team-config.js';
|
|
7
6
|
import { resolveConfigDir } from './config/paths.js';
|
|
8
7
|
import { GitHubClient } from './clients/github-client.js';
|
|
9
8
|
import { JiraClient } from './clients/jira-client.js';
|
|
9
|
+
import { resolveGitHubAuth, resolveJiraAuth, getAuthStatuses, AuthExpiredError, } from './auth/auth-provider.js';
|
|
10
10
|
import { discoverGitHubTeam } from './discovery/github-discovery.js';
|
|
11
11
|
import { discoverJiraTeam } from './discovery/jira-discovery.js';
|
|
12
12
|
import { fetchGitHubActivityForUser, fetchTeamGitHubActivity, fetchJiraActivityForUser, fetchTeamJiraActivity, fetchSprintJiraActivity, detectActiveSprint, } from './orchestrator/data-fetcher.js';
|
|
13
13
|
import { dailyTimeRange, weeklyTimeRange, sprintTimeRange } from './orchestrator/time-range.js';
|
|
14
|
-
import { generateDailyReport, generateTeamDailyReport, generateWeeklyReport, generateRetrospective, } from './generators/report-generator.js';
|
|
14
|
+
import { generateDailyReport, generateTeamDailyReport, generateWeeklyReport, generateRetrospective, generateSprintReview, } from './generators/report-generator.js';
|
|
15
15
|
import { HistoryStore } from './history/history-store.js';
|
|
16
16
|
import { buildSnapshot } from './history/snapshot-builder.js';
|
|
17
17
|
import { analyzeTrends } from './insights/trend-analyzer.js';
|
|
18
18
|
import { detectAnomalies } from './insights/anomaly-detector.js';
|
|
19
19
|
import { assessTeamHealth } from './insights/team-health.js';
|
|
20
20
|
import { resolveThresholds } from './config/thresholds.js';
|
|
21
|
-
const
|
|
21
|
+
const SERVER_INSTRUCTIONS = `Prodbeam is an engineering intelligence server that generates reports from GitHub and Jira data.
|
|
22
|
+
|
|
23
|
+
Use prodbeam tools when the user asks about:
|
|
24
|
+
- Daily standups, what they or their team worked on, yesterday's activity → standup or team_standup
|
|
25
|
+
- Weekly engineering summaries, metrics, productivity reports → weekly_summary
|
|
26
|
+
- Sprint retrospectives, retros, sprint reviews, sprint health → sprint_retro or sprint_review
|
|
27
|
+
- Team setup, adding/removing members, configuration → setup_team, add_member, remove_member
|
|
28
|
+
- Refreshing repos/sprints, re-scanning → refresh_config
|
|
29
|
+
- What tools are available, credential status → get_capabilities
|
|
30
|
+
|
|
31
|
+
Common triggers: "standup", "what did I do", "weekly report", "sprint retro", "sprint review", "team activity", "engineering metrics"`;
|
|
32
|
+
const server = new Server({ name: 'prodbeam', version: '2.0.0' }, {
|
|
33
|
+
capabilities: { tools: {}, prompts: {} },
|
|
34
|
+
instructions: SERVER_INSTRUCTIONS,
|
|
35
|
+
});
|
|
36
|
+
async function createGitHubClient() {
|
|
37
|
+
const auth = await resolveGitHubAuth();
|
|
38
|
+
if (!auth) {
|
|
39
|
+
throw new Error('GitHub credentials required. Set GITHUB_TOKEN or run "prodbeam auth login".');
|
|
40
|
+
}
|
|
41
|
+
return new GitHubClient(auth.token);
|
|
42
|
+
}
|
|
43
|
+
async function createJiraClient() {
|
|
44
|
+
const auth = await resolveJiraAuth();
|
|
45
|
+
if (!auth)
|
|
46
|
+
return null;
|
|
47
|
+
return new JiraClient({
|
|
48
|
+
getBaseUrl: () => auth.baseUrl,
|
|
49
|
+
getAuthHeader: () => Promise.resolve(auth.authHeader),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
22
52
|
server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
23
53
|
return {
|
|
24
54
|
tools: [
|
|
@@ -79,7 +109,7 @@ server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
|
79
109
|
},
|
|
80
110
|
{
|
|
81
111
|
name: 'standup',
|
|
82
|
-
description: 'Generate a personal daily standup report. Fetches your GitHub commits, PRs, reviews, and Jira issues from the last 24 hours. Requires team setup.',
|
|
112
|
+
description: 'Generate a personal daily standup report. Fetches your GitHub commits, PRs, reviews, and Jira issues from the last 24 hours. Use when the user asks: "standup", "what did I work on", "my activity", "daily update". Requires team setup.',
|
|
83
113
|
inputSchema: {
|
|
84
114
|
type: 'object',
|
|
85
115
|
properties: {
|
|
@@ -92,7 +122,7 @@ server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
|
92
122
|
},
|
|
93
123
|
{
|
|
94
124
|
name: 'team_standup',
|
|
95
|
-
description: 'Generate a full team standup report. Shows per-member activity from the last 24 hours with aggregate stats.',
|
|
125
|
+
description: 'Generate a full team standup report. Shows per-member activity from the last 24 hours with aggregate stats. Use when the user asks: "team standup", "what did the team do", "team activity", "everyone\'s update".',
|
|
96
126
|
inputSchema: {
|
|
97
127
|
type: 'object',
|
|
98
128
|
properties: {},
|
|
@@ -100,7 +130,7 @@ server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
|
100
130
|
},
|
|
101
131
|
{
|
|
102
132
|
name: 'weekly_summary',
|
|
103
|
-
description: 'Generate a weekly engineering summary with metrics, repo breakdown, and Jira stats. Covers the last 7 days by default.',
|
|
133
|
+
description: 'Generate a weekly engineering summary with metrics, repo breakdown, and Jira stats. Covers the last 7 days by default. Use when the user asks: "weekly summary", "weekly report", "this week\'s metrics", "engineering summary", "productivity report".',
|
|
104
134
|
inputSchema: {
|
|
105
135
|
type: 'object',
|
|
106
136
|
properties: {
|
|
@@ -113,7 +143,20 @@ server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
|
113
143
|
},
|
|
114
144
|
{
|
|
115
145
|
name: 'sprint_retro',
|
|
116
|
-
description: 'Generate a sprint retrospective report with merge time analysis, completion rates, and Jira metrics. Auto-detects the active sprint from Jira.',
|
|
146
|
+
description: 'Generate a sprint retrospective report with merge time analysis, completion rates, and Jira metrics. Auto-detects the active sprint from Jira. Use when the user asks: "sprint retro", "retrospective", "sprint review meeting", "how did the sprint go".',
|
|
147
|
+
inputSchema: {
|
|
148
|
+
type: 'object',
|
|
149
|
+
properties: {
|
|
150
|
+
sprintName: {
|
|
151
|
+
type: 'string',
|
|
152
|
+
description: 'Sprint name (optional — auto-detects active sprint if not provided)',
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: 'sprint_review',
|
|
159
|
+
description: 'Review current sprint progress with deliverables, risks, and developer status. Mid-sprint health check. Use when the user asks: "sprint review", "sprint status", "sprint health", "how is the sprint going", "sprint progress".',
|
|
117
160
|
inputSchema: {
|
|
118
161
|
type: 'object',
|
|
119
162
|
properties: {
|
|
@@ -135,6 +178,98 @@ server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
|
135
178
|
],
|
|
136
179
|
};
|
|
137
180
|
});
|
|
181
|
+
const PROMPTS = [
|
|
182
|
+
{
|
|
183
|
+
name: 'standup',
|
|
184
|
+
description: 'Generate your personal daily standup report',
|
|
185
|
+
arguments: [
|
|
186
|
+
{
|
|
187
|
+
name: 'email',
|
|
188
|
+
description: 'Team member email (optional — defaults to first member)',
|
|
189
|
+
required: false,
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: 'team-standup',
|
|
195
|
+
description: "Generate the full team's daily standup report",
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: 'weekly-summary',
|
|
199
|
+
description: 'Generate a weekly engineering summary with metrics',
|
|
200
|
+
arguments: [
|
|
201
|
+
{
|
|
202
|
+
name: 'weeksAgo',
|
|
203
|
+
description: 'Offset in weeks (0 = current, 1 = last week)',
|
|
204
|
+
required: false,
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
name: 'sprint-retro',
|
|
210
|
+
description: 'Generate a sprint retrospective with what went well, improvements, and action items',
|
|
211
|
+
arguments: [
|
|
212
|
+
{
|
|
213
|
+
name: 'sprintName',
|
|
214
|
+
description: 'Sprint name (optional — auto-detects active sprint)',
|
|
215
|
+
required: false,
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: 'sprint-review',
|
|
221
|
+
description: 'Review current sprint progress, deliverables, and risks',
|
|
222
|
+
arguments: [
|
|
223
|
+
{
|
|
224
|
+
name: 'sprintName',
|
|
225
|
+
description: 'Sprint name (optional — auto-detects active sprint)',
|
|
226
|
+
required: false,
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
];
|
|
231
|
+
server.setRequestHandler(ListPromptsRequestSchema, () => {
|
|
232
|
+
return { prompts: PROMPTS };
|
|
233
|
+
});
|
|
234
|
+
server.setRequestHandler(GetPromptRequestSchema, (request) => {
|
|
235
|
+
const { name, arguments: promptArgs } = request.params;
|
|
236
|
+
const prompt = PROMPTS.find((p) => p.name === name);
|
|
237
|
+
if (!prompt) {
|
|
238
|
+
throw new Error(`Unknown prompt: ${name}`);
|
|
239
|
+
}
|
|
240
|
+
const toolMap = {
|
|
241
|
+
standup: { tool: 'standup', argMap: { email: 'email' } },
|
|
242
|
+
'team-standup': { tool: 'team_standup', argMap: {} },
|
|
243
|
+
'weekly-summary': { tool: 'weekly_summary', argMap: { weeksAgo: 'weeksAgo' } },
|
|
244
|
+
'sprint-retro': { tool: 'sprint_retro', argMap: { sprintName: 'sprintName' } },
|
|
245
|
+
'sprint-review': { tool: 'sprint_review', argMap: { sprintName: 'sprintName' } },
|
|
246
|
+
};
|
|
247
|
+
const mapping = toolMap[name];
|
|
248
|
+
if (!mapping) {
|
|
249
|
+
throw new Error(`Unknown prompt: ${name}`);
|
|
250
|
+
}
|
|
251
|
+
const toolArgs = {};
|
|
252
|
+
if (promptArgs) {
|
|
253
|
+
for (const [promptKey, toolKey] of Object.entries(mapping.argMap)) {
|
|
254
|
+
if (promptArgs[promptKey]) {
|
|
255
|
+
toolArgs[toolKey] = promptArgs[promptKey];
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const argsDescription = Object.keys(toolArgs).length > 0 ? ` with ${JSON.stringify(toolArgs)}` : '';
|
|
260
|
+
return {
|
|
261
|
+
description: prompt.description,
|
|
262
|
+
messages: [
|
|
263
|
+
{
|
|
264
|
+
role: 'user',
|
|
265
|
+
content: {
|
|
266
|
+
type: 'text',
|
|
267
|
+
text: `Use the prodbeam ${mapping.tool} tool${argsDescription} to generate the report.`,
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
};
|
|
272
|
+
});
|
|
138
273
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
139
274
|
const { name, arguments: args } = request.params;
|
|
140
275
|
try {
|
|
@@ -155,6 +290,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
155
290
|
return await handleWeeklySummary(args);
|
|
156
291
|
case 'sprint_retro':
|
|
157
292
|
return await handleSprintRetro(args);
|
|
293
|
+
case 'sprint_review':
|
|
294
|
+
return await handleSprintReview(args);
|
|
158
295
|
case 'get_capabilities':
|
|
159
296
|
return handleGetCapabilities();
|
|
160
297
|
default:
|
|
@@ -162,6 +299,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
162
299
|
}
|
|
163
300
|
}
|
|
164
301
|
catch (error) {
|
|
302
|
+
if (error instanceof AuthExpiredError) {
|
|
303
|
+
return {
|
|
304
|
+
content: [{ type: 'text', text: `Authentication expired: ${error.message}` }],
|
|
305
|
+
isError: true,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
165
308
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
166
309
|
return {
|
|
167
310
|
content: [{ type: 'text', text: `Error: ${errorMessage}` }],
|
|
@@ -189,11 +332,10 @@ async function handleSetupTeam(args) {
|
|
|
189
332
|
parts.push(`**Team:** ${teamName}`);
|
|
190
333
|
parts.push(`**Config directory:** ${resolveConfigDir()}`);
|
|
191
334
|
parts.push('');
|
|
192
|
-
const
|
|
193
|
-
if (
|
|
335
|
+
const ghClient = await createGitHubClient().catch(() => null);
|
|
336
|
+
if (ghClient) {
|
|
194
337
|
parts.push('## GitHub Discovery');
|
|
195
338
|
parts.push('');
|
|
196
|
-
const ghClient = new GitHubClient(ghCreds.token);
|
|
197
339
|
const ghResult = await discoverGitHubTeam(ghClient, emailList);
|
|
198
340
|
for (const member of ghResult.members) {
|
|
199
341
|
const configMember = config.team.members.find((m) => m.email === member.email);
|
|
@@ -222,15 +364,15 @@ async function handleSetupTeam(args) {
|
|
|
222
364
|
else {
|
|
223
365
|
parts.push('## GitHub Discovery');
|
|
224
366
|
parts.push('');
|
|
225
|
-
parts.push('⚠️ No GitHub credentials found. Set `GITHUB_TOKEN` env var or run
|
|
367
|
+
parts.push('⚠️ No GitHub credentials found. Set `GITHUB_TOKEN` env var or run `prodbeam auth login`.');
|
|
226
368
|
parts.push('');
|
|
227
369
|
}
|
|
228
|
-
const
|
|
229
|
-
|
|
370
|
+
const setupJiraClient = await createJiraClient().catch(() => null);
|
|
371
|
+
const setupJiraAuth = setupJiraClient ? await resolveJiraAuth().catch(() => null) : null;
|
|
372
|
+
if (setupJiraClient) {
|
|
230
373
|
parts.push('## Jira Discovery');
|
|
231
374
|
parts.push('');
|
|
232
|
-
const
|
|
233
|
-
const jiraResult = await discoverJiraTeam(jiraClient, emailList, jiraCreds.host);
|
|
375
|
+
const jiraResult = await discoverJiraTeam(setupJiraClient, emailList, setupJiraAuth?.baseUrl ?? '');
|
|
234
376
|
config.jira.host = jiraResult.host;
|
|
235
377
|
for (const member of jiraResult.members) {
|
|
236
378
|
const configMember = config.team.members.find((m) => m.email === member.email);
|
|
@@ -263,7 +405,7 @@ async function handleSetupTeam(args) {
|
|
|
263
405
|
else {
|
|
264
406
|
parts.push('## Jira Discovery');
|
|
265
407
|
parts.push('');
|
|
266
|
-
parts.push('⚠️ No Jira credentials found. Set
|
|
408
|
+
parts.push('⚠️ No Jira credentials found. Set Jira env vars or run `prodbeam auth login`.');
|
|
267
409
|
parts.push('');
|
|
268
410
|
}
|
|
269
411
|
writeTeamConfig(config);
|
|
@@ -293,15 +435,14 @@ async function handleAddMember(args) {
|
|
|
293
435
|
const parts = [];
|
|
294
436
|
parts.push(`# Add Member: ${email}`);
|
|
295
437
|
parts.push('');
|
|
296
|
-
const
|
|
297
|
-
if (
|
|
298
|
-
const
|
|
299
|
-
const username = await ghClient.searchUserByEmail(email);
|
|
438
|
+
const addGhClient = await createGitHubClient().catch(() => null);
|
|
439
|
+
if (addGhClient) {
|
|
440
|
+
const username = await addGhClient.searchUserByEmail(email);
|
|
300
441
|
if (username) {
|
|
301
442
|
newMember.github = username;
|
|
302
443
|
newMember.name = username;
|
|
303
444
|
parts.push(`GitHub: @${username}`);
|
|
304
|
-
const repos = await
|
|
445
|
+
const repos = await addGhClient.getRecentRepos(username).catch(() => []);
|
|
305
446
|
const newRepos = repos.filter((r) => !config.github.repos.includes(r));
|
|
306
447
|
if (newRepos.length > 0) {
|
|
307
448
|
config.github.repos.push(...newRepos);
|
|
@@ -312,10 +453,9 @@ async function handleAddMember(args) {
|
|
|
312
453
|
parts.push(`GitHub: not found for ${email}`);
|
|
313
454
|
}
|
|
314
455
|
}
|
|
315
|
-
const
|
|
316
|
-
if (
|
|
317
|
-
const
|
|
318
|
-
const user = await jiraClient.searchUserByEmail(email);
|
|
456
|
+
const addJiraClient = await createJiraClient().catch(() => null);
|
|
457
|
+
if (addJiraClient) {
|
|
458
|
+
const user = await addJiraClient.searchUserByEmail(email);
|
|
319
459
|
if (user) {
|
|
320
460
|
newMember.jiraAccountId = user.accountId;
|
|
321
461
|
if (!newMember.name) {
|
|
@@ -371,10 +511,9 @@ async function handleRefreshConfig() {
|
|
|
371
511
|
parts.push('# Config Refresh');
|
|
372
512
|
parts.push('');
|
|
373
513
|
const emails = config.team.members.map((m) => m.email);
|
|
374
|
-
const
|
|
375
|
-
if (
|
|
376
|
-
const
|
|
377
|
-
const ghResult = await discoverGitHubTeam(ghClient, emails);
|
|
514
|
+
const refreshGhClient = await createGitHubClient().catch(() => null);
|
|
515
|
+
if (refreshGhClient) {
|
|
516
|
+
const ghResult = await discoverGitHubTeam(refreshGhClient, emails);
|
|
378
517
|
for (const member of ghResult.members) {
|
|
379
518
|
const configMember = config.team.members.find((m) => m.email === member.email);
|
|
380
519
|
if (configMember && member.username) {
|
|
@@ -394,10 +533,10 @@ async function handleRefreshConfig() {
|
|
|
394
533
|
config.github.org = ghResult.orgs[0];
|
|
395
534
|
}
|
|
396
535
|
}
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
const jiraResult = await discoverJiraTeam(
|
|
536
|
+
const refreshJiraClient = await createJiraClient().catch(() => null);
|
|
537
|
+
const refreshJiraAuth = refreshJiraClient ? await resolveJiraAuth().catch(() => null) : null;
|
|
538
|
+
if (refreshJiraClient) {
|
|
539
|
+
const jiraResult = await discoverJiraTeam(refreshJiraClient, emails, refreshJiraAuth?.baseUrl ?? '');
|
|
401
540
|
for (const member of jiraResult.members) {
|
|
402
541
|
const configMember = config.team.members.find((m) => m.email === member.email);
|
|
403
542
|
if (configMember && member.accountId) {
|
|
@@ -429,43 +568,31 @@ async function handleStandup(args) {
|
|
|
429
568
|
throw new Error(`No GitHub username for ${member.email}. Run refresh_config to re-discover.`);
|
|
430
569
|
}
|
|
431
570
|
const timeRange = dailyTimeRange();
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
throw new Error('GitHub credentials required. Set GITHUB_TOKEN env var.');
|
|
435
|
-
}
|
|
436
|
-
const ghClient = new GitHubClient(ghCreds.token);
|
|
437
|
-
const github = await fetchGitHubActivityForUser(ghClient, member.github, config.github.repos, timeRange);
|
|
571
|
+
const standupGhClient = await createGitHubClient();
|
|
572
|
+
const github = await fetchGitHubActivityForUser(standupGhClient, member.github, config.github.repos, timeRange);
|
|
438
573
|
let jira;
|
|
439
|
-
const
|
|
440
|
-
if (
|
|
441
|
-
|
|
442
|
-
jira = await fetchJiraActivityForUser(jiraClient, member.jiraAccountId, config.jira.projects, timeRange);
|
|
574
|
+
const standupJiraClient = await createJiraClient().catch(() => null);
|
|
575
|
+
if (standupJiraClient && member.jiraAccountId) {
|
|
576
|
+
jira = await fetchJiraActivityForUser(standupJiraClient, member.jiraAccountId, config.jira.projects, timeRange);
|
|
443
577
|
}
|
|
444
578
|
const report = generateDailyReport({ github, jira });
|
|
445
579
|
return { content: [{ type: 'text', text: report }] };
|
|
446
580
|
}
|
|
447
581
|
async function handleTeamStandup() {
|
|
448
582
|
const config = requireTeamConfig();
|
|
449
|
-
const ghCreds = resolveGitHubCredentials();
|
|
450
|
-
if (!ghCreds) {
|
|
451
|
-
throw new Error('GitHub credentials required. Set GITHUB_TOKEN env var.');
|
|
452
|
-
}
|
|
453
583
|
const timeRange = dailyTimeRange();
|
|
454
|
-
const
|
|
455
|
-
const
|
|
456
|
-
const jiraClient = jiraCreds
|
|
457
|
-
? new JiraClient(jiraCreds.host, jiraCreds.email, jiraCreds.apiToken)
|
|
458
|
-
: null;
|
|
584
|
+
const teamGhClient = await createGitHubClient();
|
|
585
|
+
const teamJiraClient = await createJiraClient().catch(() => null);
|
|
459
586
|
const membersWithGH = config.team.members.filter((m) => m.github);
|
|
460
587
|
const usernames = membersWithGH.map((m) => m.github);
|
|
461
|
-
const ghActivities = await fetchTeamGitHubActivity(
|
|
588
|
+
const ghActivities = await fetchTeamGitHubActivity(teamGhClient, usernames, config.github.repos, timeRange);
|
|
462
589
|
const memberActivities = [];
|
|
463
590
|
for (let i = 0; i < membersWithGH.length; i++) {
|
|
464
591
|
const member = membersWithGH[i];
|
|
465
592
|
const github = ghActivities[i];
|
|
466
593
|
let jira;
|
|
467
|
-
if (
|
|
468
|
-
jira = await fetchJiraActivityForUser(
|
|
594
|
+
if (teamJiraClient && member.jiraAccountId) {
|
|
595
|
+
jira = await fetchJiraActivityForUser(teamJiraClient, member.jiraAccountId, config.jira.projects, timeRange);
|
|
469
596
|
}
|
|
470
597
|
memberActivities.push({ github, jira });
|
|
471
598
|
}
|
|
@@ -474,22 +601,17 @@ async function handleTeamStandup() {
|
|
|
474
601
|
}
|
|
475
602
|
async function handleWeeklySummary(args) {
|
|
476
603
|
const config = requireTeamConfig();
|
|
477
|
-
const ghCreds = resolveGitHubCredentials();
|
|
478
|
-
if (!ghCreds) {
|
|
479
|
-
throw new Error('GitHub credentials required. Set GITHUB_TOKEN env var.');
|
|
480
|
-
}
|
|
481
604
|
const weeksAgo = typeof args?.['weeksAgo'] === 'number' ? args['weeksAgo'] : 0;
|
|
482
605
|
const timeRange = weeklyTimeRange(weeksAgo);
|
|
483
|
-
const
|
|
606
|
+
const weeklyGhClient = await createGitHubClient();
|
|
484
607
|
const membersWithGH = config.team.members.filter((m) => m.github);
|
|
485
608
|
const usernames = membersWithGH.map((m) => m.github);
|
|
486
|
-
const perMember = await fetchTeamGitHubActivity(
|
|
609
|
+
const perMember = await fetchTeamGitHubActivity(weeklyGhClient, usernames, config.github.repos, timeRange);
|
|
487
610
|
const github = mergeGitHubActivities(perMember, config.team.name, timeRange);
|
|
488
611
|
let jira;
|
|
489
|
-
const
|
|
490
|
-
if (
|
|
491
|
-
|
|
492
|
-
jira = await fetchTeamJiraActivity(jiraClient, config.jira.projects, timeRange);
|
|
612
|
+
const weeklyJiraClient = await createJiraClient().catch(() => null);
|
|
613
|
+
if (weeklyJiraClient) {
|
|
614
|
+
jira = await fetchTeamJiraActivity(weeklyJiraClient, config.jira.projects, timeRange);
|
|
493
615
|
}
|
|
494
616
|
const snapshot = buildSnapshot({
|
|
495
617
|
teamName: config.team.name,
|
|
@@ -524,23 +646,21 @@ async function handleWeeklySummary(args) {
|
|
|
524
646
|
}
|
|
525
647
|
catch {
|
|
526
648
|
}
|
|
527
|
-
const report = generateWeeklyReport({ github, jira }, extras);
|
|
649
|
+
const report = generateWeeklyReport({ github, jira, perMember }, extras);
|
|
528
650
|
return { content: [{ type: 'text', text: report }] };
|
|
529
651
|
}
|
|
530
652
|
async function handleSprintRetro(args) {
|
|
531
653
|
const config = requireTeamConfig();
|
|
532
|
-
const
|
|
533
|
-
const
|
|
534
|
-
if (!ghCreds) {
|
|
535
|
-
throw new Error('GitHub credentials required. Set GITHUB_TOKEN env var.');
|
|
536
|
-
}
|
|
654
|
+
const retroGhClient = await createGitHubClient();
|
|
655
|
+
const retroJiraClient = await createJiraClient().catch(() => null);
|
|
537
656
|
let sprintName = typeof args?.['sprintName'] === 'string' ? args['sprintName'] : undefined;
|
|
538
657
|
let timeRange;
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
const activeSprint = await detectActiveSprint(
|
|
658
|
+
let sprintGoal;
|
|
659
|
+
if (!sprintName && retroJiraClient) {
|
|
660
|
+
const activeSprint = await detectActiveSprint(retroJiraClient, config.jira.projects);
|
|
542
661
|
if (activeSprint) {
|
|
543
662
|
sprintName = activeSprint.name;
|
|
663
|
+
sprintGoal = activeSprint.goal;
|
|
544
664
|
timeRange = sprintTimeRange(activeSprint.startDate, activeSprint.endDate);
|
|
545
665
|
}
|
|
546
666
|
}
|
|
@@ -552,15 +672,13 @@ async function handleSprintRetro(args) {
|
|
|
552
672
|
const from = new Date(new Date(timeRange.to).getTime() - 14 * 24 * 60 * 60 * 1000);
|
|
553
673
|
timeRange = { from: from.toISOString(), to: timeRange.to };
|
|
554
674
|
}
|
|
555
|
-
const
|
|
556
|
-
const
|
|
557
|
-
const
|
|
558
|
-
const perMember = await fetchTeamGitHubActivity(ghClient, usernames, config.github.repos, timeRange);
|
|
675
|
+
const retroMembersWithGH = config.team.members.filter((m) => m.github);
|
|
676
|
+
const retroUsernames = retroMembersWithGH.map((m) => m.github);
|
|
677
|
+
const perMember = await fetchTeamGitHubActivity(retroGhClient, retroUsernames, config.github.repos, timeRange);
|
|
559
678
|
const github = mergeGitHubActivities(perMember, config.team.name, timeRange);
|
|
560
679
|
let jira;
|
|
561
|
-
if (
|
|
562
|
-
|
|
563
|
-
jira = await fetchSprintJiraActivity(jiraClient, sprintName, timeRange);
|
|
680
|
+
if (retroJiraClient) {
|
|
681
|
+
jira = await fetchSprintJiraActivity(retroJiraClient, sprintName, timeRange);
|
|
564
682
|
}
|
|
565
683
|
const dateRange = {
|
|
566
684
|
from: timeRange.from.split('T')[0],
|
|
@@ -600,7 +718,69 @@ async function handleSprintRetro(args) {
|
|
|
600
718
|
}
|
|
601
719
|
catch {
|
|
602
720
|
}
|
|
603
|
-
const report = generateRetrospective({ github, jira, sprintName, dateRange }, extras);
|
|
721
|
+
const report = generateRetrospective({ github, jira, sprintName, dateRange, sprintGoal, perMember }, extras);
|
|
722
|
+
return { content: [{ type: 'text', text: report }] };
|
|
723
|
+
}
|
|
724
|
+
async function handleSprintReview(args) {
|
|
725
|
+
const config = requireTeamConfig();
|
|
726
|
+
const reviewGhClient = await createGitHubClient();
|
|
727
|
+
const reviewJiraClient = await createJiraClient().catch(() => null);
|
|
728
|
+
let sprintName = typeof args?.['sprintName'] === 'string' ? args['sprintName'] : undefined;
|
|
729
|
+
let timeRange;
|
|
730
|
+
let sprintGoal;
|
|
731
|
+
let sprintStartDate;
|
|
732
|
+
let sprintEndDate;
|
|
733
|
+
if (!sprintName && reviewJiraClient) {
|
|
734
|
+
const activeSprint = await detectActiveSprint(reviewJiraClient, config.jira.projects);
|
|
735
|
+
if (activeSprint) {
|
|
736
|
+
sprintName = activeSprint.name;
|
|
737
|
+
sprintGoal = activeSprint.goal;
|
|
738
|
+
sprintStartDate = activeSprint.startDate;
|
|
739
|
+
sprintEndDate = activeSprint.endDate;
|
|
740
|
+
timeRange = sprintTimeRange(activeSprint.startDate, activeSprint.endDate);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (!sprintName) {
|
|
744
|
+
throw new Error('No active sprint detected. Provide a sprintName parameter or ensure Jira credentials are configured.');
|
|
745
|
+
}
|
|
746
|
+
if (!timeRange) {
|
|
747
|
+
timeRange = weeklyTimeRange(0);
|
|
748
|
+
const from = new Date(new Date(timeRange.to).getTime() - 14 * 24 * 60 * 60 * 1000);
|
|
749
|
+
timeRange = { from: from.toISOString(), to: timeRange.to };
|
|
750
|
+
}
|
|
751
|
+
const reviewMembersWithGH = config.team.members.filter((m) => m.github);
|
|
752
|
+
const reviewUsernames = reviewMembersWithGH.map((m) => m.github);
|
|
753
|
+
const perMember = await fetchTeamGitHubActivity(reviewGhClient, reviewUsernames, config.github.repos, timeRange);
|
|
754
|
+
const github = mergeGitHubActivities(perMember, config.team.name, timeRange);
|
|
755
|
+
let jira;
|
|
756
|
+
if (reviewJiraClient) {
|
|
757
|
+
jira = await fetchSprintJiraActivity(reviewJiraClient, sprintName, timeRange);
|
|
758
|
+
}
|
|
759
|
+
const dateRange = {
|
|
760
|
+
from: timeRange.from.split('T')[0],
|
|
761
|
+
to: timeRange.to.split('T')[0],
|
|
762
|
+
};
|
|
763
|
+
const now = new Date();
|
|
764
|
+
const start = sprintStartDate ? new Date(sprintStartDate) : new Date(timeRange.from);
|
|
765
|
+
const end = sprintEndDate ? new Date(sprintEndDate) : new Date(timeRange.to);
|
|
766
|
+
const daysElapsed = Math.max(0, Math.floor((now.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)));
|
|
767
|
+
const daysTotal = Math.max(1, Math.floor((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)));
|
|
768
|
+
const thresholds = resolveThresholds(config.settings.thresholds);
|
|
769
|
+
const extras = {};
|
|
770
|
+
try {
|
|
771
|
+
const store = new HistoryStore();
|
|
772
|
+
extras.anomalies = detectAnomalies({
|
|
773
|
+
pullRequests: github.pullRequests,
|
|
774
|
+
reviews: github.reviews,
|
|
775
|
+
jiraIssues: jira?.issues ?? [],
|
|
776
|
+
memberActivity: buildMemberActivity(perMember),
|
|
777
|
+
thresholds,
|
|
778
|
+
});
|
|
779
|
+
store.close();
|
|
780
|
+
}
|
|
781
|
+
catch {
|
|
782
|
+
}
|
|
783
|
+
const report = generateSprintReview({ github, jira, sprintName, dateRange, sprintGoal, perMember, daysElapsed, daysTotal }, extras);
|
|
604
784
|
return { content: [{ type: 'text', text: report }] };
|
|
605
785
|
}
|
|
606
786
|
function requireTeamConfig() {
|
|
@@ -658,8 +838,9 @@ function buildMemberSnapshots(perMember) {
|
|
|
658
838
|
function handleGetCapabilities() {
|
|
659
839
|
const configExists = teamConfigExists();
|
|
660
840
|
const config = configExists ? readTeamConfig() : null;
|
|
661
|
-
const
|
|
662
|
-
const
|
|
841
|
+
const authStatuses = getAuthStatuses();
|
|
842
|
+
const ghStatus = authStatuses.find((s) => s.service === 'github');
|
|
843
|
+
const jiraStatus = authStatuses.find((s) => s.service === 'jira');
|
|
663
844
|
const parts = [];
|
|
664
845
|
parts.push('# Prodbeam MCP v2');
|
|
665
846
|
parts.push('');
|
|
@@ -669,13 +850,17 @@ function handleGetCapabilities() {
|
|
|
669
850
|
parts.push(`|-----------|--------|`);
|
|
670
851
|
parts.push(`| Config directory | \`${resolveConfigDir()}\` |`);
|
|
671
852
|
parts.push(`| Team config | ${configExists ? `✅ ${config?.team.name} (${config?.team.members.length} members)` : '❌ Not configured'} |`);
|
|
672
|
-
parts.push(`| GitHub
|
|
673
|
-
parts.push(`| Jira
|
|
853
|
+
parts.push(`| GitHub | ${ghStatus?.valid ? `✅ ${ghStatus.method === 'oauth' ? 'OAuth (auto-refresh)' : 'Token configured'}` : `❌ ${ghStatus?.error ?? 'Not configured'}`} |`);
|
|
854
|
+
parts.push(`| Jira | ${jiraStatus?.valid ? `✅ ${jiraStatus.method === 'oauth' ? 'OAuth (auto-refresh)' : 'Token configured'}` : `❌ ${jiraStatus?.error ?? 'Not configured (optional)'}`} |`);
|
|
674
855
|
parts.push('');
|
|
675
|
-
if (!
|
|
856
|
+
if (!ghStatus?.valid || !configExists) {
|
|
676
857
|
parts.push('## Getting Started');
|
|
677
858
|
parts.push('');
|
|
678
|
-
renderSetupGuide(parts, {
|
|
859
|
+
renderSetupGuide(parts, {
|
|
860
|
+
ghCreds: !!ghStatus?.valid,
|
|
861
|
+
jiraCreds: !!jiraStatus?.valid,
|
|
862
|
+
configExists,
|
|
863
|
+
});
|
|
679
864
|
}
|
|
680
865
|
if (config) {
|
|
681
866
|
parts.push('## Team');
|
|
@@ -719,6 +904,7 @@ function handleGetCapabilities() {
|
|
|
719
904
|
parts.push('- **team_standup** — Full team standup (last 24h)');
|
|
720
905
|
parts.push('- **weekly_summary** — Week-in-review with metrics');
|
|
721
906
|
parts.push('- **sprint_retro** — Sprint retrospective with completion rates');
|
|
907
|
+
parts.push('- **sprint_review** — Mid-sprint health check with risks and progress');
|
|
722
908
|
return { content: [{ type: 'text', text: parts.join('\n') }] };
|
|
723
909
|
}
|
|
724
910
|
function renderSetupGuide(parts, status) {
|