feat(lean-canvas): add Lean Canvas Generator MCP tool#18
feat(lean-canvas): add Lean Canvas Generator MCP tool#18danielfurt wants to merge 1 commit intomainfrom
Conversation
Add a new MCP tool that guides users through building a Lean Canvas business model via AI-driven conversation. The AI acts as a strategic business partner, asking questions about the solution and progressively filling canvas sections following lean methodology best practices. Key features: - Welcome screen with idea input that triggers AI conversation - Real-time canvas updates via MCP protocol polling - Editable sections with inline editing, add/delete items - Manual fill capability for any section - Recommended next section highlighting - Export to PDF and Markdown - Canvas state synced to AI context via updateModelContext - Storage abstraction (MemoryCanvasStore) for easy future swapping - All UI text in Portuguese BR Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
10 issues found across 23 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="web/tools/lean-canvas/export-canvas.ts">
<violation number="1" location="web/tools/lean-canvas/export-canvas.ts:135">
P2: `downloadPdf` can call `printWindow.print()` twice because both `onload` and the timeout execute. Guard printing so it only happens once.</violation>
</file>
<file name="web/tools/lean-canvas/editable-item.tsx">
<violation number="1" location="web/tools/lean-canvas/editable-item.tsx:57">
P2: `onBlur` always calling `onSave` conflicts with Escape cancel and can also trigger duplicate saves.</violation>
</file>
<file name="web/tools/lean-canvas/use-canvas-polling.ts">
<violation number="1" location="web/tools/lean-canvas/use-canvas-polling.ts:68">
P2: Using `setInterval` with an async poll function can trigger overlapping requests and race updates. Schedule the next poll only after the previous one finishes.</violation>
</file>
<file name="web/tools/lean-canvas/use-canvas-state.ts">
<violation number="1" location="web/tools/lean-canvas/use-canvas-state.ts:67">
P2: `persistCanvas` does not handle async rejections from MCP calls, so persistence/context update failures can bypass error handling.</violation>
<violation number="2" location="web/tools/lean-canvas/use-canvas-state.ts:129">
P2: `previousCanvasRef` is updated with stale state, so new-item detection compares against an outdated canvas snapshot.</violation>
</file>
<file name="api/resources/canvas-state.ts">
<violation number="1" location="api/resources/canvas-state.ts:17">
P1: Canvas state is stored and read under a single global key, so concurrent users can read/overwrite each other’s Lean Canvas data.</violation>
</file>
<file name="api/prompts/lean-canvas.ts">
<violation number="1" location="api/prompts/lean-canvas.ts:22">
P2: Use nullish coalescing instead of `||` so empty-string input does not incorrectly fall back to previously saved idea.</violation>
</file>
<file name="api/tools/lean-canvas.ts">
<violation number="1" location="api/tools/lean-canvas.ts:64">
P1: `LeanCanvasOutput` is inferred from the input schema, not the declared output schema, causing a schema/type contract mismatch.</violation>
<violation number="2" location="api/tools/lean-canvas.ts:76">
P2: `readOnlyHint` is set to `true`, but `execute` mutates storage via `store.set`, so the tool is not read-only.</violation>
</file>
<file name="api/resources/save-idea.ts">
<violation number="1" location="api/resources/save-idea.ts:6">
P1: Using a fixed global storage key causes cross-session data overwrites and can leak one user’s idea to another.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| "Current Lean Canvas state as JSON, read by the side panel UI via polling", | ||
| mimeType: "application/json", | ||
| read: async () => { | ||
| const data = await store.get(CANVAS_STORE_KEY); |
There was a problem hiding this comment.
P1: Canvas state is stored and read under a single global key, so concurrent users can read/overwrite each other’s Lean Canvas data.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At api/resources/canvas-state.ts, line 17:
<comment>Canvas state is stored and read under a single global key, so concurrent users can read/overwrite each other’s Lean Canvas data.</comment>
<file context>
@@ -0,0 +1,24 @@
+ "Current Lean Canvas state as JSON, read by the side panel UI via polling",
+ mimeType: "application/json",
+ read: async () => {
+ const data = await store.get(CANVAS_STORE_KEY);
+ return {
+ uri: CANVAS_STATE_RESOURCE_URI,
</file context>
| filledSections: z.array(z.string()).describe("Seções que foram preenchidas"), | ||
| }); | ||
|
|
||
| export type LeanCanvasOutput = z.infer<typeof leanCanvasInputSchema>; |
There was a problem hiding this comment.
P1: LeanCanvasOutput is inferred from the input schema, not the declared output schema, causing a schema/type contract mismatch.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At api/tools/lean-canvas.ts, line 64:
<comment>`LeanCanvasOutput` is inferred from the input schema, not the declared output schema, causing a schema/type contract mismatch.</comment>
<file context>
@@ -0,0 +1,114 @@
+ filledSections: z.array(z.string()).describe("Seções que foram preenchidas"),
+});
+
+export type LeanCanvasOutput = z.infer<typeof leanCanvasInputSchema>;
+export type LeanCanvasToolOutput = z.infer<typeof leanCanvasOutputSchema>;
+
</file context>
| export type LeanCanvasOutput = z.infer<typeof leanCanvasInputSchema>; | |
| export type LeanCanvasOutput = z.infer<typeof leanCanvasOutputSchema>; |
| import { store } from "../storage/index.ts"; | ||
| import type { Env } from "../types/env.ts"; | ||
|
|
||
| export const IDEA_STORE_KEY = "user-idea"; |
There was a problem hiding this comment.
P1: Using a fixed global storage key causes cross-session data overwrites and can leak one user’s idea to another.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At api/resources/save-idea.ts, line 6:
<comment>Using a fixed global storage key causes cross-session data overwrites and can leak one user’s idea to another.</comment>
<file context>
@@ -0,0 +1,48 @@
+import { store } from "../storage/index.ts";
+import type { Env } from "../types/env.ts";
+
+export const IDEA_STORE_KEY = "user-idea";
+
+/**
</file context>
| if (printWindow) { | ||
| printWindow.document.write(html); | ||
| printWindow.document.close(); | ||
| printWindow.onload = () => { |
There was a problem hiding this comment.
P2: downloadPdf can call printWindow.print() twice because both onload and the timeout execute. Guard printing so it only happens once.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/tools/lean-canvas/export-canvas.ts, line 135:
<comment>`downloadPdf` can call `printWindow.print()` twice because both `onload` and the timeout execute. Guard printing so it only happens once.</comment>
<file context>
@@ -0,0 +1,154 @@
+ if (printWindow) {
+ printWindow.document.write(html);
+ printWindow.document.close();
+ printWindow.onload = () => {
+ printWindow.print();
+ };
</file context>
| onCancel(); | ||
| } | ||
| }} | ||
| onBlur={() => onSave(editValue)} |
There was a problem hiding this comment.
P2: onBlur always calling onSave conflicts with Escape cancel and can also trigger duplicate saves.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/tools/lean-canvas/editable-item.tsx, line 57:
<comment>`onBlur` always calling `onSave` conflicts with Escape cancel and can also trigger duplicate saves.</comment>
<file context>
@@ -0,0 +1,87 @@
+ onCancel();
+ }
+ }}
+ onBlur={() => onSave(editValue)}
+ className="min-h-0 py-0.5 px-1.5 text-xs leading-relaxed border-primary/30 focus-visible:ring-primary/20 resize-none"
+ rows={1}
</file context>
|
|
||
| useEffect(() => { | ||
| if (!app) return; | ||
| poll(); |
There was a problem hiding this comment.
P2: Using setInterval with an async poll function can trigger overlapping requests and race updates. Schedule the next poll only after the previous one finishes.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/tools/lean-canvas/use-canvas-polling.ts, line 68:
<comment>Using `setInterval` with an async poll function can trigger overlapping requests and race updates. Schedule the next poll only after the previous one finishes.</comment>
<file context>
@@ -0,0 +1,76 @@
+
+ useEffect(() => {
+ if (!app) return;
+ poll();
+ intervalRef.current = setInterval(poll, POLL_INTERVAL);
+ return () => {
</file context>
| // Save to store | ||
| try { | ||
| const encoded = encodeURIComponent(JSON.stringify(canvas)); | ||
| app.readServerResource({ uri: `data://mcp-app/save-canvas/${encoded}` }); |
There was a problem hiding this comment.
P2: persistCanvas does not handle async rejections from MCP calls, so persistence/context update failures can bypass error handling.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/tools/lean-canvas/use-canvas-state.ts, line 67:
<comment>`persistCanvas` does not handle async rejections from MCP calls, so persistence/context update failures can bypass error handling.</comment>
<file context>
@@ -0,0 +1,284 @@
+ // Save to store
+ try {
+ const encoded = encodeURIComponent(JSON.stringify(canvas));
+ app.readServerResource({ uri: `data://mcp-app/save-canvas/${encoded}` });
+ } catch {
+ console.log("[Canvas] Failed to persist canvas to store");
</file context>
| } | ||
| } | ||
|
|
||
| previousCanvasRef.current = canvas ? cloneCanvas(canvas) : null; |
There was a problem hiding this comment.
P2: previousCanvasRef is updated with stale state, so new-item detection compares against an outdated canvas snapshot.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/tools/lean-canvas/use-canvas-state.ts, line 129:
<comment>`previousCanvasRef` is updated with stale state, so new-item detection compares against an outdated canvas snapshot.</comment>
<file context>
@@ -0,0 +1,284 @@
+ }
+ }
+
+ previousCanvasRef.current = canvas ? cloneCanvas(canvas) : null;
+ setNewItemsBySection(newItems);
+ setCanvas(incoming);
</file context>
| execute: async ({ args }) => { | ||
| // Check for idea from prompt args first, then from the stored idea (set by UI) | ||
| const savedIdea = await store.get<string>(IDEA_STORE_KEY); | ||
| const idea = args.idea || savedIdea; |
There was a problem hiding this comment.
P2: Use nullish coalescing instead of || so empty-string input does not incorrectly fall back to previously saved idea.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At api/prompts/lean-canvas.ts, line 22:
<comment>Use nullish coalescing instead of `||` so empty-string input does not incorrectly fall back to previously saved idea.</comment>
<file context>
@@ -0,0 +1,70 @@
+ execute: async ({ args }) => {
+ // Check for idea from prompt args first, then from the stored idea (set by UI)
+ const savedIdea = await store.get<string>(IDEA_STORE_KEY);
+ const idea = args.idea || savedIdea;
+ const ideaContext = idea
+ ? `\n\nA ideia inicial do usuário: "${idea}"`
</file context>
| outputSchema: leanCanvasOutputSchema, | ||
| _meta: { ui: { resourceUri: LEAN_CANVAS_RESOURCE_URI } }, | ||
| annotations: { | ||
| readOnlyHint: true, |
There was a problem hiding this comment.
P2: readOnlyHint is set to true, but execute mutates storage via store.set, so the tool is not read-only.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At api/tools/lean-canvas.ts, line 76:
<comment>`readOnlyHint` is set to `true`, but `execute` mutates storage via `store.set`, so the tool is not read-only.</comment>
<file context>
@@ -0,0 +1,114 @@
+ outputSchema: leanCanvasOutputSchema,
+ _meta: { ui: { resourceUri: LEAN_CANVAS_RESOURCE_URI } },
+ annotations: {
+ readOnlyHint: true,
+ destructiveHint: false,
+ idempotentHint: true,
</file context>
Summary
MemoryCanvasStore) for easy future swapping (Redis, DB, etc.)New files
api/tools/lean-canvas.ts— tool definition with Zod input/output schemasapi/prompts/lean-canvas.ts— AI prompt for strategic business partner behaviorapi/resources/canvas-state.ts— MCP data resource for canvas state pollingapi/resources/lean-canvas.ts— serves bundled HTML for MCP App UIapi/resources/save-canvas.ts/save-idea.ts— persist state via MCP protocolapi/storage/index.ts— storage abstraction with in-memory implementationweb/tools/lean-canvas/— React UI (canvas grid, sections, editing, export, polling hooks)Test plan
bun run dev), connect via MCP hostupdateModelContext🤖 Generated with Claude Code
Summary by cubic
Add an interactive Lean Canvas generator MCP tool and side-panel UI that guide users through building a business model with real-time, in-sync canvas updates. Includes inline editing, manual fill, export to PDF/Markdown, and an in-memory store, all in pt-BR.
lean_canvaswith full Lean Canvas schema and idempotent updates; exposes UI atui://mcp-app/lean-canvas.lean-canvas(pt-BR) that asks business-focused questions and fills 1–2 sections at a time; reads current state fromdata://mcp-app/canvas-state.data://mcp-app/canvas-state,data://mcp-app/save-canvas/{data},data://mcp-app/save-idea/{idea}(polled every 2s).storeto allow future backends.api/main.ts; add router entry; minor logging in web context.Written for commit 1461918. Summary will update on new commits.