@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.
- package/.claude-plugin/plugin.json +29 -0
- package/README.md +107 -0
- package/bin/postinstall.js +13 -0
- package/bin/setup.js +148 -0
- package/package.json +28 -0
- package/servers/konductro-api.js +894 -0
- package/skills/decompose/SKILL.md +97 -0
- package/skills/find-prototype/SKILL.md +57 -0
- package/skills/profile-repository/SKILL.md +52 -0
- package/skills/prototype/SKILL.md +92 -0
- package/skills/start-ticket/SKILL.md +56 -0
- package/skills/technical-analysis/SKILL.md +83 -0
|
@@ -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
|
+
});
|