Skip to content

feat(lean-canvas): add Lean Canvas Generator MCP tool#18

Open
danielfurt wants to merge 1 commit intomainfrom
danielfurt/explain-mcp-repo
Open

feat(lean-canvas): add Lean Canvas Generator MCP tool#18
danielfurt wants to merge 1 commit intomainfrom
danielfurt/explain-mcp-repo

Conversation

@danielfurt
Copy link
Copy Markdown

@danielfurt danielfurt commented Apr 9, 2026

Summary

  • Add interactive Lean Canvas Generator MCP tool with AI-guided business model building
  • Welcome screen captures initial idea, then AI conducts strategic conversation to progressively fill 9 canvas sections (Problem, Customer Segments, UVP, Solution, Channels, Revenue, Costs, Key Metrics, Unfair Advantage)
  • Real-time canvas updates via MCP protocol polling, with export to PDF/Markdown
  • Editable sections (inline edit, add/delete items), manual fill, recommended section highlighting
  • Storage abstraction (MemoryCanvasStore) for easy future swapping (Redis, DB, etc.)
  • All UI text in Portuguese BR

New files

  • api/tools/lean-canvas.ts — tool definition with Zod input/output schemas
  • api/prompts/lean-canvas.ts — AI prompt for strategic business partner behavior
  • api/resources/canvas-state.ts — MCP data resource for canvas state polling
  • api/resources/lean-canvas.ts — serves bundled HTML for MCP App UI
  • api/resources/save-canvas.ts / save-idea.ts — persist state via MCP protocol
  • api/storage/index.ts — storage abstraction with in-memory implementation
  • web/tools/lean-canvas/ — React UI (canvas grid, sections, editing, export, polling hooks)

Test plan

  • Start dev server (bun run dev), connect via MCP host
  • Verify welcome screen appears with idea input
  • Submit an idea and confirm AI starts asking strategic questions
  • Verify canvas sections fill progressively (1-2 at a time)
  • Test inline editing, adding, and deleting items
  • Test manual fill (clicking empty section)
  • Verify export to PDF and Markdown
  • Confirm AI sees manual edits via updateModelContext

🤖 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.

  • New Features
    • Added MCP tool lean_canvas with full Lean Canvas schema and idempotent updates; exposes UI at ui://mcp-app/lean-canvas.
    • New prompt lean-canvas (pt-BR) that asks business-focused questions and fills 1–2 sections at a time; reads current state from data://mcp-app/canvas-state.
    • React UI: canvas grid, inline edit/add/delete, manual fill, next-section highlight, and PDF/Markdown export.
    • Real-time sync via MCP resources: data://mcp-app/canvas-state, data://mcp-app/save-canvas/{data}, data://mcp-app/save-idea/{idea} (polled every 2s).
    • Storage abstraction with in-memory store to allow future backends.
    • Wiring: register tool, prompt, and resources in api/main.ts; add router entry; minor logging in web context.

Written for commit 1461918. Summary will update on new commits.

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>
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

Comment thread api/tools/lean-canvas.ts
filledSections: z.array(z.string()).describe("Seções que foram preenchidas"),
});

export type LeanCanvasOutput = z.infer<typeof leanCanvasInputSchema>;
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
export type LeanCanvasOutput = z.infer<typeof leanCanvasInputSchema>;
export type LeanCanvasOutput = z.infer<typeof leanCanvasOutputSchema>;
Fix with Cubic

import { store } from "../storage/index.ts";
import type { Env } from "../types/env.ts";

export const IDEA_STORE_KEY = "user-idea";
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

if (printWindow) {
printWindow.document.write(html);
printWindow.document.close();
printWindow.onload = () => {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

onCancel();
}
}}
onBlur={() => onSave(editValue)}
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic


useEffect(() => {
if (!app) return;
poll();
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

// Save to store
try {
const encoded = encodeURIComponent(JSON.stringify(canvas));
app.readServerResource({ uri: `data://mcp-app/save-canvas/${encoded}` });
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

}
}

previousCanvasRef.current = canvas ? cloneCanvas(canvas) : null;
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

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;
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

Comment thread api/tools/lean-canvas.ts
outputSchema: leanCanvasOutputSchema,
_meta: { ui: { resourceUri: LEAN_CANVAS_RESOURCE_URI } },
annotations: {
readOnlyHint: true,
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant