@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.
Files changed (76) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +626 -67
  3. package/dist/auth/app-config.d.ts +8 -0
  4. package/dist/auth/app-config.d.ts.map +1 -0
  5. package/dist/auth/app-config.js +32 -0
  6. package/dist/auth/app-config.js.map +1 -0
  7. package/dist/auth/auth-provider.d.ts +9 -0
  8. package/dist/auth/auth-provider.d.ts.map +1 -0
  9. package/dist/auth/auth-provider.js +173 -0
  10. package/dist/auth/auth-provider.js.map +1 -0
  11. package/dist/auth/github-device-flow.d.ts +5 -0
  12. package/dist/auth/github-device-flow.d.ts.map +1 -0
  13. package/dist/auth/github-device-flow.js +139 -0
  14. package/dist/auth/github-device-flow.js.map +1 -0
  15. package/dist/auth/jira-oauth-flow.d.ts +19 -0
  16. package/dist/auth/jira-oauth-flow.d.ts.map +1 -0
  17. package/dist/auth/jira-oauth-flow.js +210 -0
  18. package/dist/auth/jira-oauth-flow.js.map +1 -0
  19. package/dist/auth/token-store.d.ts +7 -0
  20. package/dist/auth/token-store.d.ts.map +1 -0
  21. package/dist/auth/token-store.js +74 -0
  22. package/dist/auth/token-store.js.map +1 -0
  23. package/dist/auth/types.d.ts +51 -0
  24. package/dist/auth/types.d.ts.map +1 -0
  25. package/dist/auth/types.js +2 -0
  26. package/dist/auth/types.js.map +1 -0
  27. package/dist/cli.js +403 -64
  28. package/dist/cli.js.map +1 -1
  29. package/dist/clients/github-client.d.ts +2 -2
  30. package/dist/clients/github-client.d.ts.map +1 -1
  31. package/dist/clients/github-client.js +14 -4
  32. package/dist/clients/github-client.js.map +1 -1
  33. package/dist/clients/jira-client.d.ts +9 -3
  34. package/dist/clients/jira-client.d.ts.map +1 -1
  35. package/dist/clients/jira-client.js +53 -10
  36. package/dist/clients/jira-client.js.map +1 -1
  37. package/dist/clients/types.d.ts +21 -0
  38. package/dist/clients/types.d.ts.map +1 -1
  39. package/dist/commands/auth.d.ts +4 -0
  40. package/dist/commands/auth.d.ts.map +1 -0
  41. package/dist/commands/auth.js +254 -0
  42. package/dist/commands/auth.js.map +1 -0
  43. package/dist/commands/init.d.ts.map +1 -1
  44. package/dist/commands/init.js +45 -1
  45. package/dist/commands/init.js.map +1 -1
  46. package/dist/config/credentials.d.ts +2 -0
  47. package/dist/config/credentials.d.ts.map +1 -1
  48. package/dist/config/credentials.js +6 -0
  49. package/dist/config/credentials.js.map +1 -1
  50. package/dist/generators/metrics-calculator.d.ts.map +1 -1
  51. package/dist/generators/metrics-calculator.js +28 -0
  52. package/dist/generators/metrics-calculator.js.map +1 -1
  53. package/dist/generators/report-generator.d.ts +2 -1
  54. package/dist/generators/report-generator.d.ts.map +1 -1
  55. package/dist/generators/report-generator.js +565 -131
  56. package/dist/generators/report-generator.js.map +1 -1
  57. package/dist/index.js +275 -89
  58. package/dist/index.js.map +1 -1
  59. package/dist/insights/content-insights.d.ts +46 -0
  60. package/dist/insights/content-insights.d.ts.map +1 -0
  61. package/dist/insights/content-insights.js +193 -0
  62. package/dist/insights/content-insights.js.map +1 -0
  63. package/dist/orchestrator/data-fetcher.d.ts +3 -1
  64. package/dist/orchestrator/data-fetcher.d.ts.map +1 -1
  65. package/dist/orchestrator/data-fetcher.js +15 -0
  66. package/dist/orchestrator/data-fetcher.js.map +1 -1
  67. package/dist/types/github.d.ts +3 -0
  68. package/dist/types/github.d.ts.map +1 -1
  69. package/dist/types/jira.d.ts +9 -0
  70. package/dist/types/jira.d.ts.map +1 -1
  71. package/dist/types/retrospective.d.ts +15 -0
  72. package/dist/types/retrospective.d.ts.map +1 -1
  73. package/dist/types/weekly.d.ts +7 -0
  74. package/dist/types/weekly.d.ts.map +1 -1
  75. package/dist/validators.d.ts +6 -6
  76. 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 server = new Server({ name: 'prodbeam', version: '2.0.0' }, { capabilities: { tools: {} } });
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 ghCreds = resolveGitHubCredentials();
193
- if (ghCreds) {
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 setup again after configuring credentials.');
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 jiraCreds = resolveJiraCredentials();
229
- if (jiraCreds) {
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 jiraClient = new JiraClient(jiraCreds.host, jiraCreds.email, jiraCreds.apiToken);
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 `JIRA_HOST`, `JIRA_EMAIL`, and `JIRA_API_TOKEN` env vars or configure credentials.');
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 ghCreds = resolveGitHubCredentials();
297
- if (ghCreds) {
298
- const ghClient = new GitHubClient(ghCreds.token);
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 ghClient.getRecentRepos(username).catch(() => []);
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 jiraCreds = resolveJiraCredentials();
316
- if (jiraCreds) {
317
- const jiraClient = new JiraClient(jiraCreds.host, jiraCreds.email, jiraCreds.apiToken);
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 ghCreds = resolveGitHubCredentials();
375
- if (ghCreds) {
376
- const ghClient = new GitHubClient(ghCreds.token);
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 jiraCreds = resolveJiraCredentials();
398
- if (jiraCreds) {
399
- const jiraClient = new JiraClient(jiraCreds.host, jiraCreds.email, jiraCreds.apiToken);
400
- const jiraResult = await discoverJiraTeam(jiraClient, emails, jiraCreds.host);
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 ghCreds = resolveGitHubCredentials();
433
- if (!ghCreds) {
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 jiraCreds = resolveJiraCredentials();
440
- if (jiraCreds && member.jiraAccountId) {
441
- const jiraClient = new JiraClient(jiraCreds.host, jiraCreds.email, jiraCreds.apiToken);
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 ghClient = new GitHubClient(ghCreds.token);
455
- const jiraCreds = resolveJiraCredentials();
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(ghClient, usernames, config.github.repos, timeRange);
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 (jiraClient && member.jiraAccountId) {
468
- jira = await fetchJiraActivityForUser(jiraClient, member.jiraAccountId, config.jira.projects, timeRange);
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 ghClient = new GitHubClient(ghCreds.token);
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(ghClient, usernames, config.github.repos, timeRange);
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 jiraCreds = resolveJiraCredentials();
490
- if (jiraCreds) {
491
- const jiraClient = new JiraClient(jiraCreds.host, jiraCreds.email, jiraCreds.apiToken);
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 ghCreds = resolveGitHubCredentials();
533
- const jiraCreds = resolveJiraCredentials();
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
- if (!sprintName && jiraCreds) {
540
- const jiraClient = new JiraClient(jiraCreds.host, jiraCreds.email, jiraCreds.apiToken);
541
- const activeSprint = await detectActiveSprint(jiraClient, config.jira.projects);
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 ghClient = new GitHubClient(ghCreds.token);
556
- const membersWithGH = config.team.members.filter((m) => m.github);
557
- const usernames = membersWithGH.map((m) => m.github);
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 (jiraCreds) {
562
- const jiraClient = new JiraClient(jiraCreds.host, jiraCreds.email, jiraCreds.apiToken);
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 ghCreds = resolveGitHubCredentials();
662
- const jiraCreds = resolveJiraCredentials();
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 credentials | ${ghCreds ? ' Token configured' : 'Not configured'} |`);
673
- parts.push(`| Jira credentials | ${jiraCreds ? `✅ ${jiraCreds.host}` : 'Not configured (optional)'} |`);
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 (!ghCreds || !configExists) {
856
+ if (!ghStatus?.valid || !configExists) {
676
857
  parts.push('## Getting Started');
677
858
  parts.push('');
678
- renderSetupGuide(parts, { ghCreds: !!ghCreds, jiraCreds: !!jiraCreds, configExists });
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) {