@vibekiln/cutline-mcp-cli 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 (43) hide show
  1. package/Dockerfile +11 -0
  2. package/README.md +248 -0
  3. package/dist/auth/callback.d.ts +6 -0
  4. package/dist/auth/callback.js +97 -0
  5. package/dist/auth/keychain.d.ts +3 -0
  6. package/dist/auth/keychain.js +16 -0
  7. package/dist/commands/init.d.ts +4 -0
  8. package/dist/commands/init.js +309 -0
  9. package/dist/commands/login.d.ts +7 -0
  10. package/dist/commands/login.js +166 -0
  11. package/dist/commands/logout.d.ts +1 -0
  12. package/dist/commands/logout.js +25 -0
  13. package/dist/commands/serve.d.ts +1 -0
  14. package/dist/commands/serve.js +38 -0
  15. package/dist/commands/setup.d.ts +5 -0
  16. package/dist/commands/setup.js +278 -0
  17. package/dist/commands/status.d.ts +3 -0
  18. package/dist/commands/status.js +127 -0
  19. package/dist/commands/upgrade.d.ts +3 -0
  20. package/dist/commands/upgrade.js +112 -0
  21. package/dist/index.d.ts +2 -0
  22. package/dist/index.js +64 -0
  23. package/dist/servers/chunk-DE7R7WKY.js +331 -0
  24. package/dist/servers/chunk-KMUSQOTJ.js +47 -0
  25. package/dist/servers/chunk-OP4EO6FV.js +454 -0
  26. package/dist/servers/chunk-UBBAYTW3.js +946 -0
  27. package/dist/servers/chunk-ZVWDXO6M.js +1063 -0
  28. package/dist/servers/cutline-server.js +10448 -0
  29. package/dist/servers/data-client-FPUZBUO3.js +160 -0
  30. package/dist/servers/exploration-server.js +930 -0
  31. package/dist/servers/graph-metrics-DCNR7JZN.js +12 -0
  32. package/dist/servers/integrations-server.js +107 -0
  33. package/dist/servers/output-server.js +107 -0
  34. package/dist/servers/premortem-server.js +971 -0
  35. package/dist/servers/tools-server.js +287 -0
  36. package/dist/utils/config-store.d.ts +8 -0
  37. package/dist/utils/config-store.js +35 -0
  38. package/dist/utils/config.d.ts +22 -0
  39. package/dist/utils/config.js +48 -0
  40. package/mcpb/manifest.json +77 -0
  41. package/package.json +76 -0
  42. package/server.json +42 -0
  43. package/smithery.yaml +10 -0
