@prodbeam/mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +208 -0
  4. package/dist/CLAUDE.md +7 -0
  5. package/dist/adapters/github-mcp.d.ts +25 -0
  6. package/dist/adapters/github-mcp.d.ts.map +1 -0
  7. package/dist/adapters/github-mcp.js +307 -0
  8. package/dist/adapters/github-mcp.js.map +1 -0
  9. package/dist/adapters/jira-mcp.d.ts +14 -0
  10. package/dist/adapters/jira-mcp.d.ts.map +1 -0
  11. package/dist/adapters/jira-mcp.js +159 -0
  12. package/dist/adapters/jira-mcp.js.map +1 -0
  13. package/dist/cli.d.ts +3 -0
  14. package/dist/cli.d.ts.map +1 -0
  15. package/dist/cli.js +256 -0
  16. package/dist/cli.js.map +1 -0
  17. package/dist/clients/github-client.d.ts +29 -0
  18. package/dist/clients/github-client.d.ts.map +1 -0
  19. package/dist/clients/github-client.js +116 -0
  20. package/dist/clients/github-client.js.map +1 -0
  21. package/dist/clients/jira-client.d.ts +30 -0
  22. package/dist/clients/jira-client.d.ts.map +1 -0
  23. package/dist/clients/jira-client.js +115 -0
  24. package/dist/clients/jira-client.js.map +1 -0
  25. package/dist/clients/types.d.ts +155 -0
  26. package/dist/clients/types.d.ts.map +1 -0
  27. package/dist/clients/types.js +2 -0
  28. package/dist/clients/types.js.map +1 -0
  29. package/dist/commands/init.d.ts +2 -0
  30. package/dist/commands/init.d.ts.map +1 -0
  31. package/dist/commands/init.js +426 -0
  32. package/dist/commands/init.js.map +1 -0
  33. package/dist/commands/prompt.d.ts +18 -0
  34. package/dist/commands/prompt.d.ts.map +1 -0
  35. package/dist/commands/prompt.js +122 -0
  36. package/dist/commands/prompt.js.map +1 -0
  37. package/dist/config/credentials.d.ts +9 -0
  38. package/dist/config/credentials.d.ts.map +1 -0
  39. package/dist/config/credentials.js +87 -0
  40. package/dist/config/credentials.js.map +1 -0
  41. package/dist/config/paths.d.ts +6 -0
  42. package/dist/config/paths.d.ts.map +1 -0
  43. package/dist/config/paths.js +31 -0
  44. package/dist/config/paths.js.map +1 -0
  45. package/dist/config/team-config.d.ts +204 -0
  46. package/dist/config/team-config.d.ts.map +1 -0
  47. package/dist/config/team-config.js +86 -0
  48. package/dist/config/team-config.js.map +1 -0
  49. package/dist/config/thresholds.d.ts +15 -0
  50. package/dist/config/thresholds.d.ts.map +1 -0
  51. package/dist/config/thresholds.js +18 -0
  52. package/dist/config/thresholds.js.map +1 -0
  53. package/dist/config/types.d.ts +42 -0
  54. package/dist/config/types.d.ts.map +1 -0
  55. package/dist/config/types.js +2 -0
  56. package/dist/config/types.js.map +1 -0
  57. package/dist/discovery/github-discovery.d.ts +4 -0
  58. package/dist/discovery/github-discovery.d.ts.map +1 -0
  59. package/dist/discovery/github-discovery.js +46 -0
  60. package/dist/discovery/github-discovery.js.map +1 -0
  61. package/dist/discovery/jira-discovery.d.ts +4 -0
  62. package/dist/discovery/jira-discovery.d.ts.map +1 -0
  63. package/dist/discovery/jira-discovery.js +68 -0
  64. package/dist/discovery/jira-discovery.js.map +1 -0
  65. package/dist/discovery/types.d.ts +40 -0
  66. package/dist/discovery/types.d.ts.map +1 -0
  67. package/dist/discovery/types.js +2 -0
  68. package/dist/discovery/types.js.map +1 -0
  69. package/dist/generators/metrics-calculator.d.ts +5 -0
  70. package/dist/generators/metrics-calculator.d.ts.map +1 -0
  71. package/dist/generators/metrics-calculator.js +101 -0
  72. package/dist/generators/metrics-calculator.js.map +1 -0
  73. package/dist/generators/report-generator.d.ts +25 -0
  74. package/dist/generators/report-generator.d.ts.map +1 -0
  75. package/dist/generators/report-generator.js +375 -0
  76. package/dist/generators/report-generator.js.map +1 -0
  77. package/dist/generators/sprint-analyzer.d.ts +5 -0
  78. package/dist/generators/sprint-analyzer.d.ts.map +1 -0
  79. package/dist/generators/sprint-analyzer.js +88 -0
  80. package/dist/generators/sprint-analyzer.js.map +1 -0
  81. package/dist/history/history-store.d.ts +15 -0
  82. package/dist/history/history-store.d.ts.map +1 -0
  83. package/dist/history/history-store.js +197 -0
  84. package/dist/history/history-store.js.map +1 -0
  85. package/dist/history/snapshot-builder.d.ts +15 -0
  86. package/dist/history/snapshot-builder.d.ts.map +1 -0
  87. package/dist/history/snapshot-builder.js +45 -0
  88. package/dist/history/snapshot-builder.js.map +1 -0
  89. package/dist/history/types.d.ts +33 -0
  90. package/dist/history/types.d.ts.map +1 -0
  91. package/dist/history/types.js +2 -0
  92. package/dist/history/types.js.map +1 -0
  93. package/dist/index.d.ts +3 -0
  94. package/dist/index.d.ts.map +1 -0
  95. package/dist/index.js +797 -0
  96. package/dist/index.js.map +1 -0
  97. package/dist/insights/anomaly-detector.d.ts +28 -0
  98. package/dist/insights/anomaly-detector.d.ts.map +1 -0
  99. package/dist/insights/anomaly-detector.js +154 -0
  100. package/dist/insights/anomaly-detector.js.map +1 -0
  101. package/dist/insights/team-health.d.ts +24 -0
  102. package/dist/insights/team-health.d.ts.map +1 -0
  103. package/dist/insights/team-health.js +151 -0
  104. package/dist/insights/team-health.js.map +1 -0
  105. package/dist/insights/trend-analyzer.d.ts +5 -0
  106. package/dist/insights/trend-analyzer.d.ts.map +1 -0
  107. package/dist/insights/trend-analyzer.js +79 -0
  108. package/dist/insights/trend-analyzer.js.map +1 -0
  109. package/dist/insights/types.d.ts +10 -0
  110. package/dist/insights/types.d.ts.map +1 -0
  111. package/dist/insights/types.js +2 -0
  112. package/dist/insights/types.js.map +1 -0
  113. package/dist/orchestrator/data-fetcher.d.ts +16 -0
  114. package/dist/orchestrator/data-fetcher.d.ts.map +1 -0
  115. package/dist/orchestrator/data-fetcher.js +169 -0
  116. package/dist/orchestrator/data-fetcher.js.map +1 -0
  117. package/dist/orchestrator/time-range.d.ts +8 -0
  118. package/dist/orchestrator/time-range.d.ts.map +1 -0
  119. package/dist/orchestrator/time-range.js +27 -0
  120. package/dist/orchestrator/time-range.js.map +1 -0
  121. package/dist/types/github.d.ts +40 -0
  122. package/dist/types/github.d.ts.map +1 -0
  123. package/dist/types/github.js +2 -0
  124. package/dist/types/github.js.map +1 -0
  125. package/dist/types/jira.d.ts +18 -0
  126. package/dist/types/jira.d.ts.map +1 -0
  127. package/dist/types/jira.js +2 -0
  128. package/dist/types/jira.js.map +1 -0
  129. package/dist/types/retrospective.d.ts +38 -0
  130. package/dist/types/retrospective.d.ts.map +1 -0
  131. package/dist/types/retrospective.js +2 -0
  132. package/dist/types/retrospective.js.map +1 -0
  133. package/dist/types/weekly.d.ts +41 -0
  134. package/dist/types/weekly.d.ts.map +1 -0
  135. package/dist/types/weekly.js +2 -0
  136. package/dist/types/weekly.js.map +1 -0
  137. package/dist/validators.d.ts +236 -0
  138. package/dist/validators.d.ts.map +1 -0
  139. package/dist/validators.js +62 -0
  140. package/dist/validators.js.map +1 -0
  141. package/package.json +72 -0
package/dist/index.js ADDED
@@ -0,0 +1,797 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
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';
6
+ import { readTeamConfig, writeTeamConfig, teamConfigExists, createDefaultConfig, } from './config/team-config.js';
7
+ import { resolveConfigDir } from './config/paths.js';
8
+ import { GitHubClient } from './clients/github-client.js';
9
+ import { JiraClient } from './clients/jira-client.js';
10
+ import { discoverGitHubTeam } from './discovery/github-discovery.js';
11
+ import { discoverJiraTeam } from './discovery/jira-discovery.js';
12
+ import { fetchGitHubActivityForUser, fetchTeamGitHubActivity, fetchJiraActivityForUser, fetchTeamJiraActivity, fetchSprintJiraActivity, detectActiveSprint, } from './orchestrator/data-fetcher.js';
13
+ import { dailyTimeRange, weeklyTimeRange, sprintTimeRange } from './orchestrator/time-range.js';
14
+ import { generateDailyReport, generateTeamDailyReport, generateWeeklyReport, generateRetrospective, } from './generators/report-generator.js';
15
+ import { HistoryStore } from './history/history-store.js';
16
+ import { buildSnapshot } from './history/snapshot-builder.js';
17
+ import { analyzeTrends } from './insights/trend-analyzer.js';
18
+ import { detectAnomalies } from './insights/anomaly-detector.js';
19
+ import { assessTeamHealth } from './insights/team-health.js';
20
+ import { resolveThresholds } from './config/thresholds.js';
21
+ const server = new Server({ name: 'prodbeam', version: '2.0.0' }, { capabilities: { tools: {} } });
22
+ server.setRequestHandler(ListToolsRequestSchema, () => {
23
+ return {
24
+ tools: [
25
+ {
26
+ name: 'setup_team',
27
+ description: 'One-time team setup. Provide team name and member emails — prodbeam auto-discovers GitHub usernames, Jira accounts, active repos, projects, and sprints.',
28
+ inputSchema: {
29
+ type: 'object',
30
+ properties: {
31
+ teamName: {
32
+ type: 'string',
33
+ description: 'Team name (e.g., "Platform Engineering")',
34
+ },
35
+ emails: {
36
+ type: 'array',
37
+ items: { type: 'string' },
38
+ description: 'Email addresses of team members',
39
+ },
40
+ },
41
+ required: ['teamName', 'emails'],
42
+ },
43
+ },
44
+ {
45
+ name: 'add_member',
46
+ description: 'Add a new member to the team. Provide their email — prodbeam auto-discovers their GitHub and Jira identities.',
47
+ inputSchema: {
48
+ type: 'object',
49
+ properties: {
50
+ email: {
51
+ type: 'string',
52
+ description: 'Email address of the new team member',
53
+ },
54
+ },
55
+ required: ['email'],
56
+ },
57
+ },
58
+ {
59
+ name: 'remove_member',
60
+ description: 'Remove a member from the team by email address.',
61
+ inputSchema: {
62
+ type: 'object',
63
+ properties: {
64
+ email: {
65
+ type: 'string',
66
+ description: 'Email address of the member to remove',
67
+ },
68
+ },
69
+ required: ['email'],
70
+ },
71
+ },
72
+ {
73
+ name: 'refresh_config',
74
+ description: 'Re-scan repos and sprints for existing team members. Updates team config with newly discovered repos and current sprint info.',
75
+ inputSchema: {
76
+ type: 'object',
77
+ properties: {},
78
+ },
79
+ },
80
+ {
81
+ 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.',
83
+ inputSchema: {
84
+ type: 'object',
85
+ properties: {
86
+ email: {
87
+ type: 'string',
88
+ description: 'Email of the team member (optional — defaults to first member in config)',
89
+ },
90
+ },
91
+ },
92
+ },
93
+ {
94
+ name: 'team_standup',
95
+ description: 'Generate a full team standup report. Shows per-member activity from the last 24 hours with aggregate stats.',
96
+ inputSchema: {
97
+ type: 'object',
98
+ properties: {},
99
+ },
100
+ },
101
+ {
102
+ name: 'weekly_summary',
103
+ description: 'Generate a weekly engineering summary with metrics, repo breakdown, and Jira stats. Covers the last 7 days by default.',
104
+ inputSchema: {
105
+ type: 'object',
106
+ properties: {
107
+ weeksAgo: {
108
+ type: 'number',
109
+ description: 'Offset in weeks (0 = current week, 1 = last week, etc.)',
110
+ },
111
+ },
112
+ },
113
+ },
114
+ {
115
+ 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.',
117
+ inputSchema: {
118
+ type: 'object',
119
+ properties: {
120
+ sprintName: {
121
+ type: 'string',
122
+ description: 'Sprint name (optional — auto-detects active sprint if not provided)',
123
+ },
124
+ },
125
+ },
126
+ },
127
+ {
128
+ name: 'get_capabilities',
129
+ description: 'Returns available tools, current team config status, and credential status.',
130
+ inputSchema: {
131
+ type: 'object',
132
+ properties: {},
133
+ },
134
+ },
135
+ ],
136
+ };
137
+ });
138
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
139
+ const { name, arguments: args } = request.params;
140
+ try {
141
+ switch (name) {
142
+ case 'setup_team':
143
+ return await handleSetupTeam(args);
144
+ case 'add_member':
145
+ return await handleAddMember(args);
146
+ case 'remove_member':
147
+ return handleRemoveMember(args);
148
+ case 'refresh_config':
149
+ return await handleRefreshConfig();
150
+ case 'standup':
151
+ return await handleStandup(args);
152
+ case 'team_standup':
153
+ return await handleTeamStandup();
154
+ case 'weekly_summary':
155
+ return await handleWeeklySummary(args);
156
+ case 'sprint_retro':
157
+ return await handleSprintRetro(args);
158
+ case 'get_capabilities':
159
+ return handleGetCapabilities();
160
+ default:
161
+ throw new Error(`Unknown tool: ${name}`);
162
+ }
163
+ }
164
+ catch (error) {
165
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
166
+ return {
167
+ content: [{ type: 'text', text: `Error: ${errorMessage}` }],
168
+ isError: true,
169
+ };
170
+ }
171
+ });
172
+ async function handleSetupTeam(args) {
173
+ const teamName = args?.['teamName'];
174
+ const emails = args?.['emails'];
175
+ if (typeof teamName !== 'string' || !teamName.trim()) {
176
+ throw new Error('teamName is required and must be a non-empty string');
177
+ }
178
+ if (!Array.isArray(emails) || emails.length === 0) {
179
+ throw new Error('emails is required and must be a non-empty array of email strings');
180
+ }
181
+ const emailList = emails.filter((e) => typeof e === 'string' && e.includes('@'));
182
+ if (emailList.length === 0) {
183
+ throw new Error('No valid email addresses provided');
184
+ }
185
+ const config = createDefaultConfig(teamName.trim(), emailList);
186
+ const parts = [];
187
+ parts.push(`# Prodbeam Team Setup`);
188
+ parts.push('');
189
+ parts.push(`**Team:** ${teamName}`);
190
+ parts.push(`**Config directory:** ${resolveConfigDir()}`);
191
+ parts.push('');
192
+ const ghCreds = resolveGitHubCredentials();
193
+ if (ghCreds) {
194
+ parts.push('## GitHub Discovery');
195
+ parts.push('');
196
+ const ghClient = new GitHubClient(ghCreds.token);
197
+ const ghResult = await discoverGitHubTeam(ghClient, emailList);
198
+ for (const member of ghResult.members) {
199
+ const configMember = config.team.members.find((m) => m.email === member.email);
200
+ if (configMember && member.username) {
201
+ configMember.github = member.username;
202
+ configMember.name = configMember.name ?? member.username;
203
+ }
204
+ const status = member.username ? `@${member.username}` : `not found`;
205
+ const suffix = member.error ? ` (${member.error})` : '';
206
+ parts.push(`- ${member.email} → ${status}${suffix}`);
207
+ }
208
+ if (ghResult.orgs.length > 0) {
209
+ config.github.org = ghResult.orgs[0];
210
+ parts.push('');
211
+ parts.push(`**Orgs:** ${ghResult.orgs.join(', ')}`);
212
+ }
213
+ if (ghResult.repos.length > 0) {
214
+ config.github.repos = ghResult.repos;
215
+ parts.push(`**Active repos (last 90 days):** ${ghResult.repos.length}`);
216
+ for (const repo of ghResult.repos) {
217
+ parts.push(` - ${repo}`);
218
+ }
219
+ }
220
+ parts.push('');
221
+ }
222
+ else {
223
+ parts.push('## GitHub Discovery');
224
+ parts.push('');
225
+ parts.push('⚠️ No GitHub credentials found. Set `GITHUB_TOKEN` env var or run setup again after configuring credentials.');
226
+ parts.push('');
227
+ }
228
+ const jiraCreds = resolveJiraCredentials();
229
+ if (jiraCreds) {
230
+ parts.push('## Jira Discovery');
231
+ parts.push('');
232
+ const jiraClient = new JiraClient(jiraCreds.host, jiraCreds.email, jiraCreds.apiToken);
233
+ const jiraResult = await discoverJiraTeam(jiraClient, emailList, jiraCreds.host);
234
+ config.jira.host = jiraResult.host;
235
+ for (const member of jiraResult.members) {
236
+ const configMember = config.team.members.find((m) => m.email === member.email);
237
+ if (configMember && member.accountId) {
238
+ configMember.jiraAccountId = member.accountId;
239
+ if (!configMember.name && member.displayName) {
240
+ configMember.name = member.displayName;
241
+ }
242
+ }
243
+ const status = member.displayName ?? 'not found';
244
+ const suffix = member.error ? ` (${member.error})` : '';
245
+ parts.push(`- ${member.email} → ${status}${suffix}`);
246
+ }
247
+ if (jiraResult.projects.length > 0) {
248
+ config.jira.projects = jiraResult.projects.map((p) => p.key);
249
+ parts.push('');
250
+ parts.push(`**Projects:** ${jiraResult.projects.map((p) => `${p.key} (${p.name})`).join(', ')}`);
251
+ }
252
+ if (jiraResult.activeSprints.length > 0) {
253
+ parts.push(`**Active sprints:**`);
254
+ for (const sprint of jiraResult.activeSprints) {
255
+ const dates = sprint.startDate && sprint.endDate
256
+ ? ` (${sprint.startDate.split('T')[0]} → ${sprint.endDate.split('T')[0]})`
257
+ : '';
258
+ parts.push(` - ${sprint.name}${dates}`);
259
+ }
260
+ }
261
+ parts.push('');
262
+ }
263
+ else {
264
+ parts.push('## Jira Discovery');
265
+ parts.push('');
266
+ parts.push('⚠️ No Jira credentials found. Set `JIRA_HOST`, `JIRA_EMAIL`, and `JIRA_API_TOKEN` env vars or configure credentials.');
267
+ parts.push('');
268
+ }
269
+ writeTeamConfig(config);
270
+ parts.push('---');
271
+ parts.push('');
272
+ parts.push(`✅ Team config written to \`${resolveConfigDir()}/team.json\``);
273
+ parts.push('');
274
+ parts.push('**Generated config:**');
275
+ parts.push('```json');
276
+ parts.push(JSON.stringify(config, null, 2));
277
+ parts.push('```');
278
+ return { content: [{ type: 'text', text: parts.join('\n') }] };
279
+ }
280
+ async function handleAddMember(args) {
281
+ const email = args?.['email'];
282
+ if (typeof email !== 'string' || !email.includes('@')) {
283
+ throw new Error('A valid email address is required');
284
+ }
285
+ const config = readTeamConfig();
286
+ if (!config) {
287
+ throw new Error('No team config found. Run setup_team first.');
288
+ }
289
+ if (config.team.members.some((m) => m.email.toLowerCase() === email.toLowerCase())) {
290
+ throw new Error(`${email} is already a team member`);
291
+ }
292
+ const newMember = { email };
293
+ const parts = [];
294
+ parts.push(`# Add Member: ${email}`);
295
+ parts.push('');
296
+ const ghCreds = resolveGitHubCredentials();
297
+ if (ghCreds) {
298
+ const ghClient = new GitHubClient(ghCreds.token);
299
+ const username = await ghClient.searchUserByEmail(email);
300
+ if (username) {
301
+ newMember.github = username;
302
+ newMember.name = username;
303
+ parts.push(`GitHub: @${username}`);
304
+ const repos = await ghClient.getRecentRepos(username).catch(() => []);
305
+ const newRepos = repos.filter((r) => !config.github.repos.includes(r));
306
+ if (newRepos.length > 0) {
307
+ config.github.repos.push(...newRepos);
308
+ parts.push(`New repos discovered: ${newRepos.join(', ')}`);
309
+ }
310
+ }
311
+ else {
312
+ parts.push(`GitHub: not found for ${email}`);
313
+ }
314
+ }
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);
319
+ if (user) {
320
+ newMember.jiraAccountId = user.accountId;
321
+ if (!newMember.name) {
322
+ newMember.name = user.displayName;
323
+ }
324
+ parts.push(`Jira: ${user.displayName} (${user.accountId})`);
325
+ }
326
+ else {
327
+ parts.push(`Jira: not found for ${email}`);
328
+ }
329
+ }
330
+ config.team.members.push(newMember);
331
+ writeTeamConfig(config);
332
+ parts.push('');
333
+ parts.push(`✅ Added ${newMember.name ?? email} to team "${config.team.name}"`);
334
+ parts.push(`Team now has ${config.team.members.length} members.`);
335
+ return { content: [{ type: 'text', text: parts.join('\n') }] };
336
+ }
337
+ function handleRemoveMember(args) {
338
+ const email = args?.['email'];
339
+ if (typeof email !== 'string' || !email.includes('@')) {
340
+ throw new Error('A valid email address is required');
341
+ }
342
+ const config = readTeamConfig();
343
+ if (!config) {
344
+ throw new Error('No team config found. Run setup_team first.');
345
+ }
346
+ const index = config.team.members.findIndex((m) => m.email.toLowerCase() === email.toLowerCase());
347
+ if (index === -1) {
348
+ throw new Error(`${email} is not a team member`);
349
+ }
350
+ const removed = config.team.members[index];
351
+ config.team.members.splice(index, 1);
352
+ if (config.team.members.length === 0) {
353
+ throw new Error('Cannot remove the last team member. Delete the config file instead.');
354
+ }
355
+ writeTeamConfig(config);
356
+ return {
357
+ content: [
358
+ {
359
+ type: 'text',
360
+ text: `Removed ${removed?.name ?? email} from team "${config.team.name}"\nTeam now has ${config.team.members.length} members.`,
361
+ },
362
+ ],
363
+ };
364
+ }
365
+ async function handleRefreshConfig() {
366
+ const config = readTeamConfig();
367
+ if (!config) {
368
+ throw new Error('No team config found. Run setup_team first.');
369
+ }
370
+ const parts = [];
371
+ parts.push('# Config Refresh');
372
+ parts.push('');
373
+ 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);
378
+ for (const member of ghResult.members) {
379
+ const configMember = config.team.members.find((m) => m.email === member.email);
380
+ if (configMember && member.username) {
381
+ configMember.github = member.username;
382
+ }
383
+ }
384
+ const previousRepos = new Set(config.github.repos);
385
+ const newRepos = ghResult.repos.filter((r) => !previousRepos.has(r));
386
+ if (newRepos.length > 0) {
387
+ config.github.repos = ghResult.repos;
388
+ parts.push(`**New repos discovered:** ${newRepos.join(', ')}`);
389
+ }
390
+ else {
391
+ parts.push(`**Repos:** no changes (${config.github.repos.length} tracked)`);
392
+ }
393
+ if (ghResult.orgs.length > 0) {
394
+ config.github.org = ghResult.orgs[0];
395
+ }
396
+ }
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);
401
+ for (const member of jiraResult.members) {
402
+ const configMember = config.team.members.find((m) => m.email === member.email);
403
+ if (configMember && member.accountId) {
404
+ configMember.jiraAccountId = member.accountId;
405
+ if (!configMember.name && member.displayName) {
406
+ configMember.name = member.displayName;
407
+ }
408
+ }
409
+ }
410
+ const newProjects = jiraResult.projects.map((p) => p.key);
411
+ config.jira.projects = newProjects;
412
+ if (jiraResult.activeSprints.length > 0) {
413
+ parts.push(`**Active sprints:**`);
414
+ for (const sprint of jiraResult.activeSprints) {
415
+ parts.push(` - ${sprint.name} [${sprint.state}]`);
416
+ }
417
+ }
418
+ }
419
+ writeTeamConfig(config);
420
+ parts.push('');
421
+ parts.push(`✅ Config refreshed at \`${resolveConfigDir()}/team.json\``);
422
+ return { content: [{ type: 'text', text: parts.join('\n') }] };
423
+ }
424
+ async function handleStandup(args) {
425
+ const config = requireTeamConfig();
426
+ const email = typeof args?.['email'] === 'string' ? args['email'] : undefined;
427
+ const member = resolveMember(config, email);
428
+ if (!member.github) {
429
+ throw new Error(`No GitHub username for ${member.email}. Run refresh_config to re-discover.`);
430
+ }
431
+ 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);
438
+ 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);
443
+ }
444
+ const report = generateDailyReport({ github, jira });
445
+ return { content: [{ type: 'text', text: report }] };
446
+ }
447
+ async function handleTeamStandup() {
448
+ const config = requireTeamConfig();
449
+ const ghCreds = resolveGitHubCredentials();
450
+ if (!ghCreds) {
451
+ throw new Error('GitHub credentials required. Set GITHUB_TOKEN env var.');
452
+ }
453
+ 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;
459
+ const membersWithGH = config.team.members.filter((m) => m.github);
460
+ const usernames = membersWithGH.map((m) => m.github);
461
+ const ghActivities = await fetchTeamGitHubActivity(ghClient, usernames, config.github.repos, timeRange);
462
+ const memberActivities = [];
463
+ for (let i = 0; i < membersWithGH.length; i++) {
464
+ const member = membersWithGH[i];
465
+ const github = ghActivities[i];
466
+ let jira;
467
+ if (jiraClient && member.jiraAccountId) {
468
+ jira = await fetchJiraActivityForUser(jiraClient, member.jiraAccountId, config.jira.projects, timeRange);
469
+ }
470
+ memberActivities.push({ github, jira });
471
+ }
472
+ const report = generateTeamDailyReport(memberActivities, config.team.name);
473
+ return { content: [{ type: 'text', text: report }] };
474
+ }
475
+ async function handleWeeklySummary(args) {
476
+ const config = requireTeamConfig();
477
+ const ghCreds = resolveGitHubCredentials();
478
+ if (!ghCreds) {
479
+ throw new Error('GitHub credentials required. Set GITHUB_TOKEN env var.');
480
+ }
481
+ const weeksAgo = typeof args?.['weeksAgo'] === 'number' ? args['weeksAgo'] : 0;
482
+ const timeRange = weeklyTimeRange(weeksAgo);
483
+ const ghClient = new GitHubClient(ghCreds.token);
484
+ const membersWithGH = config.team.members.filter((m) => m.github);
485
+ const usernames = membersWithGH.map((m) => m.github);
486
+ const perMember = await fetchTeamGitHubActivity(ghClient, usernames, config.github.repos, timeRange);
487
+ const github = mergeGitHubActivities(perMember, config.team.name, timeRange);
488
+ 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);
493
+ }
494
+ const snapshot = buildSnapshot({
495
+ teamName: config.team.name,
496
+ snapshotType: 'weekly',
497
+ periodStart: timeRange.from,
498
+ periodEnd: timeRange.to,
499
+ github,
500
+ jira,
501
+ });
502
+ const thresholds = resolveThresholds(config.settings.thresholds);
503
+ const extras = {};
504
+ try {
505
+ const store = new HistoryStore();
506
+ const previous = store.getPreviousSnapshot('weekly', timeRange.to);
507
+ const history = store.getWeeklyHistory(5);
508
+ extras.trends = analyzeTrends(snapshot, previous, thresholds);
509
+ extras.anomalies = detectAnomalies({
510
+ pullRequests: github.pullRequests,
511
+ reviews: github.reviews,
512
+ jiraIssues: jira?.issues ?? [],
513
+ memberActivity: buildMemberActivity(perMember),
514
+ thresholds,
515
+ });
516
+ extras.health = assessTeamHealth({
517
+ current: snapshot,
518
+ history,
519
+ memberSnapshots: buildMemberSnapshots(perMember),
520
+ thresholds,
521
+ });
522
+ store.saveSnapshot(snapshot);
523
+ store.close();
524
+ }
525
+ catch {
526
+ }
527
+ const report = generateWeeklyReport({ github, jira }, extras);
528
+ return { content: [{ type: 'text', text: report }] };
529
+ }
530
+ async function handleSprintRetro(args) {
531
+ 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
+ }
537
+ let sprintName = typeof args?.['sprintName'] === 'string' ? args['sprintName'] : undefined;
538
+ 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);
542
+ if (activeSprint) {
543
+ sprintName = activeSprint.name;
544
+ timeRange = sprintTimeRange(activeSprint.startDate, activeSprint.endDate);
545
+ }
546
+ }
547
+ if (!sprintName) {
548
+ throw new Error('No active sprint detected. Provide a sprintName parameter or ensure Jira credentials are configured.');
549
+ }
550
+ if (!timeRange) {
551
+ timeRange = weeklyTimeRange(0);
552
+ const from = new Date(new Date(timeRange.to).getTime() - 14 * 24 * 60 * 60 * 1000);
553
+ timeRange = { from: from.toISOString(), to: timeRange.to };
554
+ }
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);
559
+ const github = mergeGitHubActivities(perMember, config.team.name, timeRange);
560
+ let jira;
561
+ if (jiraCreds) {
562
+ const jiraClient = new JiraClient(jiraCreds.host, jiraCreds.email, jiraCreds.apiToken);
563
+ jira = await fetchSprintJiraActivity(jiraClient, sprintName, timeRange);
564
+ }
565
+ const dateRange = {
566
+ from: timeRange.from.split('T')[0],
567
+ to: timeRange.to.split('T')[0],
568
+ };
569
+ const snapshot = buildSnapshot({
570
+ teamName: config.team.name,
571
+ snapshotType: 'sprint',
572
+ periodStart: timeRange.from,
573
+ periodEnd: timeRange.to,
574
+ sprintName,
575
+ github,
576
+ jira,
577
+ });
578
+ const thresholds = resolveThresholds(config.settings.thresholds);
579
+ const extras = {};
580
+ try {
581
+ const store = new HistoryStore();
582
+ const previous = store.getPreviousSnapshot('sprint', timeRange.to);
583
+ const history = store.getSprintHistory(5);
584
+ extras.trends = analyzeTrends(snapshot, previous, thresholds);
585
+ extras.anomalies = detectAnomalies({
586
+ pullRequests: github.pullRequests,
587
+ reviews: github.reviews,
588
+ jiraIssues: jira?.issues ?? [],
589
+ memberActivity: buildMemberActivity(perMember),
590
+ thresholds,
591
+ });
592
+ extras.health = assessTeamHealth({
593
+ current: snapshot,
594
+ history,
595
+ memberSnapshots: buildMemberSnapshots(perMember),
596
+ thresholds,
597
+ });
598
+ store.saveSnapshot(snapshot);
599
+ store.close();
600
+ }
601
+ catch {
602
+ }
603
+ const report = generateRetrospective({ github, jira, sprintName, dateRange }, extras);
604
+ return { content: [{ type: 'text', text: report }] };
605
+ }
606
+ function requireTeamConfig() {
607
+ const config = readTeamConfig();
608
+ if (!config) {
609
+ throw new Error('No team config found. Run setup_team first.');
610
+ }
611
+ return config;
612
+ }
613
+ function resolveMember(config, email) {
614
+ if (email) {
615
+ const member = config.team.members.find((m) => m.email.toLowerCase() === email.toLowerCase());
616
+ if (!member) {
617
+ throw new Error(`${email} is not a team member. Check your config.`);
618
+ }
619
+ return member;
620
+ }
621
+ const first = config.team.members[0];
622
+ if (!first) {
623
+ throw new Error('No team members configured.');
624
+ }
625
+ return first;
626
+ }
627
+ function mergeGitHubActivities(activities, teamName, timeRange) {
628
+ return {
629
+ username: teamName,
630
+ commits: activities.flatMap((a) => a.commits),
631
+ pullRequests: activities.flatMap((a) => a.pullRequests),
632
+ reviews: activities.flatMap((a) => a.reviews),
633
+ timeRange,
634
+ };
635
+ }
636
+ function buildMemberActivity(perMember) {
637
+ return perMember.map((m) => ({
638
+ username: m.username,
639
+ commits: m.commits.length,
640
+ prsAuthored: m.pullRequests.length,
641
+ reviewsGiven: m.reviews.length,
642
+ additions: m.pullRequests.reduce((sum, pr) => sum + (pr.additions ?? 0), 0),
643
+ deletions: m.pullRequests.reduce((sum, pr) => sum + (pr.deletions ?? 0), 0),
644
+ }));
645
+ }
646
+ function buildMemberSnapshots(perMember) {
647
+ return perMember.map((m) => ({
648
+ memberGithub: m.username,
649
+ commits: m.commits.length,
650
+ prs: m.pullRequests.length,
651
+ prsMerged: m.pullRequests.filter((pr) => pr.state === 'merged').length,
652
+ reviewsGiven: m.reviews.length,
653
+ additions: m.pullRequests.reduce((sum, pr) => sum + (pr.additions ?? 0), 0),
654
+ deletions: m.pullRequests.reduce((sum, pr) => sum + (pr.deletions ?? 0), 0),
655
+ jiraCompleted: 0,
656
+ }));
657
+ }
658
+ function handleGetCapabilities() {
659
+ const configExists = teamConfigExists();
660
+ const config = configExists ? readTeamConfig() : null;
661
+ const ghCreds = resolveGitHubCredentials();
662
+ const jiraCreds = resolveJiraCredentials();
663
+ const parts = [];
664
+ parts.push('# Prodbeam MCP v2');
665
+ parts.push('');
666
+ parts.push('## Status');
667
+ parts.push('');
668
+ parts.push(`| Component | Status |`);
669
+ parts.push(`|-----------|--------|`);
670
+ parts.push(`| Config directory | \`${resolveConfigDir()}\` |`);
671
+ 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)'} |`);
674
+ parts.push('');
675
+ if (!ghCreds || !configExists) {
676
+ parts.push('## Getting Started');
677
+ parts.push('');
678
+ renderSetupGuide(parts, { ghCreds: !!ghCreds, jiraCreds: !!jiraCreds, configExists });
679
+ }
680
+ if (config) {
681
+ parts.push('## Team');
682
+ parts.push('');
683
+ for (const m of config.team.members) {
684
+ const gh = m.github ? `@${m.github}` : 'no GitHub';
685
+ const jira = m.jiraAccountId ? '✅ Jira' : 'no Jira';
686
+ parts.push(`- ${m.name ?? m.email} (${gh}, ${jira})`);
687
+ }
688
+ parts.push('');
689
+ parts.push(`**Repos:** ${config.github.repos.join(', ') || 'none'}`);
690
+ parts.push(`**Jira projects:** ${config.jira.projects.join(', ') || 'none'}`);
691
+ parts.push('');
692
+ }
693
+ parts.push('## Intelligence Thresholds');
694
+ parts.push('');
695
+ const t = resolveThresholds(config?.settings.thresholds);
696
+ parts.push(`| Threshold | Value |`);
697
+ parts.push(`|-----------|-------|`);
698
+ parts.push(`| Stale PR warning | ${t.stalePrWarningDays}d |`);
699
+ parts.push(`| Stale PR alert | ${t.stalePrAlertDays}d |`);
700
+ parts.push(`| Stale issue | ${t.staleIssueDays}d |`);
701
+ parts.push(`| Review imbalance | ${Math.round(t.reviewImbalanceThreshold * 100)}% |`);
702
+ parts.push(`| High churn multiplier | ${t.highChurnMultiplier}x avg |`);
703
+ parts.push(`| High churn minimum | ${t.highChurnMinimum} lines |`);
704
+ parts.push(`| Trend alert | ${t.trendAlertPercent}% change |`);
705
+ parts.push(`| Trend warning | ${t.trendWarningPercent}% change |`);
706
+ parts.push(`| Merge time warning | ${t.mergeTimeWarningH}h |`);
707
+ parts.push(`| Merge time alert | ${t.mergeTimeAlertH}h |`);
708
+ parts.push('');
709
+ parts.push('## Available Tools');
710
+ parts.push('');
711
+ parts.push('### Setup');
712
+ parts.push('- **setup_team** — One-time setup with team name + member emails');
713
+ parts.push('- **add_member** — Add a member by email');
714
+ parts.push('- **remove_member** — Remove a member by email');
715
+ parts.push('- **refresh_config** — Re-scan repos and sprints');
716
+ parts.push('');
717
+ parts.push('### Reports');
718
+ parts.push('- **standup** — Personal daily standup (last 24h)');
719
+ parts.push('- **team_standup** — Full team standup (last 24h)');
720
+ parts.push('- **weekly_summary** — Week-in-review with metrics');
721
+ parts.push('- **sprint_retro** — Sprint retrospective with completion rates');
722
+ return { content: [{ type: 'text', text: parts.join('\n') }] };
723
+ }
724
+ function renderSetupGuide(parts, status) {
725
+ let step = 1;
726
+ if (!status.ghCreds) {
727
+ parts.push(`### Step ${step}: Set up GitHub credentials (required)`);
728
+ parts.push('');
729
+ parts.push('1. Go to https://github.com/settings/tokens');
730
+ parts.push('2. Click **"Generate new token (classic)"**');
731
+ parts.push('3. Select these scopes: `repo`, `read:user`, `read:org`');
732
+ parts.push('4. Copy the token and add it to your MCP server config:');
733
+ parts.push('');
734
+ parts.push('```json');
735
+ parts.push('{');
736
+ parts.push(' "mcpServers": {');
737
+ parts.push(' "prodbeam": {');
738
+ parts.push(' "command": "npx",');
739
+ parts.push(' "args": ["-y", "@prodbeam/mcp"],');
740
+ parts.push(' "env": {');
741
+ parts.push(' "GITHUB_TOKEN": "ghp_your_token_here"');
742
+ parts.push(' }');
743
+ parts.push(' }');
744
+ parts.push(' }');
745
+ parts.push('}');
746
+ parts.push('```');
747
+ parts.push('');
748
+ parts.push('After updating your config, restart your MCP client and run `get_capabilities` again.');
749
+ parts.push('');
750
+ step++;
751
+ }
752
+ if (!status.jiraCreds) {
753
+ parts.push(`### Step ${step}: Set up Jira credentials (optional)`);
754
+ parts.push('');
755
+ parts.push('Skip this step if your team does not use Jira. Prodbeam works with GitHub alone.');
756
+ parts.push('');
757
+ parts.push('1. Go to https://id.atlassian.com/manage/api-tokens');
758
+ parts.push('2. Click **"Create API token"**, give it a label like "Prodbeam"');
759
+ parts.push('3. Add these env vars to your MCP server config:');
760
+ parts.push('');
761
+ parts.push('```json');
762
+ parts.push('"env": {');
763
+ parts.push(' "GITHUB_TOKEN": "ghp_your_token_here",');
764
+ parts.push(' "JIRA_HOST": "https://yourcompany.atlassian.net",');
765
+ parts.push(' "JIRA_EMAIL": "you@company.com",');
766
+ parts.push(' "JIRA_API_TOKEN": "your_jira_api_token"');
767
+ parts.push('}');
768
+ parts.push('```');
769
+ parts.push('');
770
+ step++;
771
+ }
772
+ if (!status.configExists) {
773
+ parts.push(`### Step ${step}: Set up your team`);
774
+ parts.push('');
775
+ if (!status.ghCreds) {
776
+ parts.push('Once GitHub credentials are configured, run:');
777
+ }
778
+ else {
779
+ parts.push('Run:');
780
+ }
781
+ parts.push('');
782
+ parts.push('> `setup_team` with your team name and member email addresses');
783
+ parts.push('');
784
+ parts.push('Prodbeam will auto-discover GitHub usernames, Jira accounts, active repos, and projects.');
785
+ parts.push('');
786
+ }
787
+ }
788
+ async function main() {
789
+ const transport = new StdioServerTransport();
790
+ await server.connect(transport);
791
+ console.error('[prodbeam] MCP server v2.0.0 started');
792
+ }
793
+ main().catch((error) => {
794
+ console.error('[prodbeam] Fatal error:', error);
795
+ process.exit(1);
796
+ });
797
+ //# sourceMappingURL=index.js.map