@konductro/claude-plugin 2.0.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.
@@ -0,0 +1,894 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Konductro MCP Server
5
+ *
6
+ * Bridges Claude Code to the Konductro platform REST API.
7
+ * Runs as a stdio MCP server, spawned by the plugin config.
8
+ *
9
+ * Environment variables (set by plugin.json from userConfig):
10
+ * KONDUCTRO_URL — Konductro platform URL
11
+ * KONDUCTRO_CLI_TOKEN — Personal CLI access token
12
+ */
13
+
14
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
15
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
16
+ import { z } from 'zod';
17
+
18
+ const KONDUCTRO_URL = process.env.KONDUCTRO_URL?.replace(/\/$/, '') || '';
19
+ const CLI_TOKEN = process.env.KONDUCTRO_CLI_TOKEN || '';
20
+
21
+ async function konductroFetch(path, options = {}) {
22
+ const url = `${KONDUCTRO_URL}${path}`;
23
+ const headers = {
24
+ 'Authorization': `Bearer ${CLI_TOKEN}`,
25
+ ...options.headers,
26
+ };
27
+ // Only set Content-Type and default body for methods that send JSON
28
+ if (options.method === 'POST' || options.method === 'PUT' || options.method === 'PATCH') {
29
+ headers['Content-Type'] = 'application/json';
30
+ if (!options.body) options.body = '{}';
31
+ }
32
+ const response = await fetch(url, {
33
+ ...options,
34
+ headers,
35
+ });
36
+
37
+ if (!response.ok) {
38
+ const body = await response.text().catch(() => '');
39
+ throw new Error(`Konductro API error (${response.status}): ${body}`);
40
+ }
41
+
42
+ return response.json();
43
+ }
44
+
45
+ const server = new McpServer({
46
+ name: 'konductro',
47
+ version: '1.0.0',
48
+ });
49
+
50
+ // Tool: List pending tech analysis tasks
51
+ server.tool(
52
+ 'list_tech_analysis_tasks',
53
+ 'List your pending technical analysis assignments from Konductro',
54
+ {},
55
+ async () => {
56
+ if (!KONDUCTRO_URL || !CLI_TOKEN) {
57
+ return {
58
+ content: [{
59
+ type: 'text',
60
+ text: 'Konductro is not configured. Please set your Konductro URL and CLI token in the plugin settings.',
61
+ }],
62
+ };
63
+ }
64
+
65
+ const tasks = await konductroFetch('/api/cli/tech-analysis-tasks');
66
+
67
+ if (tasks.length === 0) {
68
+ return {
69
+ content: [{
70
+ type: 'text',
71
+ text: 'No pending technical analysis tasks assigned to you.',
72
+ }],
73
+ };
74
+ }
75
+
76
+ const taskList = tasks.map((t, i) =>
77
+ `${i + 1}. **${t.projectName}** — ${t.phaseName}\n Client: ${t.clientName}\n Phase ID: ${t.phaseId}\n ${t.hasExistingDraft ? '(has existing draft)' : '(new analysis)'}`
78
+ ).join('\n\n');
79
+
80
+ return {
81
+ content: [{
82
+ type: 'text',
83
+ text: `## Your Pending Technical Analysis Tasks\n\n${taskList}\n\nUse the **get_task_context** tool with the phase ID to load the full context for a task.`,
84
+ }],
85
+ };
86
+ }
87
+ );
88
+
89
+ // Tool: Get full context for a tech analysis task
90
+ server.tool(
91
+ 'get_task_context',
92
+ 'Load the full context (requirements, project info, repositories) for a technical analysis task',
93
+ {
94
+ phaseId: z.string().describe('The phase ID from the task list'),
95
+ },
96
+ async ({ phaseId }) => {
97
+ const context = await konductroFetch(`/api/cli/tech-analysis-tasks/${phaseId}/context`);
98
+
99
+ let contextText = `## Technical Analysis Context\n\n`;
100
+ contextText += `**Project:** ${context.project.name}\n`;
101
+ contextText += `**Client:** ${context.project.client.name}`;
102
+ if (context.project.client.industry) contextText += ` (${context.project.client.industry})`;
103
+ contextText += `\n**Phase:** ${context.phase.name}\n`;
104
+ if (context.phase.description) contextText += `**Description:** ${context.phase.description}\n`;
105
+ contextText += `\n`;
106
+
107
+ if (context.repositories.length > 0) {
108
+ contextText += `### Repositories\n\n`;
109
+ for (const repo of context.repositories) {
110
+ contextText += `- **${repo.name}** (${repo.repoType}) — ${repo.stacks.join(', ')}\n`;
111
+ contextText += ` URL: ${repo.repoUrl}\n`;
112
+ contextText += ` Base branch: ${repo.branchBase}\n`;
113
+ }
114
+ contextText += `\n`;
115
+ }
116
+
117
+ if (context.requirements) {
118
+ contextText += `### Approved Requirements (v${context.requirements.version})\n\n`;
119
+ contextText += context.requirements.content;
120
+ contextText += `\n\n`;
121
+ }
122
+
123
+ if (context.approvedArtefacts.length > 0) {
124
+ for (const artefact of context.approvedArtefacts) {
125
+ if (artefact.type !== 'requirements') {
126
+ contextText += `### ${artefact.title} (v${artefact.version})\n\n`;
127
+ contextText += artefact.content;
128
+ contextText += `\n\n`;
129
+ }
130
+ }
131
+ }
132
+
133
+ return {
134
+ content: [{
135
+ type: 'text',
136
+ text: contextText,
137
+ }],
138
+ };
139
+ }
140
+ );
141
+
142
+ // Tool: Submit tech analysis
143
+ server.tool(
144
+ 'submit_tech_analysis',
145
+ 'Submit your technical analysis document and conversation summary back to Konductro',
146
+ {
147
+ phaseId: z.string().describe('The phase ID for the task'),
148
+ document: z.string().describe('The complete technical analysis document in markdown'),
149
+ conversationSummary: z.string().optional().describe('Summary of key findings and decisions from the analysis conversation'),
150
+ },
151
+ async ({ phaseId, document, conversationSummary }) => {
152
+ const result = await konductroFetch(`/api/cli/tech-analysis-tasks/${phaseId}/submit`, {
153
+ method: 'POST',
154
+ body: JSON.stringify({ document, conversationSummary }),
155
+ });
156
+
157
+ return {
158
+ content: [{
159
+ type: 'text',
160
+ text: `## Technical Analysis Submitted\n\n${result.message}\n\n- Artefact ID: ${result.artefactId}\n- Version: ${result.version}\n\nThe document is now available in Konductro for review and approval.`,
161
+ }],
162
+ };
163
+ }
164
+ );
165
+
166
+ // Tool: List pending decomposition tasks
167
+ server.tool(
168
+ 'list_decomposition_tasks',
169
+ 'List your pending story decomposition assignments from Konductro',
170
+ {},
171
+ async () => {
172
+ if (!KONDUCTRO_URL || !CLI_TOKEN) {
173
+ return {
174
+ content: [{
175
+ type: 'text',
176
+ text: 'Konductro is not configured. Please set your Konductro URL and CLI token in the plugin settings.',
177
+ }],
178
+ };
179
+ }
180
+
181
+ const tasks = await konductroFetch('/api/cli/decomposition-tasks');
182
+
183
+ if (tasks.length === 0) {
184
+ return {
185
+ content: [{
186
+ type: 'text',
187
+ text: 'No pending decomposition tasks assigned to you.',
188
+ }],
189
+ };
190
+ }
191
+
192
+ const taskList = tasks.map((t, i) =>
193
+ `${i + 1}. **${t.ticketKey}** — ${t.storyTitle}\n Project: ${t.projectName} (${t.clientName})\n Phase: ${t.phaseName}\n Complexity: ${t.complexityScore || 'not scored'}\n Story ID: ${t.storyId}\n ${t.existingTaskCount > 0 ? `(${t.existingTaskCount} existing tasks)` : '(no tasks yet)'}`
194
+ ).join('\n\n');
195
+
196
+ return {
197
+ content: [{
198
+ type: 'text',
199
+ text: `## Your Pending Decomposition Tasks\n\n${taskList}\n\nUse the **get_decomposition_context** tool with the story ID to load the full context.`,
200
+ }],
201
+ };
202
+ }
203
+ );
204
+
205
+ // Tool: Get full context for a decomposition task
206
+ server.tool(
207
+ 'get_decomposition_context',
208
+ 'Load the full context (story details, requirements, architecture, repositories) for a decomposition task',
209
+ {
210
+ storyId: z.string().describe('The story ID from the task list'),
211
+ },
212
+ async ({ storyId }) => {
213
+ const ctx = await konductroFetch(`/api/cli/decomposition-tasks/${storyId}/context`);
214
+
215
+ let text = `## Decomposition Context\n\n`;
216
+ text += `**Story:** ${ctx.story.ticketKey} — ${ctx.story.title}\n`;
217
+ text += `**Project:** ${ctx.project.name}\n`;
218
+ text += `**Phase:** ${ctx.phase.name}\n`;
219
+ if (ctx.story.complexityScore) text += `**Complexity:** ${ctx.story.complexityScore}`;
220
+ if (ctx.story.complexityReasoning) text += ` — ${ctx.story.complexityReasoning}`;
221
+ text += `\n\n`;
222
+
223
+ if (ctx.story.description) {
224
+ text += `### Story Description\n\n${ctx.story.description}\n\n`;
225
+ }
226
+
227
+ if (ctx.story.acceptanceCriteria?.length > 0) {
228
+ text += `### Acceptance Criteria\n\n`;
229
+ ctx.story.acceptanceCriteria.forEach((ac, i) => {
230
+ text += `${i + 1}. ${ac}\n`;
231
+ });
232
+ text += `\n`;
233
+ }
234
+
235
+ if (ctx.story.dependencies?.length > 0) {
236
+ text += `### Dependencies\n\n`;
237
+ ctx.story.dependencies.forEach(d => { text += `- ${d}\n`; });
238
+ text += `\n`;
239
+ }
240
+
241
+ if (ctx.repositories.length > 0) {
242
+ text += `### Repositories\n\n`;
243
+ for (const repo of ctx.repositories) {
244
+ text += `- **${repo.name}** (${repo.repoType}) — ${repo.stacks.join(', ')}\n`;
245
+ text += ` ID: ${repo.id}\n`;
246
+ text += ` URL: ${repo.repoUrl}\n`;
247
+ if (repo.profile) {
248
+ text += ` Role: ${repo.profile.role}`;
249
+ if (repo.profile.domainOwned) text += `, Domain: ${repo.profile.domainOwned}`;
250
+ text += `\n`;
251
+ }
252
+ }
253
+ text += `\n`;
254
+ }
255
+
256
+ if (ctx.requirements) {
257
+ text += `### Requirements (v${ctx.requirements.version})\n\n${ctx.requirements.content}\n\n`;
258
+ }
259
+
260
+ if (ctx.architecture) {
261
+ text += `### Architecture (v${ctx.architecture.version})\n\n${ctx.architecture.content}\n\n`;
262
+ }
263
+
264
+ if (ctx.existingTasks.length > 0) {
265
+ text += `### Existing Tasks\n\n`;
266
+ ctx.existingTasks.forEach(t => {
267
+ text += `- ${t.title} (${t.type}, ${t.status})\n`;
268
+ });
269
+ text += `\n`;
270
+ }
271
+
272
+ if (ctx.siblingStories.length > 0) {
273
+ text += `### Sibling Stories (for context)\n\n`;
274
+ for (const s of ctx.siblingStories) {
275
+ text += `- **${s.title}**`;
276
+ if (s.children.length > 0) text += ` — ${s.children.length} tasks`;
277
+ text += `\n`;
278
+ }
279
+ text += `\n`;
280
+ }
281
+
282
+ return {
283
+ content: [{
284
+ type: 'text',
285
+ text,
286
+ }],
287
+ };
288
+ }
289
+ );
290
+
291
+ // Tool: Submit a single decomposed task
292
+ server.tool(
293
+ 'submit_decomposition_task',
294
+ 'Submit a single decomposed task for a story to Konductro. Call once per task.',
295
+ {
296
+ storyId: z.string().describe('The story ID being decomposed'),
297
+ title: z.string().describe('Task title'),
298
+ description: z.string().describe('Technical description of the task'),
299
+ repositoryId: z.string().describe('Repository ID from the context'),
300
+ type: z.enum(['ui_component', 'api_java', 'api_node', 'android', 'ios', 'infra']).default('api_node').describe('Task type'),
301
+ acceptanceCriteria: z.array(z.string()).default([]).describe('Technical acceptance criteria'),
302
+ architecturalNotes: z.array(z.string()).default([]).describe('Implementation notes for the agent'),
303
+ componentPath: z.string().optional().nullable().describe('Primary file/directory path for this task'),
304
+ order: z.number().int().default(0).describe('Execution order (0-based)'),
305
+ filesToCreate: z.array(z.string()).default([]).describe('Files that need to be created'),
306
+ filesToModify: z.array(z.object({
307
+ path: z.string(),
308
+ change: z.string(),
309
+ })).default([]).describe('Files that need to be modified and what to change'),
310
+ implementationSteps: z.array(z.string()).default([]).describe('Step-by-step implementation guide'),
311
+ outOfScope: z.array(z.string()).default([]).describe('What is explicitly NOT included in this task'),
312
+ },
313
+ async ({ storyId, ...task }) => {
314
+ const result = await konductroFetch(`/api/cli/decomposition-tasks/${storyId}/task`, {
315
+ method: 'POST',
316
+ body: JSON.stringify(task),
317
+ });
318
+
319
+ return {
320
+ content: [{
321
+ type: 'text',
322
+ text: `Task created: **${result.title}** (${result.ticketNumber})`,
323
+ }],
324
+ };
325
+ }
326
+ );
327
+
328
+ // Tool: Complete decomposition — finalise after all tasks submitted
329
+ server.tool(
330
+ 'complete_decomposition',
331
+ 'Mark a story decomposition as complete after all tasks have been submitted. Call this once after submitting all tasks.',
332
+ {
333
+ storyId: z.string().describe('The story ID that was decomposed'),
334
+ conversationSummary: z.string().optional().describe('Summary of decomposition decisions and rationale'),
335
+ },
336
+ async ({ storyId, conversationSummary }) => {
337
+ const result = await konductroFetch(`/api/cli/decomposition-tasks/${storyId}/complete`, {
338
+ method: 'POST',
339
+ body: JSON.stringify({ conversationSummary }),
340
+ });
341
+
342
+ return {
343
+ content: [{
344
+ type: 'text',
345
+ text: `## Decomposition Complete\n\n${result.message}\n\nThe tasks are now available in Konductro for review and sprint planning.`,
346
+ }],
347
+ };
348
+ }
349
+ );
350
+
351
+ // Tool: List my tickets
352
+ server.tool(
353
+ 'list_my_tickets',
354
+ 'List all tickets assigned to you in Konductro. Shows ticket key, title, status, project, phase, sprint, and branch info.',
355
+ {
356
+ status: z.string().optional().describe('Filter by ticket status (e.g. in_progress, assigned, pr_open)'),
357
+ level: z.enum(['story', 'task']).optional().describe('Filter by ticket level'),
358
+ },
359
+ async ({ status, level }) => {
360
+ if (!KONDUCTRO_URL || !CLI_TOKEN) {
361
+ return {
362
+ content: [{
363
+ type: 'text',
364
+ text: 'Konductro is not configured. Please set your Konductro URL and CLI token in the plugin settings.',
365
+ }],
366
+ };
367
+ }
368
+
369
+ const params = new URLSearchParams();
370
+ if (status) params.set('status', status);
371
+ if (level) params.set('level', level);
372
+ const qs = params.toString() ? `?${params.toString()}` : '';
373
+
374
+ const tickets = await konductroFetch(`/api/cli/my-tickets${qs}`);
375
+
376
+ if (tickets.length === 0) {
377
+ return {
378
+ content: [{
379
+ type: 'text',
380
+ text: status ? `No tickets with status "${status}" assigned to you.` : 'No tickets assigned to you.',
381
+ }],
382
+ };
383
+ }
384
+
385
+ const ticketList = tickets.map((t, i) => {
386
+ let line = `${i + 1}. **${t.ticketKey}** — ${t.title}\n`;
387
+ line += ` Status: ${t.status} | Level: ${t.level} | Kind: ${t.kind}\n`;
388
+ line += ` Project: ${t.project.name} | Phase: ${t.phase.name}`;
389
+ if (t.sprint) line += ` | Sprint: ${t.sprint.name}`;
390
+ if (t.branchName) line += `\n Branch: ${t.branchName}`;
391
+ if (t.prUrl) line += `\n PR: ${t.prUrl}`;
392
+ if (t.parent) line += `\n Parent: ${t.parent.ticketKey} — ${t.parent.title}`;
393
+ return line;
394
+ }).join('\n\n');
395
+
396
+ return {
397
+ content: [{
398
+ type: 'text',
399
+ text: `## Your Tickets (${tickets.length})\n\n${ticketList}\n\nUse **get_ticket_context** with a ticket ID to load the full context.`,
400
+ }],
401
+ };
402
+ }
403
+ );
404
+
405
+ // Tool: Get full ticket context
406
+ server.tool(
407
+ 'get_ticket_context',
408
+ 'Load the full context for a specific ticket — description, acceptance criteria, architectural notes, parent story, repository info, and approved artefacts. IMPORTANT: After showing context, do NOT offer to start implementing or writing code. If the developer wants to start work, they must use the /start-ticket skill which creates the branch and updates the ticket status in Konductro.',
409
+ {
410
+ ticketId: z.string().describe('The ticket ID to load context for'),
411
+ },
412
+ async ({ ticketId }) => {
413
+ const ctx = await konductroFetch(`/api/cli/tickets/${ticketId}/context`);
414
+
415
+ let text = `## Ticket Context\n\n`;
416
+ text += `**${ctx.ticket.ticketKey}** — ${ctx.ticket.title}\n`;
417
+ text += `**Status:** ${ctx.ticket.status} | **Level:** ${ctx.ticket.level} | **Kind:** ${ctx.ticket.kind}\n`;
418
+ text += `**Project:** ${ctx.project.name} | **Phase:** ${ctx.phase.name}\n`;
419
+ if (ctx.ticket.complexityScore) text += `**Complexity:** ${ctx.ticket.complexityScore}`;
420
+ if (ctx.ticket.complexityReasoning) text += ` — ${ctx.ticket.complexityReasoning}`;
421
+ if (ctx.ticket.complexityScore) text += `\n`;
422
+ if (ctx.ticket.branchName) text += `**Branch:** ${ctx.ticket.branchName}\n`;
423
+ if (ctx.ticket.prUrl) text += `**PR:** ${ctx.ticket.prUrl}\n`;
424
+ if (ctx.ticket.estimatedHours) text += `**Estimated:** ${ctx.ticket.estimatedHours}h\n`;
425
+ if (ctx.ticket.assignee) text += `**Assignee:** ${ctx.ticket.assignee.name}\n`;
426
+ if (ctx.ticket.developer) text += `**Developer:** ${ctx.ticket.developer.name}\n`;
427
+ text += `\n`;
428
+
429
+ // Description
430
+ text += `### Description\n\n${ctx.ticket.description}\n\n`;
431
+
432
+ // Acceptance Criteria
433
+ if (ctx.ticket.acceptanceCriteria?.length > 0) {
434
+ text += `### Acceptance Criteria\n\n`;
435
+ ctx.ticket.acceptanceCriteria.forEach((ac, i) => {
436
+ text += `${i + 1}. ${ac}\n`;
437
+ });
438
+ text += `\n`;
439
+ }
440
+
441
+ // Architectural Notes
442
+ if (ctx.ticket.architecturalNotes?.length > 0) {
443
+ text += `### Architectural Notes\n\n`;
444
+ ctx.ticket.architecturalNotes.forEach(n => { text += `- ${n}\n`; });
445
+ text += `\n`;
446
+ }
447
+
448
+ // Out of Scope
449
+ if (ctx.ticket.outOfScope?.length > 0) {
450
+ text += `### Out of Scope\n\n`;
451
+ ctx.ticket.outOfScope.forEach(o => { text += `- ${o}\n`; });
452
+ text += `\n`;
453
+ }
454
+
455
+ // Dependencies
456
+ if (ctx.ticket.dependencies?.length > 0) {
457
+ text += `### Dependencies\n\n`;
458
+ ctx.ticket.dependencies.forEach(d => { text += `- ${d}\n`; });
459
+ text += `\n`;
460
+ }
461
+
462
+ // Context Pack
463
+ if (ctx.ticket.contextPack) {
464
+ text += `### Context Pack\n\n${ctx.ticket.contextPack}\n\n`;
465
+ }
466
+
467
+ // Parent Story
468
+ if (ctx.parent) {
469
+ text += `### Parent Story: ${ctx.parent.ticketKey} — ${ctx.parent.title}\n\n`;
470
+ text += ctx.parent.description + '\n\n';
471
+ if (ctx.parent.acceptanceCriteria?.length > 0) {
472
+ text += `**Story AC:**\n`;
473
+ ctx.parent.acceptanceCriteria.forEach((ac, i) => { text += `${i + 1}. ${ac}\n`; });
474
+ text += `\n`;
475
+ }
476
+ if (ctx.parent.contextPack) {
477
+ text += `**Story Context Pack:**\n${ctx.parent.contextPack}\n\n`;
478
+ }
479
+ }
480
+
481
+ // Children tasks
482
+ if (ctx.children?.length > 0) {
483
+ text += `### Sub-tasks\n\n`;
484
+ ctx.children.forEach(c => {
485
+ text += `- ${c.ticketKey} — ${c.title} (${c.status})\n`;
486
+ });
487
+ text += `\n`;
488
+ }
489
+
490
+ // Repository
491
+ if (ctx.repository) {
492
+ text += `### Target Repository\n\n`;
493
+ text += `- **${ctx.repository.name}** (${ctx.repository.repoType}) — ${ctx.repository.stacks?.join(', ') || ''}\n`;
494
+ text += ` URL: ${ctx.repository.repoUrl}\n`;
495
+ text += ` Base branch: ${ctx.repository.branchBase}\n`;
496
+ if (ctx.repository.profile) {
497
+ text += ` Role: ${ctx.repository.profile.role}`;
498
+ if (ctx.repository.profile.domainOwned) text += `, Domain: ${ctx.repository.profile.domainOwned}`;
499
+ text += `\n`;
500
+ }
501
+ text += `\n`;
502
+ }
503
+
504
+ // Artefacts
505
+ if (ctx.artefacts.requirements) {
506
+ text += `### Requirements (v${ctx.artefacts.requirements.version})\n\n${ctx.artefacts.requirements.content}\n\n`;
507
+ }
508
+ if (ctx.artefacts.technicalAnalysis) {
509
+ text += `### Technical Analysis (v${ctx.artefacts.technicalAnalysis.version})\n\n${ctx.artefacts.technicalAnalysis.content}\n\n`;
510
+ }
511
+ if (ctx.artefacts.architecture) {
512
+ text += `### Architecture (v${ctx.artefacts.architecture.version})\n\n${ctx.artefacts.architecture.content}\n\n`;
513
+ }
514
+
515
+ return {
516
+ content: [{
517
+ type: 'text',
518
+ text,
519
+ }],
520
+ };
521
+ }
522
+ );
523
+
524
+ // Tool: Start work on a ticket
525
+ server.tool(
526
+ 'start_work',
527
+ 'Start work on an assigned ticket in Konductro. Creates a branch via ADO, pushes the ticket spec file, and sets the ticket to in_progress.',
528
+ {
529
+ ticketId: z.string().describe('The ticket ID to start work on'),
530
+ branchFrom: z.string().optional().describe('Base branch to branch from (defaults to repo base branch, e.g. dev)'),
531
+ branchName: z.string().optional().describe('Branch name to create (defaults to feature/{TICKET-KEY})'),
532
+ },
533
+ async ({ ticketId, branchFrom, branchName }) => {
534
+ try {
535
+ const body = {};
536
+ if (branchFrom) body.branchFrom = branchFrom;
537
+ if (branchName) body.branchName = branchName;
538
+
539
+ const result = await konductroFetch(`/api/cli/tickets/${ticketId}/start-work`, {
540
+ method: 'POST',
541
+ body: JSON.stringify(body),
542
+ });
543
+
544
+ let text = `## Work Started\n\n`;
545
+ text += `**${result.ticketKey}** — ${result.title ?? 'Ticket'}\n`;
546
+ text += `**Branch:** ${result.branchName}\n`;
547
+ text += `**Branched from:** ${result.branchFrom}\n`;
548
+ text += `**Status:** in_progress\n\n`;
549
+ text += `To check out the branch locally:\n\`\`\`\ngit fetch && git checkout ${result.branchName}\n\`\`\``;
550
+
551
+ return { content: [{ type: 'text', text }] };
552
+ } catch (err) {
553
+ return {
554
+ content: [{
555
+ type: 'text',
556
+ text: `## Error\n\n${err.message}`,
557
+ }],
558
+ };
559
+ }
560
+ }
561
+ );
562
+
563
+ // Tool: Create a pull request for a ticket
564
+ server.tool(
565
+ 'create_pr',
566
+ 'Create a pull request for a ticket in Konductro. Creates the PR on the git provider (ADO/Gitea), auto-assigns a reviewer based on project rules, and moves the ticket to pr_open status.',
567
+ {
568
+ ticketId: z.string().describe('The ticket ID or key (e.g. MRB-18) to create a PR for. Ticket must be in_progress with a branch.'),
569
+ },
570
+ async ({ ticketId }) => {
571
+ try {
572
+ const result = await konductroFetch(`/api/cli/tickets/${ticketId}/create-pr`, {
573
+ method: 'POST',
574
+ body: JSON.stringify({}),
575
+ });
576
+
577
+ let text = `## Pull Request Created\n\n`;
578
+ text += `**${result.ticketKey}** — ${result.title ?? 'Ticket'}\n`;
579
+ text += `**PR URL:** ${result.prUrl}\n`;
580
+ text += `**Status:** ${result.status}\n`;
581
+ if (result.reviewer) {
582
+ text += `**Reviewer:** ${result.reviewer.name}\n`;
583
+ }
584
+ text += `\n[View PR](${result.prUrl})`;
585
+
586
+ return { content: [{ type: 'text', text }] };
587
+ } catch (err) {
588
+ return {
589
+ content: [{
590
+ type: 'text',
591
+ text: `## Error\n\n${err.message}`,
592
+ }],
593
+ };
594
+ }
595
+ }
596
+ );
597
+
598
+ // Tool: Profile repository — scan local codebase and submit to Konductro
599
+ server.tool(
600
+ 'profile_repository',
601
+ 'Scan the current repository and submit its profile (tech stack, dependencies, APIs) to Konductro. Run this from the root of a cloned repository.',
602
+ {
603
+ repoUrl: z.string().describe('The git remote URL of the repository (run: git remote get-url origin)'),
604
+ role: z.string().optional().describe('Role of this repo: application, library, service, infrastructure'),
605
+ domainOwned: z.string().optional().describe('Business domain this repo owns (e.g., payments, auth, booking)'),
606
+ apisExposed: z.array(z.object({
607
+ method: z.string(),
608
+ path: z.string(),
609
+ description: z.string().optional(),
610
+ })).optional().describe('API endpoints exposed by this repo'),
611
+ dependencies: z.array(z.object({
612
+ name: z.string(),
613
+ version: z.string().optional(),
614
+ type: z.string().optional(),
615
+ })).optional().describe('Key dependencies (frameworks, libraries)'),
616
+ eventsProduced: z.array(z.string()).optional().describe('Events/messages this repo produces'),
617
+ eventsConsumed: z.array(z.string()).optional().describe('Events/messages this repo consumes'),
618
+ },
619
+ async ({ repoUrl, role, domainOwned, apisExposed, dependencies, eventsProduced, eventsConsumed }) => {
620
+ if (!KONDUCTRO_URL || !CLI_TOKEN) {
621
+ return {
622
+ content: [{
623
+ type: 'text',
624
+ text: 'Konductro is not configured. Please set your Konductro URL and CLI token.',
625
+ }],
626
+ };
627
+ }
628
+
629
+ try {
630
+ const result = await konductroFetch('/api/cli/repo-profile', {
631
+ method: 'POST',
632
+ body: JSON.stringify({
633
+ repoUrl,
634
+ profile: {
635
+ role: role || 'application',
636
+ domainOwned: domainOwned || '',
637
+ apisExposed: apisExposed || [],
638
+ dependencies: dependencies || [],
639
+ eventsProduced: eventsProduced || [],
640
+ eventsConsumed: eventsConsumed || [],
641
+ },
642
+ }),
643
+ });
644
+
645
+ return {
646
+ content: [{
647
+ type: 'text',
648
+ text: `## Repository Profile Submitted\n\n${result.message}\n\n- Repository: ${result.repoName}\n- Profile ID: ${result.profileId}`,
649
+ }],
650
+ };
651
+ } catch (err) {
652
+ return {
653
+ content: [{
654
+ type: 'text',
655
+ text: `## Error\n\n${err.message}\n\nMake sure this repository is registered in Konductro and the git remote URL matches.`,
656
+ }],
657
+ };
658
+ }
659
+ }
660
+ );
661
+
662
+ // Tool: Find prototypes for a project (by git remote URL)
663
+ server.tool(
664
+ 'find_project_prototypes',
665
+ 'Find available UX prototypes for a project by matching the git remote URL to a Konductro project',
666
+ {
667
+ repoUrl: z.string().describe('The git remote URL of the repository (e.g. from git remote get-url origin)'),
668
+ },
669
+ async ({ repoUrl }) => {
670
+ if (!KONDUCTRO_URL || !CLI_TOKEN) {
671
+ return {
672
+ content: [{
673
+ type: 'text',
674
+ text: 'Konductro is not configured. Please set your Konductro URL and CLI token in the plugin settings.',
675
+ }],
676
+ };
677
+ }
678
+
679
+ try {
680
+ const result = await konductroFetch(`/api/cli/project-prototypes?repoUrl=${encodeURIComponent(repoUrl)}`);
681
+
682
+ if (!result.prototypes || result.prototypes.length === 0) {
683
+ return {
684
+ content: [{
685
+ type: 'text',
686
+ text: 'No prototypes found for this project.',
687
+ }],
688
+ };
689
+ }
690
+
691
+ const list = result.prototypes.map((p, i) => {
692
+ const fileList = p.files.map(f => ` - [${f.filename}](${f.url})`).join('\n');
693
+ return `### ${i + 1}. ${p.engagementName} (v${p.version})\n` +
694
+ `Project: ${p.projectName}\n` +
695
+ `Index: ${p.indexUrl}\n` +
696
+ `Files:\n${fileList}`;
697
+ }).join('\n\n');
698
+
699
+ return {
700
+ content: [{
701
+ type: 'text',
702
+ text: `## Prototypes Found\n\n${list}`,
703
+ }],
704
+ };
705
+ } catch (err) {
706
+ return {
707
+ content: [{
708
+ type: 'text',
709
+ text: `## Error\n\n${err.message}\n\nMake sure this repository is registered in Konductro.`,
710
+ }],
711
+ };
712
+ }
713
+ }
714
+ );
715
+
716
+ // ─── Prototype Tasks ────────────────────────────────────────────────────────
717
+
718
+ // Tool: List pending prototype tasks
719
+ server.tool(
720
+ 'list_prototype_tasks',
721
+ 'List your pending prototype build assignments from Konductro',
722
+ {},
723
+ async () => {
724
+ if (!KONDUCTRO_URL || !CLI_TOKEN) {
725
+ return {
726
+ content: [{
727
+ type: 'text',
728
+ text: 'Konductro is not configured. Please set your Konductro URL and CLI token in the plugin settings.',
729
+ }],
730
+ };
731
+ }
732
+
733
+ const tasks = await konductroFetch('/api/cli/prototype-tasks');
734
+
735
+ if (tasks.length === 0) {
736
+ return {
737
+ content: [{
738
+ type: 'text',
739
+ text: 'No pending prototype tasks assigned to you.',
740
+ }],
741
+ };
742
+ }
743
+
744
+ const taskList = tasks.map((t, i) =>
745
+ `${i + 1}. **${t.engagementName}**\n Project: ${t.projectName} (${t.clientName})\n Status: ${t.status}\n Job ID: \`${t.jobId}\``
746
+ ).join('\n\n');
747
+
748
+ return {
749
+ content: [{
750
+ type: 'text',
751
+ text: `## Your Prototype Tasks\n\n${taskList}\n\nUse the **get_prototype_context** tool with the job ID to load the full context.`,
752
+ }],
753
+ };
754
+ }
755
+ );
756
+
757
+ // Tool: Get full context for a prototype task
758
+ server.tool(
759
+ 'get_prototype_context',
760
+ 'Load the full context (design system, approved documents, preview HTML) for a prototype build task',
761
+ {
762
+ jobId: z.string().describe('The job ID from the prototype task list'),
763
+ },
764
+ async ({ jobId }) => {
765
+ const ctx = await konductroFetch(`/api/cli/prototype-tasks/${jobId}/context`);
766
+
767
+ let contextText = `## Prototype Build Context\n\n`;
768
+ contextText += `**Engagement:** ${ctx.engagement.name}\n`;
769
+ contextText += `**Project:** ${ctx.project.name} (${ctx.project.clientName})\n`;
770
+ if (ctx.engagement.description) {
771
+ contextText += `**Description:** ${ctx.engagement.description}\n`;
772
+ }
773
+ contextText += `**S3 Prefix:** ${ctx.s3.prefix}\n`;
774
+ contextText += `**Version:** ${ctx.s3.version}\n\n`;
775
+
776
+ if (ctx.designSystem) {
777
+ contextText += `### Design System${ctx.designSystem.name ? ` — ${ctx.designSystem.name}` : ''}\n\n`;
778
+ if (ctx.designSystem.tokens) {
779
+ contextText += `**Tokens:**\n\`\`\`json\n${JSON.stringify(ctx.designSystem.tokens, null, 2)}\n\`\`\`\n\n`;
780
+ }
781
+ if (ctx.designSystem.components) {
782
+ contextText += `**Components:**\n\`\`\`json\n${JSON.stringify(ctx.designSystem.components, null, 2)}\n\`\`\`\n\n`;
783
+ }
784
+ }
785
+
786
+ if (ctx.approvedDocuments?.length) {
787
+ contextText += `### Approved Documents\n\n`;
788
+ for (const doc of ctx.approvedDocuments) {
789
+ contextText += `#### ${doc.title} (${doc.type})\n\n${doc.content}\n\n---\n\n`;
790
+ }
791
+ }
792
+
793
+ if (ctx.previewHtml) {
794
+ contextText += `### Design Studio Preview HTML\n\nA preview screen was locked in the Design Studio step. The HTML is ${ctx.previewHtml.length} characters long. Use this as visual direction for the prototype.\n\n\`\`\`html\n${ctx.previewHtml.slice(0, 3000)}${ctx.previewHtml.length > 3000 ? '\n... (truncated)' : ''}\n\`\`\`\n`;
795
+ }
796
+
797
+ return {
798
+ content: [{
799
+ type: 'text',
800
+ text: contextText,
801
+ }],
802
+ };
803
+ }
804
+ );
805
+
806
+ // Tool: Start work on a prototype task
807
+ server.tool(
808
+ 'start_prototype_work',
809
+ 'Mark a prototype task as in progress — call this before building the prototype',
810
+ {
811
+ jobId: z.string().describe('The job ID of the prototype task'),
812
+ },
813
+ async ({ jobId }) => {
814
+ const result = await konductroFetch(`/api/cli/prototype-tasks/${jobId}/start`, {
815
+ method: 'POST',
816
+ });
817
+
818
+ return {
819
+ content: [{
820
+ type: 'text',
821
+ text: `Prototype task started. Status: **${result.status}**. You can now build the prototype in a \`./prototype/\` directory.`,
822
+ }],
823
+ };
824
+ }
825
+ );
826
+
827
+ // Tool: Request scoped S3 upload credentials
828
+ server.tool(
829
+ 'request_upload_credentials',
830
+ 'Get temporary AWS credentials scoped to upload prototype files to S3',
831
+ {
832
+ jobId: z.string().describe('The job ID of the prototype task'),
833
+ },
834
+ async ({ jobId }) => {
835
+ const creds = await konductroFetch(`/api/cli/prototype-tasks/${jobId}/upload-credentials`, {
836
+ method: 'POST',
837
+ });
838
+
839
+ return {
840
+ content: [{
841
+ type: 'text',
842
+ text: `## Upload Credentials\n\n` +
843
+ `These credentials expire at **${creds.expiresAt}** (15 minutes).\n\n` +
844
+ `- **Bucket:** ${creds.bucket}\n` +
845
+ `- **Region:** ${creds.region}\n` +
846
+ `- **Prefix:** ${creds.prefix}\n` +
847
+ `- **Access Key ID:** ${creds.accessKeyId}\n` +
848
+ `- **Secret Access Key:** ${creds.secretAccessKey}\n` +
849
+ `- **Session Token:** ${creds.sessionToken}\n\n` +
850
+ `Upload each HTML file from the \`./prototype/\` directory to \`s3://${creds.bucket}/${creds.prefix}/\` using these credentials.`,
851
+ }],
852
+ };
853
+ }
854
+ );
855
+
856
+ // Tool: Submit completed prototype
857
+ server.tool(
858
+ 'submit_prototype',
859
+ 'Submit the completed prototype to Konductro — call after uploading all files to S3',
860
+ {
861
+ jobId: z.string().describe('The job ID of the prototype task'),
862
+ pages: z.array(z.object({
863
+ filename: z.string().describe('The filename (e.g. index.html, dashboard.html)'),
864
+ title: z.string().optional().describe('Optional page title'),
865
+ })).describe('List of pages that were uploaded'),
866
+ },
867
+ async ({ jobId, pages }) => {
868
+ const result = await konductroFetch(`/api/cli/prototype-tasks/${jobId}/submit`, {
869
+ method: 'POST',
870
+ body: JSON.stringify({ pages }),
871
+ });
872
+
873
+ return {
874
+ content: [{
875
+ type: 'text',
876
+ text: `## Prototype Submitted\n\n` +
877
+ `Version **${result.version}** published with ${pages.length} page(s).\n\n` +
878
+ (result.prototypeUrl ? `**URL:** ${result.prototypeUrl}\n\n` : '') +
879
+ `The SDM has been notified. Task complete.`,
880
+ }],
881
+ };
882
+ }
883
+ );
884
+
885
+ // Start the server
886
+ async function main() {
887
+ const transport = new StdioServerTransport();
888
+ await server.connect(transport);
889
+ }
890
+
891
+ main().catch((error) => {
892
+ console.error('Failed to start Konductro MCP server:', error);
893
+ process.exit(1);
894
+ });