@@ -0,0 +1,971 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ RunInputSchema
4
+ } from "./chunk-DE7R7WKY.js";
5
+ import {
6
+ isWriteTool
7
+ } from "./chunk-KMUSQOTJ.js";
8
+ import {
9
+ guardBoundary,
10
+ guardOutput,
11
+ withPerfTracking
12
+ } from "./chunk-OP4EO6FV.js";
13
+ import {
14
+ cfPremortemChatAgent,
15
+ cfPremortemKick,
16
+ cfRegenAssumptions,
17
+ cfRegenExperiments,
18
+ createPremortem,
19
+ getChat,
20
+ getPremortem,
21
+ listPremortems,
22
+ mapErrorToMcp,
23
+ requirePremiumWithAutoAuth,
24
+ resolveAuthContext,
25
+ saveChat,
26
+ updateChat,
27
+ updatePremortem,
28
+ validateAuth,
29
+ validateRequestSize
30
+ } from "./chunk-ZVWDXO6M.js";
31
+
32
+ // ../mcp/dist/mcp/src/premortem-server.js
33
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
34
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
35
+ import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from "@modelcontextprotocol/sdk/types.js";
36
+
37
+ // ../mcp/dist/mcp/src/premortem-handoff.js
38
+ function buildBrief(ctx) {
39
+ if (!ctx)
40
+ return "";
41
+ const parts = [];
42
+ if (ctx.brief)
43
+ parts.push(ctx.brief);
44
+ if (ctx.problemSolved && !ctx.brief?.toLowerCase().includes(ctx.problemSolved.toLowerCase().slice(0, 20)))
45
+ parts.push(`Problem: ${ctx.problemSolved}`);
46
+ if (ctx.uniqueValue && !ctx.brief?.toLowerCase().includes(ctx.uniqueValue.toLowerCase().slice(0, 20)))
47
+ parts.push(`Unique value: ${ctx.uniqueValue}`);
48
+ if (ctx.targetUser && !ctx.brief?.toLowerCase().includes(ctx.targetUser.toLowerCase().slice(0, 15)))
49
+ parts.push(`Target users: ${ctx.targetUser}`);
50
+ if (ctx.businessModel && !ctx.brief?.toLowerCase().includes(ctx.businessModel.toLowerCase().slice(0, 15)))
51
+ parts.push(`Business model: ${ctx.businessModel}`);
52
+ return parts.join(". ").trim() || "No description provided.";
53
+ }
54
+ function buildReferenceClasses(competitors) {
55
+ if (competitors && competitors.length > 0) {
56
+ return competitors.filter((c) => c.name && c.name.length > 0).map((c) => c.name).slice(0, 5);
57
+ }
58
+ return ["General market alternatives"];
59
+ }
60
+ function buildSeedPersonas(ctx) {
61
+ if (!ctx.productContext?.targetUser)
62
+ return void 0;
63
+ return {
64
+ user_personas: [{ name: "Primary User", description: ctx.productContext.targetUser }]
65
+ };
66
+ }
67
+ function buildRunInput(request) {
68
+ const { projectName, conversationalContext: ctx, mode = "product", clientContext } = request;
69
+ let brief = buildBrief(ctx.productContext);
70
+ if (ctx.risks.length > 0) {
71
+ const topRisks = ctx.risks.filter((r) => r.severity === "critical" || r.severity === "high").slice(0, 2).map((r) => r.title);
72
+ if (topRisks.length > 0)
73
+ brief += ` Key concerns to investigate: ${topRisks.join(", ")}.`;
74
+ }
75
+ if (ctx.verdict?.summary)
76
+ brief += ` Initial assessment: ${ctx.verdict.summary}`;
77
+ const runInput = {
78
+ mode,
79
+ project: { name: projectName, brief, reference_classes: buildReferenceClasses(ctx.competitors) }
80
+ };
81
+ if (mode === "consulting" && clientContext) {
82
+ runInput.client = {
83
+ name: clientContext.name,
84
+ industry: clientContext.industry,
85
+ size: clientContext.size,
86
+ stakeholders: clientContext.stakeholders
87
+ };
88
+ }
89
+ const seedPersonas = buildSeedPersonas(ctx);
90
+ if (seedPersonas)
91
+ runInput.seed_personas = seedPersonas;
92
+ if (ctx.risks.length > 0 || ctx.assumptions.length > 0 || ctx.competitors.length > 0) {
93
+ runInput.conversational_artifacts = {
94
+ risks: ctx.risks.length > 0 ? ctx.risks.map((r) => ({
95
+ title: r.title,
96
+ description: r.description,
97
+ category: r.category,
98
+ severity: r.severity,
99
+ likelihood: r.likelihood,
100
+ impact: r.impact
101
+ })) : void 0,
102
+ assumptions: ctx.assumptions.length > 0 ? ctx.assumptions.map((a) => ({
103
+ statement: a.statement,
104
+ category: a.category,
105
+ confidence: a.confidence,
106
+ importance: a.importance
107
+ })) : void 0,
108
+ competitors: ctx.competitors.length > 0 ? ctx.competitors.map((c) => ({
109
+ name: c.name,
110
+ description: c.description,
111
+ threat_level: c.threatLevel
112
+ })) : void 0,
113
+ conversation_summary: ctx.verdict?.summary
114
+ };
115
+ }
116
+ return runInput;
117
+ }
118
+ function validateForGraduation(ctx) {
119
+ const errors = [];
120
+ const warnings = [];
121
+ if (!ctx.productContext?.brief && !ctx.productContext?.problemSolved)
122
+ errors.push("No product description found. Please describe what you're building first.");
123
+ if (ctx.assumptions.length === 0)
124
+ warnings.push("No assumptions were surfaced. The full analysis will discover them.");
125
+ if (ctx.risks.length === 0)
126
+ warnings.push("No risks were identified. The full analysis will analyze risks in depth.");
127
+ if (ctx.competitors.length === 0)
128
+ warnings.push("No competitors were discussed. Provide reference companies for better analysis.");
129
+ return { valid: errors.length === 0, errors, warnings };
130
+ }
131
+ function buildGraduationMetadata(sessionId, ctx, currentAct) {
132
+ return {
133
+ sourceType: "conversational_premortem",
134
+ sourceSessionId: sessionId,
135
+ graduatedAt: Date.now(),
136
+ conversationSummary: {
137
+ actsCompleted: currentAct,
138
+ assumptionsCount: ctx.assumptions.length,
139
+ risksCount: ctx.risks.length,
140
+ competitorsCount: ctx.competitors.length,
141
+ hasVerdict: !!ctx.verdict
142
+ }
143
+ };
144
+ }
145
+
146
+ // ../mcp/dist/mcp/src/premortem-server.js
147
+ var ACT_NAMES = {
148
+ 1: "Product Understanding",
149
+ 2: "Assumption Surfacing",
150
+ 3: "Risk Discovery",
151
+ 4: "Competitive Landscape",
152
+ 5: "Verdict & Next Steps"
153
+ };
154
+ var chatSessionCache = /* @__PURE__ */ new Map();
155
+ function generateChatSessionId() {
156
+ return `pmc_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
157
+ }
158
+ async function saveChatSession(session) {
159
+ session.updatedAt = Date.now();
160
+ chatSessionCache.set(session.id, session);
161
+ if (session.uid) {
162
+ try {
163
+ await saveChat(session.id, { ...session });
164
+ } catch (e) {
165
+ console.error("[PremortemChat] Failed to persist session:", e);
166
+ }
167
+ }
168
+ }
169
+ async function getChatSession(sessionId) {
170
+ if (chatSessionCache.has(sessionId)) {
171
+ return chatSessionCache.get(sessionId);
172
+ }
173
+ try {
174
+ const data = await getChat(sessionId);
175
+ if (data) {
176
+ chatSessionCache.set(sessionId, data);
177
+ return data;
178
+ }
179
+ } catch (e) {
180
+ console.error("[PremortemChat] Failed to load session:", e);
181
+ }
182
+ return null;
183
+ }
184
+ async function createChatSession(initialInput, uid) {
185
+ const session = {
186
+ id: generateChatSessionId(),
187
+ uid,
188
+ currentAct: 1,
189
+ initialInput,
190
+ assumptions: [],
191
+ risks: [],
192
+ competitors: [],
193
+ conversationHistory: [],
194
+ createdAt: Date.now(),
195
+ updatedAt: Date.now()
196
+ };
197
+ await saveChatSession(session);
198
+ return session;
199
+ }
200
+ function sessionToContext(session) {
201
+ return {
202
+ currentAct: session.currentAct,
203
+ initialInput: session.initialInput,
204
+ productContext: session.productContext,
205
+ assumptions: session.assumptions,
206
+ risks: session.risks,
207
+ competitors: session.competitors,
208
+ conversationHistory: session.conversationHistory
209
+ };
210
+ }
211
+ function canGraduateSession(session) {
212
+ if (session.verdict) {
213
+ return { canGraduate: true };
214
+ }
215
+ if (session.currentAct < 3) {
216
+ return { canGraduate: false, reason: "Need to complete at least Act 3 (Risk Discovery)" };
217
+ }
218
+ const hasProductContext = session.productContext?.brief;
219
+ const hasMinimumContent = session.assumptions.length >= 2 || session.risks.length >= 2;
220
+ if (!hasProductContext) {
221
+ return { canGraduate: false, reason: "Need product context before graduation" };
222
+ }
223
+ if (!hasMinimumContent) {
224
+ return { canGraduate: false, reason: "Need at least 2 assumptions or risks before graduation" };
225
+ }
226
+ return { canGraduate: true };
227
+ }
228
+ var server = new Server({
229
+ name: "cutline-premortem",
230
+ version: "0.1.0"
231
+ }, {
232
+ capabilities: {
233
+ tools: {}
234
+ }
235
+ });
236
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
237
+ return {
238
+ tools: [
239
+ {
240
+ name: "premortem_run",
241
+ description: "Run a synchronous pre-mortem generation",
242
+ inputSchema: {
243
+ type: "object",
244
+ properties: {
245
+ input: { type: "object", description: "RunInput object" },
246
+ auth_token: { type: "string", description: "Optional Firebase ID token" }
247
+ },
248
+ required: ["input"]
249
+ }
250
+ },
251
+ {
252
+ name: "premortem_queue",
253
+ description: "Enqueue an async pre-mortem job",
254
+ inputSchema: {
255
+ type: "object",
256
+ properties: {
257
+ input: { type: "object", description: "RunInput object" },
258
+ auth_token: { type: "string" }
259
+ },
260
+ required: ["input"]
261
+ }
262
+ },
263
+ {
264
+ name: "premortem_status",
265
+ description: "Get status of a pre-mortem job",
266
+ inputSchema: {
267
+ type: "object",
268
+ properties: {
269
+ jobId: { type: "string" },
270
+ auth_token: { type: "string" }
271
+ },
272
+ required: ["jobId"]
273
+ }
274
+ },
275
+ {
276
+ name: "premortem_kick",
277
+ description: "Trigger job execution or resume",
278
+ inputSchema: {
279
+ type: "object",
280
+ properties: {
281
+ jobId: { type: "string" },
282
+ auth_token: { type: "string" }
283
+ },
284
+ required: ["jobId"]
285
+ }
286
+ },
287
+ {
288
+ name: "premortem_regen_assumptions",
289
+ description: "Regenerate assumptions section",
290
+ inputSchema: {
291
+ type: "object",
292
+ properties: {
293
+ input: { type: "object" },
294
+ doc: { type: "object" },
295
+ auth_token: { type: "string" }
296
+ },
297
+ required: ["input", "doc"]
298
+ }
299
+ },
300
+ {
301
+ name: "premortem_regen_experiments",
302
+ description: "Regenerate experiments section",
303
+ inputSchema: {
304
+ type: "object",
305
+ properties: {
306
+ input: { type: "object" },
307
+ doc: { type: "object" },
308
+ auth_token: { type: "string" }
309
+ },
310
+ required: ["input", "doc"]
311
+ }
312
+ },
313
+ {
314
+ name: "premortem_list",
315
+ description: "List user's premortems",
316
+ inputSchema: {
317
+ type: "object",
318
+ properties: {
319
+ limit: { type: "number", description: "Max number of items to return" },
320
+ auth_token: { type: "string" }
321
+ }
322
+ }
323
+ },
324
+ {
325
+ name: "premortem_graduate",
326
+ description: "Graduate a conversational premortem to full analysis. Takes context from a chat session and runs the full 9-agent pipeline. Requires premium subscription.",
327
+ inputSchema: {
328
+ type: "object",
329
+ properties: {
330
+ session_id: { type: "string", description: "ID of the conversational premortem session" },
331
+ project_name: { type: "string", description: "Name for the project (required)" },
332
+ conversational_context: {
333
+ type: "object",
334
+ description: "Context gathered from conversational premortem",
335
+ properties: {
336
+ productContext: {
337
+ type: "object",
338
+ properties: {
339
+ name: { type: "string" },
340
+ brief: { type: "string" },
341
+ targetUser: { type: "string" },
342
+ problemSolved: { type: "string" },
343
+ uniqueValue: { type: "string" },
344
+ businessModel: { type: "string" }
345
+ }
346
+ },
347
+ assumptions: {
348
+ type: "array",
349
+ items: {
350
+ type: "object",
351
+ properties: {
352
+ statement: { type: "string" },
353
+ category: { type: "string" },
354
+ confidence: { type: "number" },
355
+ importance: { type: "number" }
356
+ }
357
+ }
358
+ },
359
+ risks: {
360
+ type: "array",
361
+ items: {
362
+ type: "object",
363
+ properties: {
364
+ title: { type: "string" },
365
+ description: { type: "string" },
366
+ category: { type: "string" },
367
+ severity: { type: "string" },
368
+ likelihood: { type: "number" },
369
+ impact: { type: "number" }
370
+ }
371
+ }
372
+ },
373
+ competitors: {
374
+ type: "array",
375
+ items: {
376
+ type: "object",
377
+ properties: {
378
+ name: { type: "string" },
379
+ description: { type: "string" },
380
+ threatLevel: { type: "string" }
381
+ }
382
+ }
383
+ },
384
+ verdict: {
385
+ type: "object",
386
+ properties: {
387
+ recommendation: { type: "string" },
388
+ confidence: { type: "number" },
389
+ summary: { type: "string" }
390
+ }
391
+ }
392
+ }
393
+ },
394
+ auth_token: { type: "string" }
395
+ },
396
+ required: ["session_id", "project_name", "conversational_context"]
397
+ }
398
+ },
399
+ // ==========================================
400
+ // Conversational Premortem Chat Tools
401
+ // ==========================================
402
+ {
403
+ name: "premortem_chat_start",
404
+ description: "Start a conversational premortem session. Guides you through a 5-act stress test of your product idea: Product Understanding \u2192 Assumptions \u2192 Risks \u2192 Competition \u2192 Verdict. FREE to use, graduation to full analysis requires premium.",
405
+ inputSchema: {
406
+ type: "object",
407
+ properties: {
408
+ product_brief: {
409
+ type: "string",
410
+ description: "Brief description of your product or idea to stress-test"
411
+ },
412
+ auth_token: { type: "string", description: "Optional Firebase ID token" }
413
+ },
414
+ required: ["product_brief"]
415
+ }
416
+ },
417
+ {
418
+ name: "premortem_chat",
419
+ description: "Continue a conversational premortem session. Send messages to the AI strategist who will probe your assumptions, identify risks, and help you stress-test your idea.",
420
+ inputSchema: {
421
+ type: "object",
422
+ properties: {
423
+ session_id: { type: "string", description: "Session ID from premortem_chat_start" },
424
+ message: { type: "string", description: "Your response or question" },
425
+ advance_act: {
426
+ type: "boolean",
427
+ description: "If true, advance to the next act when ready"
428
+ },
429
+ auth_token: { type: "string" }
430
+ },
431
+ required: ["session_id", "message"]
432
+ }
433
+ },
434
+ {
435
+ name: "premortem_chat_status",
436
+ description: "Get the current status of a conversational premortem session, including gathered artifacts and graduation readiness.",
437
+ inputSchema: {
438
+ type: "object",
439
+ properties: {
440
+ session_id: { type: "string", description: "Session ID to check" },
441
+ auth_token: { type: "string" }
442
+ },
443
+ required: ["session_id"]
444
+ }
445
+ },
446
+ {
447
+ name: "premortem_chat_graduate",
448
+ description: "Graduate a conversational premortem session to full analysis. Uses the gathered context to run the complete 9-agent pipeline. Requires premium subscription.",
449
+ inputSchema: {
450
+ type: "object",
451
+ properties: {
452
+ session_id: { type: "string", description: "Session ID to graduate" },
453
+ auth_token: { type: "string" }
454
+ },
455
+ required: ["session_id"]
456
+ }
457
+ }
458
+ ]
459
+ };
460
+ });
461
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
462
+ try {
463
+ const { name, arguments: rawArgs } = request.params;
464
+ if (!rawArgs)
465
+ throw new McpError(ErrorCode.InvalidParams, "Missing arguments");
466
+ validateRequestSize(rawArgs);
467
+ const { args } = guardBoundary(name, rawArgs);
468
+ const rawResponse = await withPerfTracking(name, async () => {
469
+ if (isWriteTool(name)) {
470
+ const peekToken = args.auth_token;
471
+ if (peekToken) {
472
+ const peekDecoded = await validateAuth(peekToken).catch(() => null);
473
+ if (peekDecoded && peekDecoded.accountType === "agent") {
474
+ throw new McpError(ErrorCode.InvalidRequest, "This is a read-only agent account. Write operations require the owner account.");
475
+ }
476
+ }
477
+ }
478
+ switch (name) {
479
+ case "premortem_run": {
480
+ console.error(`[Premortem DEBUG] Starting premortem_run...`);
481
+ const { input, auth_token } = args;
482
+ console.error(`[Premortem DEBUG] Step 1: Authenticating...`);
483
+ const decoded = await requirePremiumWithAutoAuth(auth_token);
484
+ console.error(`[Premortem DEBUG] Step 1 done: UID=${decoded?.uid}`);
485
+ console.error(`[Premortem DEBUG] Step 2: Parsing input...`);
486
+ const parsedInput = RunInputSchema.parse(input);
487
+ console.error(`[Premortem DEBUG] Step 2 done: project=${parsedInput.project.name}`);
488
+ console.error(`[Premortem] Queuing analysis for: ${parsedInput.project.name}`);
489
+ const startTime = Date.now();
490
+ console.error(`[Premortem DEBUG] Step 4: Creating job document...`);
491
+ const { id: jobId } = await createPremortem({
492
+ status: "queued",
493
+ payload: parsedInput,
494
+ uid: decoded?.uid || null,
495
+ source: "mcp_sync"
496
+ });
497
+ console.error(`[Premortem DEBUG] Step 4 done: jobId=${jobId}`);
498
+ console.error(`[Premortem] Job queued: ${jobId}, polling for completion...`);
499
+ const maxWaitMs = 10 * 60 * 1e3;
500
+ const pollIntervalMs = 3e3;
501
+ let lastStage = "";
502
+ let kickedFallback = false;
503
+ while (Date.now() - startTime < maxWaitMs) {
504
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
505
+ const data = await getPremortem(jobId) || {};
506
+ if (!kickedFallback && data.status === "queued" && Date.now() - startTime > 1e4) {
507
+ console.error(`[Premortem] Job still queued after 10s, kicking via Cloud Function...`);
508
+ kickedFallback = true;
509
+ cfPremortemKick(jobId).catch((e) => console.error(`[Premortem] Kick fallback error:`, e?.message));
510
+ }
511
+ const stage = data.stage_label || data.stage;
512
+ const progress = data.progress;
513
+ if (stage && stage !== lastStage) {
514
+ lastStage = stage;
515
+ const pct = progress ? ` (${Math.round(progress * 100)}%)` : "";
516
+ console.error(`[Premortem] ${stage}${pct}`);
517
+ }
518
+ if (data.status === "completed") {
519
+ const elapsed2 = ((Date.now() - startTime) / 1e3).toFixed(1);
520
+ console.error(`[Premortem] Complete in ${elapsed2}s`);
521
+ const result = data.result || {};
522
+ result._jobId = jobId;
523
+ return {
524
+ content: [{ type: "text", text: JSON.stringify(result) }]
525
+ };
526
+ }
527
+ if (data.status === "failed" || data.status === "error") {
528
+ throw new McpError(ErrorCode.InternalError, `Job failed: ${data.error || "Unknown error"}`);
529
+ }
530
+ }
531
+ const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
532
+ console.error(`[Premortem] Timed out after ${elapsed}s, job still running`);
533
+ return {
534
+ content: [{ type: "text", text: JSON.stringify({
535
+ status: "running",
536
+ jobId,
537
+ message: `Job is still running after ${elapsed}s. Use premortem_status to check progress.`
538
+ }) }]
539
+ };
540
+ }
541
+ case "premortem_queue": {
542
+ const { input, auth_token } = args;
543
+ const decoded = await requirePremiumWithAutoAuth(auth_token);
544
+ const parsedInput = RunInputSchema.parse(input);
545
+ const { id: queuedJobId } = await createPremortem({
546
+ status: "queued",
547
+ payload: parsedInput,
548
+ uid: decoded?.uid || null
549
+ });
550
+ return {
551
+ content: [{ type: "text", text: JSON.stringify({ jobId: queuedJobId }) }]
552
+ };
553
+ }
554
+ case "premortem_status": {
555
+ const { jobId, auth_token } = args;
556
+ const { effectiveUid } = await resolveAuthContext(auth_token);
557
+ const data = await getPremortem(jobId);
558
+ if (!data)
559
+ throw new McpError(ErrorCode.InvalidRequest, "Job not found");
560
+ const allowPublic = process.env.ALLOW_PUBLIC_PREMORTEM === "true";
561
+ if (!allowPublic && effectiveUid && data.uid && data.uid !== effectiveUid) {
562
+ throw new McpError(ErrorCode.InvalidRequest, "You do not have permission to access this job");
563
+ }
564
+ const status = {
565
+ status: data.status,
566
+ result: data.result || null,
567
+ error: data.error || null,
568
+ progress: data.progress,
569
+ stage: data.stage,
570
+ stage_label: data.stage_label,
571
+ updatedAt: data.updatedAt
572
+ };
573
+ return {
574
+ content: [{ type: "text", text: JSON.stringify(status) }]
575
+ };
576
+ }
577
+ case "premortem_kick": {
578
+ console.error(`[Premortem Kick] Starting kick for job...`);
579
+ const { jobId, auth_token } = args;
580
+ const decoded = await requirePremiumWithAutoAuth(auth_token);
581
+ console.error(`[Premortem Kick] Authenticated: UID=${decoded?.uid}`);
582
+ console.error(`[Premortem Kick] Calling Cloud Function for job ${jobId}...`);
583
+ try {
584
+ const result = await cfPremortemKick(jobId);
585
+ console.error(`[Premortem Kick] Cloud Function returned:`, result);
586
+ return {
587
+ content: [{ type: "text", text: JSON.stringify({
588
+ kicked: result.kicked,
589
+ status: result.status,
590
+ jobId,
591
+ message: result.status === "completed" ? "\u2615 Full analysis complete! Use premortem_status to view results." : "Job processed. Use premortem_status to check results."
592
+ }) }]
593
+ };
594
+ } catch (e) {
595
+ console.error(`[Premortem Kick] Cloud Function error:`, e?.message || e);
596
+ throw new McpError(ErrorCode.InternalError, `Failed to process job: ${e?.message || "Unknown error"}`);
597
+ }
598
+ }
599
+ case "premortem_regen_assumptions": {
600
+ const { input, doc, auth_token } = args;
601
+ await requirePremiumWithAutoAuth(auth_token);
602
+ const out = await cfRegenAssumptions(input, doc);
603
+ return {
604
+ content: [{ type: "text", text: JSON.stringify(out) }]
605
+ };
606
+ }
607
+ case "premortem_regen_experiments": {
608
+ const { input, doc, auth_token } = args;
609
+ await requirePremiumWithAutoAuth(auth_token);
610
+ const out = await cfRegenExperiments(input, doc);
611
+ return {
612
+ content: [{ type: "text", text: JSON.stringify(out) }]
613
+ };
614
+ }
615
+ case "premortem_list": {
616
+ const { limit = 10, auth_token } = args;
617
+ const { effectiveUid } = await resolveAuthContext(auth_token);
618
+ const premortems = await listPremortems({ limit });
619
+ return {
620
+ content: [{ type: "text", text: JSON.stringify(premortems, null, 2) }]
621
+ };
622
+ }
623
+ case "premortem_graduate": {
624
+ const { session_id, project_name, conversational_context, auth_token } = args;
625
+ if (!session_id || typeof session_id !== "string") {
626
+ throw new McpError(ErrorCode.InvalidParams, "session_id is required");
627
+ }
628
+ if (!project_name || typeof project_name !== "string" || project_name.trim().length === 0) {
629
+ throw new McpError(ErrorCode.InvalidParams, "project_name is required and cannot be empty");
630
+ }
631
+ if (!conversational_context || typeof conversational_context !== "object") {
632
+ throw new McpError(ErrorCode.InvalidParams, "conversational_context is required");
633
+ }
634
+ const decoded = await requirePremiumWithAutoAuth(auth_token);
635
+ const validation = validateForGraduation(conversational_context);
636
+ if (!validation.valid) {
637
+ throw new McpError(ErrorCode.InvalidParams, `Cannot graduate: ${validation.errors.join(", ")}`);
638
+ }
639
+ const runInput = buildRunInput({
640
+ sessionId: session_id,
641
+ projectName: project_name.trim(),
642
+ conversationalContext: conversational_context
643
+ });
644
+ const parsedInput = RunInputSchema.parse(runInput);
645
+ const graduationMetadata = buildGraduationMetadata(
646
+ session_id,
647
+ conversational_context,
648
+ conversational_context.verdict ? 5 : 4
649
+ // Estimate current act
650
+ );
651
+ const { id: gradJobId } = await createPremortem({
652
+ status: "queued",
653
+ payload: parsedInput,
654
+ uid: decoded?.uid || null,
655
+ source: "mcp_graduate",
656
+ graduation: graduationMetadata
657
+ });
658
+ console.error(`[Premortem Graduate] Created job ${gradJobId} from session ${session_id}`);
659
+ try {
660
+ const sessionData = await getPremortem(session_id);
661
+ if (sessionData) {
662
+ await updatePremortem(session_id, {
663
+ linkedJobId: gradJobId,
664
+ graduatedAt: (/* @__PURE__ */ new Date()).toISOString()
665
+ });
666
+ console.error(`[Premortem Graduate] Linked session ${session_id} to job ${gradJobId}`);
667
+ }
668
+ } catch (linkError) {
669
+ console.error(`[Premortem Graduate] Failed to link session:`, linkError);
670
+ }
671
+ return {
672
+ content: [{ type: "text", text: JSON.stringify({
673
+ success: true,
674
+ jobId: gradJobId,
675
+ sourceSessionId: session_id,
676
+ projectName: project_name,
677
+ warnings: validation.warnings,
678
+ message: `\u2615 Full analysis queued! This will take a minute\u2014go grab a cup of coffee. Use premortem_status with jobId "${gradJobId}" to check progress.`
679
+ }, null, 2) }]
680
+ };
681
+ }
682
+ // ==========================================
683
+ // Conversational Premortem Chat Handlers
684
+ // ==========================================
685
+ case "premortem_chat_start": {
686
+ const { product_brief, auth_token } = args;
687
+ if (!product_brief || typeof product_brief !== "string" || product_brief.trim().length < 10) {
688
+ throw new McpError(ErrorCode.InvalidParams, "product_brief is required (min 10 characters)");
689
+ }
690
+ let uid;
691
+ try {
692
+ const decoded = await requirePremiumWithAutoAuth(auth_token).catch(() => null);
693
+ uid = decoded?.uid;
694
+ } catch {
695
+ }
696
+ const session = await createChatSession(product_brief.trim(), uid);
697
+ const welcomeMessage = `Let's stress-test **"${session.initialInput.slice(0, 100)}${session.initialInput.length > 100 ? "..." : ""}"**.
698
+
699
+ I'm going to play devil's advocate and help you find the blind spots, risky assumptions, and potential failure modes in your idea\u2014**before** you invest significant resources.
700
+
701
+ We'll go through 5 phases:
702
+ 1. **Product Understanding** - What exactly are you building?
703
+ 2. **Assumption Surfacing** - What must be true for this to work?
704
+ 3. **Risk Discovery** - What could go wrong?
705
+ 4. **Competitive Landscape** - Who else is here?
706
+ 5. **Verdict & Next Steps** - What should you do?
707
+
708
+ \u{1F4A1} **Tip**: I have access to real-time web research. At any point, ask me to look up competitors, market data, regulations, or industry trends\u2014I'll share what I find.
709
+
710
+ ---
711
+
712
+ **Act 1: Product Understanding**
713
+
714
+ Let's start with the basics. Tell me more about what you're building:
715
+
716
+ - **Who is this for?** (Be specific\u2014role, company size, industry)
717
+ - **What problem does it solve?** How painful is that problem today?
718
+ - **How is it different** from what's already out there?`;
719
+ session.conversationHistory.push({
720
+ role: "assistant",
721
+ content: welcomeMessage
722
+ });
723
+ await saveChatSession(session);
724
+ return {
725
+ content: [{ type: "text", text: JSON.stringify({
726
+ session_id: session.id,
727
+ current_act: session.currentAct,
728
+ act_name: ACT_NAMES[session.currentAct],
729
+ message: welcomeMessage,
730
+ can_graduate: false
731
+ }, null, 2) }]
732
+ };
733
+ }
734
+ case "premortem_chat": {
735
+ const { session_id, message, advance_act, auth_token } = args;
736
+ if (!session_id) {
737
+ throw new McpError(ErrorCode.InvalidParams, "session_id is required");
738
+ }
739
+ if (!message || typeof message !== "string") {
740
+ throw new McpError(ErrorCode.InvalidParams, "message is required");
741
+ }
742
+ try {
743
+ const decoded = await requirePremiumWithAutoAuth(auth_token).catch(() => null);
744
+ } catch {
745
+ }
746
+ const session = await getChatSession(session_id);
747
+ if (!session) {
748
+ throw new McpError(ErrorCode.InvalidRequest, `Session not found: ${session_id}`);
749
+ }
750
+ session.conversationHistory.push({
751
+ role: "user",
752
+ content: message
753
+ });
754
+ if (advance_act && session.currentAct < 5) {
755
+ session.currentAct = session.currentAct + 1;
756
+ }
757
+ let response;
758
+ try {
759
+ const context = sessionToContext(session);
760
+ console.error("[PremortemChat] Calling Cloud Function...");
761
+ response = await cfPremortemChatAgent(message, context);
762
+ console.error("[PremortemChat] Cloud Function response received");
763
+ } catch (e) {
764
+ console.error("[PremortemChat] Cloud Function error:", e?.message || e);
765
+ throw new McpError(ErrorCode.InternalError, `Failed to generate response: ${e.message}`);
766
+ }
767
+ if (response.artifacts) {
768
+ for (const artifact of response.artifacts) {
769
+ switch (artifact.type) {
770
+ case "product_context":
771
+ session.productContext = artifact.data;
772
+ break;
773
+ case "assumption":
774
+ const existingAssumption = session.assumptions.find((a) => a.statement.toLowerCase() === artifact.data.statement?.toLowerCase());
775
+ if (!existingAssumption && artifact.data.statement) {
776
+ session.assumptions.push({
777
+ id: `asm_${Date.now()}_${Math.random().toString(36).slice(2, 5)}`,
778
+ ...artifact.data
779
+ });
780
+ }
781
+ break;
782
+ case "risk":
783
+ const existingRisk = session.risks.find((r) => r.title?.toLowerCase() === artifact.data.title?.toLowerCase());
784
+ if (!existingRisk && artifact.data.title) {
785
+ session.risks.push({
786
+ id: `risk_${Date.now()}_${Math.random().toString(36).slice(2, 5)}`,
787
+ ...artifact.data
788
+ });
789
+ }
790
+ break;
791
+ case "competitor":
792
+ const existingCompetitor = session.competitors.find((c) => c.name?.toLowerCase() === artifact.data.name?.toLowerCase());
793
+ if (!existingCompetitor && artifact.data.name) {
794
+ session.competitors.push({
795
+ id: `comp_${Date.now()}_${Math.random().toString(36).slice(2, 5)}`,
796
+ ...artifact.data
797
+ });
798
+ }
799
+ break;
800
+ case "verdict":
801
+ session.verdict = artifact.data;
802
+ break;
803
+ }
804
+ }
805
+ }
806
+ if (response.suggestedAct && response.suggestedAct !== session.currentAct) {
807
+ session.currentAct = response.suggestedAct;
808
+ }
809
+ session.conversationHistory.push({
810
+ role: "assistant",
811
+ content: response.content
812
+ });
813
+ await saveChatSession(session);
814
+ const graduationStatus = canGraduateSession(session);
815
+ return {
816
+ content: [{ type: "text", text: JSON.stringify({
817
+ session_id: session.id,
818
+ current_act: session.currentAct,
819
+ act_name: ACT_NAMES[session.currentAct],
820
+ message: response.content,
821
+ artifacts_count: {
822
+ assumptions: session.assumptions.length,
823
+ risks: session.risks.length,
824
+ competitors: session.competitors.length,
825
+ has_verdict: !!session.verdict
826
+ },
827
+ can_graduate: graduationStatus.canGraduate,
828
+ graduation_hint: graduationStatus.canGraduate ? "Ready to graduate! Use premortem_chat_graduate to run full analysis." : graduationStatus.reason
829
+ }, null, 2) }]
830
+ };
831
+ }
832
+ case "premortem_chat_status": {
833
+ const { session_id, auth_token } = args;
834
+ if (!session_id) {
835
+ throw new McpError(ErrorCode.InvalidParams, "session_id is required");
836
+ }
837
+ try {
838
+ await requirePremiumWithAutoAuth(auth_token).catch(() => null);
839
+ } catch {
840
+ }
841
+ const session = await getChatSession(session_id);
842
+ if (!session) {
843
+ throw new McpError(ErrorCode.InvalidRequest, `Session not found: ${session_id}`);
844
+ }
845
+ const graduationStatus = canGraduateSession(session);
846
+ return {
847
+ content: [{ type: "text", text: JSON.stringify({
848
+ session_id: session.id,
849
+ current_act: session.currentAct,
850
+ act_name: ACT_NAMES[session.currentAct],
851
+ initial_input: session.initialInput,
852
+ product_context: session.productContext || null,
853
+ artifacts: {
854
+ assumptions: session.assumptions,
855
+ risks: session.risks,
856
+ competitors: session.competitors,
857
+ verdict: session.verdict || null
858
+ },
859
+ conversation_turns: session.conversationHistory.length,
860
+ can_graduate: graduationStatus.canGraduate,
861
+ graduation_hint: graduationStatus.canGraduate ? "Ready! Use premortem_chat_graduate to run full analysis." : graduationStatus.reason,
862
+ created_at: new Date(session.createdAt).toISOString(),
863
+ updated_at: new Date(session.updatedAt).toISOString()
864
+ }, null, 2) }]
865
+ };
866
+ }
867
+ case "premortem_chat_graduate": {
868
+ const { session_id, auth_token } = args;
869
+ if (!session_id) {
870
+ throw new McpError(ErrorCode.InvalidParams, "session_id is required");
871
+ }
872
+ const decoded = await requirePremiumWithAutoAuth(auth_token);
873
+ const session = await getChatSession(session_id);
874
+ if (!session) {
875
+ throw new McpError(ErrorCode.InvalidRequest, `Session not found: ${session_id}`);
876
+ }
877
+ const graduationStatus = canGraduateSession(session);
878
+ if (!graduationStatus.canGraduate) {
879
+ throw new McpError(ErrorCode.InvalidRequest, `Cannot graduate: ${graduationStatus.reason}`);
880
+ }
881
+ let productContext = session.productContext;
882
+ if (!productContext?.brief) {
883
+ console.error("[PremortemGraduate] Building fallback product context from initialInput");
884
+ productContext = {
885
+ name: session.initialInput.slice(0, 50),
886
+ brief: session.initialInput,
887
+ targetUser: "",
888
+ problemSolved: "",
889
+ uniqueValue: ""
890
+ };
891
+ }
892
+ const conversational_context = {
893
+ productContext,
894
+ assumptions: session.assumptions,
895
+ risks: session.risks,
896
+ competitors: session.competitors,
897
+ verdict: session.verdict
898
+ };
899
+ const validation = validateForGraduation(conversational_context);
900
+ if (!validation.valid) {
901
+ throw new McpError(ErrorCode.InvalidParams, `Cannot graduate: ${validation.errors.join(", ")}`);
902
+ }
903
+ const project_name = productContext?.name || session.initialInput.slice(0, 50);
904
+ const runInput = buildRunInput({
905
+ sessionId: session_id,
906
+ projectName: project_name,
907
+ conversationalContext: conversational_context
908
+ });
909
+ const parsedInput = RunInputSchema.parse(runInput);
910
+ const { id: chatGradJobId } = await createPremortem({
911
+ status: "queued",
912
+ payload: parsedInput,
913
+ uid: decoded?.uid || null,
914
+ source: "mcp_chat_graduate",
915
+ metadata: buildGraduationMetadata(session_id, conversational_context, conversational_context.verdict ? 5 : 4)
916
+ });
917
+ session.conversationHistory.push({
918
+ role: "assistant",
919
+ content: `\u{1F393} **Graduated to Full Analysis!**
920
+
921
+ \u2615 This will take a minute\u2014go grab a cup of coffee while our 9 AI agents stress-test your idea.
922
+
923
+ Job ID: \`${chatGradJobId}\`
924
+
925
+ The full pipeline includes deeper competitor research, market sizing, financial modeling, and more.`
926
+ });
927
+ await saveChatSession(session);
928
+ try {
929
+ await updateChat(session_id, {
930
+ linkedJobId: chatGradJobId,
931
+ graduatedAt: (/* @__PURE__ */ new Date()).toISOString()
932
+ });
933
+ } catch {
934
+ }
935
+ return {
936
+ content: [{ type: "text", text: JSON.stringify({
937
+ success: true,
938
+ jobId: chatGradJobId,
939
+ sourceSessionId: session_id,
940
+ projectName: project_name,
941
+ warnings: validation.warnings,
942
+ artifacts_used: {
943
+ assumptions: session.assumptions.length,
944
+ risks: session.risks.length,
945
+ competitors: session.competitors.length,
946
+ has_verdict: !!session.verdict
947
+ },
948
+ message: `\u2615 Full analysis queued! This will take a minute\u2014go grab a cup of coffee. Use premortem_status with jobId "${chatGradJobId}" to check progress.`
949
+ }, null, 2) }]
950
+ };
951
+ }
952
+ default:
953
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
954
+ }
955
+ });
956
+ return guardOutput(name, rawResponse);
957
+ } catch (error) {
958
+ throw mapErrorToMcp(error, { tool: request.params.name });
959
+ }
960
+ });
961
+ async function run() {
962
+ const transport = new StdioServerTransport();
963
+ await server.connect(transport);
964
+ console.error("Cutline Premortem MCP Server running on stdio");
965
+ console.error("Tools: premortem_run, premortem_queue, premortem_status, premortem_kick, premortem_list, premortem_graduate");
966
+ console.error("Chat Tools: premortem_chat_start, premortem_chat, premortem_chat_status, premortem_chat_graduate");
967
+ }
968
+ run().catch((error) => {
969
+ console.error("Fatal error running server:", error);
970
+ process.exit(1);
971
+ });