PR #3522
Sections
Review

feat: improve MCP discovery and canvas authoring prompts

main ← feature/new-conversation-326 47 files +1200 −400 PR #3522 ↗

Task prompts now use compact discovery guidance and show canvas tools only when the session MCP profile allows them, so agents find tools on demand and canvas authoring stays capability-gated.

Why this change

Task prompts listed many tools verbatim and omitted canvas guidance. Agents could not discover unlisted tools, and canvas instructions appeared even when the session could not use them.

What it does

Architecture, end to end

Prompt producers resolve the MCP profile once and inject the same canvas decision into both the stored message and the dispatched prompt.

flowchart LR
  A[Task start / Prepared session / Workflow auto-start / Direct message] --> B[Executor: resolveTaskSessionMCPProfile]
  B --> C{CapabilityCanvas?}
  C -- yes --> D[sysprompt: IncludeCanvasGuidance = true]
  C -- no --> E[sysprompt: omit canvas block]
  D --> F[InjectKandevContextWithOptions]
  E --> F
  F --> G[(Stored user message)]
  F --> H[Agent launch prompt]
  G -. agree .-> H

Key code changes

Drag to pan. Use the + and − buttons to zoom. Click a node to open the full code. The arrows show how the parts interact.

drag to pan · +/− to zoom · click a node for details

KANDEV MCP TOOLS — Selected tools
Click for details →

The template now marks the list as selected guidance and tells agents how to discover missing tools.

Template header
KANDEV MCP TOOLS — Selected tools from the "kandev" server are available here.
Names ending in `_kandev` are canonical MCP protocol names. A client-specific alias may be server-qualified; use the exact callable name and schema exposed by the client.
Discovery section
MCP DISCOVERY:
These instructions list selected Kandev tools, not the complete MCP catalog.
For Kandev operations, use tools exposed by the "kandev" server. If the needed tool is already callable, use its current schema; otherwise use your client's native tool search or discovery when available.
Search for "kandev" plus the operation or known canonical tool name. If search is unavailable, inspect the MCP tools available in your client.
Use the exact callable name and schema exposed by the client. An omitted entry here does not mean that the tool is unavailable. If discovery cannot find a required tool, report that limitation before substituting another result.
Essential workflow
ESSENTIAL WORKFLOW:
Preserve task/session identity and the system marker, question barriers, title ownership, completion gates, autopilot behavior, delegation boundaries, final-action rules, and user edits in task plans. Use create/get/update plan tools. For data requests that need a chart, preview, or metric, call show_rich_output_kandev with the schema returned by discovery.
func FormatKandevContextWithOptions(taskID, sessionID string, options KandevContextOptions) string
Click for details →

Canvas guidance is now an explicit option that the caller sets from the resolved MCP profile.

Canvas guidance constant
const canvasGuidanceSection = `CANVAS AUTHORING:
Use these tools only when the user explicitly asks for a Kandev canvas: create_canvas_kandev, read_canvas_authoring_skill_kandev, and publish_canvas_kandev. Create it in Kandev before writing app files. Read the skill once, edit only in its returned source directory, then publish through MCP. Report publication status, including failures. Local files or a successful build do not publish a canvas.
`
Compact rich output
const richOutputSection = `- show_rich_output_kandev: For a chart, graph, plot, file preview, KPI, or metrics request with data, call this now. Do not implement it as ASCII, SVG, HTML, or another app. Otherwise use prose or a small Markdown table. Get the schema and examples from tool discovery. Paths are workspace-relative; Kandev owns layout, axes, legends, and tooltips. Label series with units.
`
Options struct
type KandevContextOptions struct {
	RequiresCompletionSignal       bool
	IncludeCoordinatorTaskControls bool
	IncludeTaskTitleTool           bool
	IncludeCanvasGuidance          bool
	Autopilot                      bool
	IncludeUserQuestionTool        bool
	IncludeParentQuestionTool      bool
}
Conditional injection
	canvasGuidance := ""
	if options.IncludeCanvasGuidance {
		canvasGuidance = canvasGuidanceSection
	}
	return Resolve("kandev-context", map[string]string{
		"task_id":                          taskID,
		"session_id":                       sessionID,
		"step_complete_section":            section,
		"task_title_section":               taskTitle,
		"coordinator_task_control_section": coordinatorControls,
		"canvas_guidance_section":          canvasGuidance,
		"rich_output_section":              richOutputSection,
		"autopilot_section":                autopilot,
		"question_tool_section":            questionTool,
	})
func (e *Executor) withCanvasCapability(profile mcpprofile.Context) mcpprofile.Context
Click for details →

The executor adds CapabilityCanvas only for kanban tasks when the global canvases flag is on, and exposes the profile for prompt producers.

Profile resolver
func (e *Executor) resolveTaskSessionMCPProfile(ctx context.Context, taskID string, session *models.TaskSession, allowTitleTool bool) (mcpprofile.Context, error) {
	if isConfigModeSession(session) {
		capabilities := []mcpprofile.Capability{mcpprofile.CapabilityUserQuestion}
		if session.IsPassthrough {
			capabilities = nil
		}
		return e.withCanvasCapability(mcpprofile.New(mcpprofile.SurfaceConfiguration, capabilities, nil)), nil
	}
	// ... kanban / office / automation branches ...
	return e.withCanvasCapability(mcpprofile.New(surface, capabilities, nil)), nil
}
Canvas gate
func (e *Executor) withCanvasCapability(profile mcpprofile.Context) mcpprofile.Context {
	if e != nil && e.canvasesEnabled && profile.Surface == mcpprofile.SurfaceKanbanTask {
		return profile.WithCapability(mcpprofile.CapabilityCanvas)
	}
	return profile
}
Read-only query
func (e *Executor) ResolveTaskSessionMCPProfile(ctx context.Context, taskID string, session *models.TaskSession, allowTitleTool bool) (mcpprofile.Context, error) {
	return e.resolveTaskSessionMCPProfile(ctx, taskID, session, allowTitleTool)
}
func (s *Service) wrapCreatedSessionPrompt(ctx context.Context, prompt string, taskID string, sessionID string, session *models.TaskSession, dbTask *models.Task, isOfficeTask bool, configMode bool, titleOwner bool, includeCanvasGuidance bool, references []v1.EntityReference, promptReferenceContext string) string
Click for details →

Every launch path resolves canvas guidance from the same profile and passes the same value to both the stored and dispatched prompt.

Created session launch
	includeCanvasGuidance := false
	if (effectivePrompt != "" || len(attachments) > 0) && !isOfficeTask && !session.IsPassthrough && !configMode {
		if options.canvasGuidanceResolved {
			includeCanvasGuidance = options.includeCanvasGuidance
		} else {
			includeCanvasGuidance, err = s.taskSessionCanvasGuidanceEnabled(ctx, taskID, session, true)
			if err != nil {
				return nil, fmt.Errorf("failed to resolve canvas prompt capability: %w", err)
			}
		}
	}
	if effectivePrompt != "" || len(attachments) > 0 {
		effectivePrompt = s.wrapCreatedSessionPrompt(ctx, effectivePrompt, taskID, sessionID, session, dbTask, isOfficeTask, configMode, titleOwner, includeCanvasGuidance, references, promptReferenceContext)
	}
Workflow auto-start
func (s *Service) resolveAutoStartPromptContext(ctx context.Context, taskID string, session *models.TaskSession) (bool, *models.Task, bool, bool, error) {
	// ... office / task load ...
	includeCanvasGuidance, err = s.taskSessionCanvasGuidanceEnabled(ctx, taskID, session, true)
	// ... return includeCanvasGuidance as fourth value ...
}
Injection
return sysprompt.InjectKandevContextWithOptions(taskID, sessionID, prompt, sysprompt.KandevContextOptions{
	RequiresCompletionSignal:       s.WorkflowStepRequiresCompletionSignal(ctx, dbTask.WorkflowStepID),
	IncludeCoordinatorTaskControls: !configMode,
	IncludeTaskTitleTool:           !configMode && titleOwner,
	IncludeCanvasGuidance:          includeCanvasGuidance,
	Autopilot:                      dbTask.Autopilot,
	IncludeUserQuestionTool:        !dbTask.Autopilot && !session.IsPassthrough,
	IncludeParentQuestionTool:      dbTask.Autopilot && dbTask.ParentID != "",
}, referenceContext, promptReferenceContext, pullRequestTargetContext)
func (h *MessageHandlers) injectMessageContext(ctx context.Context, req wsAddMessageRequest, sessionResp *dto.GetTaskSessionResponse, task *models.Task, configMode bool, startCreatedSession bool, titleOwner bool, includeCanvasGuidance bool, content string, trustedPromptContext string) string
Click for details →

The WebSocket handler resolves canvas guidance once at admission and carries the projection to the launch, so the DB row and the agent prompt agree.

Resolve at admission
func (h *MessageHandlers) resolveCanvasGuidance(ctx context.Context, taskID, sessionID string) (bool, error) {
	resolver, ok := h.orchestrator.(taskCanvasGuidanceResolver)
	if !ok {
		return false, nil
	}
	return resolver.TaskSessionCanvasGuidanceEnabled(ctx, taskID, sessionID)
}
Admission path
		includeCanvasGuidance := false
		canvasGuidanceResolved := false
		if task != nil && !task.IsFromOffice && !sessionResp.Session.IsPassthrough && !configMode {
			canvasGuidanceResolved = true
			includeCanvasGuidance, resolveErr = h.resolveCanvasGuidance(ctx, req.TaskID, req.TaskSessionID)
		}
		storedContent = h.injectMessageContext(ctx, req, sessionResp, task, configMode, startCreatedSession, titleOwner, includeCanvasGuidance, storedContent, trustedPromptContext)
		req.canvasGuidanceResolved = canvasGuidanceResolved
		req.includeCanvasGuidance = includeCanvasGuidance
Forward with projection
if starter, ok := h.orchestrator.(orchestrator.DirectPromptStarterWithCanvasGuidance); ok && len(canvasGuidance) > 0 {
	_, err = starter.StartCreatedSessionWithPromptContextAndCanvasGuidance(ctx, taskID, sessionID, agentProfileID, content, true, planMode, false, attachments, references, trustedPromptContext, projection.resolved, projection.include)
}
Localized canvas creation presetapps/web/src/locales/en/canvases.json ↗
createCanvasTaskPrompt: string
Click for details →

The preset now teaches discovery and the full create-read-publish workflow with exact tool names in every locale.

Preset prompt
"createCanvasTaskPrompt": "Create an interactive canvas inside Kandev for the application I describe. If the application goal is missing, ask what the canvas must show or do. Find the Kandev canvas MCP tools before writing application files. If they are not callable, use native tool search for \"kandev canvas\" or inspect the available MCP catalog. Create the draft in Kandev before writing application files. Call `create_canvas_kandev` to create the draft and obtain its source directory. Read `read_canvas_authoring_skill_kandev` once without a path. Build inside the returned directory and use authorized live Kandev data for domain views. Call `publish_canvas_kandev` and address any validation errors. Report the canvas identity and whether its release is active, awaits permission review, or was unsuccessful. If publication is unsuccessful, report the failure and do not claim that the canvas is published. If workspace access requires promotion, explain the user action that is still required. A local build alone does not publish a canvas inside Kandev. If the tools remain unavailable, report the limitation instead of claiming that workspace files are a Kandev canvas."
Read the changes as a list

Compact discovery prompt

apps/backend/config/prompts/kandev-context.md

The template now marks the list as selected guidance and tells agents how to discover missing tools.

Template header
KANDEV MCP TOOLS — Selected tools from the "kandev" server are available here.
Names ending in `_kandev` are canonical MCP protocol names. A client-specific alias may be server-qualified; use the exact callable name and schema exposed by the client.
Discovery section
MCP DISCOVERY:
These instructions list selected Kandev tools, not the complete MCP catalog.
For Kandev operations, use tools exposed by the "kandev" server. If the needed tool is already callable, use its current schema; otherwise use your client's native tool search or discovery when available.
Search for "kandev" plus the operation or known canonical tool name. If search is unavailable, inspect the MCP tools available in your client.
Use the exact callable name and schema exposed by the client. An omitted entry here does not mean that the tool is unavailable. If discovery cannot find a required tool, report that limitation before substituting another result.
Essential workflow
ESSENTIAL WORKFLOW:
Preserve task/session identity and the system marker, question barriers, title ownership, completion gates, autopilot behavior, delegation boundaries, final-action rules, and user edits in task plans. Use create/get/update plan tools. For data requests that need a chart, preview, or metric, call show_rich_output_kandev with the schema returned by discovery.

Capability-gated sysprompt

apps/backend/internal/sysprompt/sysprompt.go

Canvas guidance is now an explicit option that the caller sets from the resolved MCP profile.

Canvas guidance constant
const canvasGuidanceSection = `CANVAS AUTHORING:
Use these tools only when the user explicitly asks for a Kandev canvas: create_canvas_kandev, read_canvas_authoring_skill_kandev, and publish_canvas_kandev. Create it in Kandev before writing app files. Read the skill once, edit only in its returned source directory, then publish through MCP. Report publication status, including failures. Local files or a successful build do not publish a canvas.
`
Compact rich output
const richOutputSection = `- show_rich_output_kandev: For a chart, graph, plot, file preview, KPI, or metrics request with data, call this now. Do not implement it as ASCII, SVG, HTML, or another app. Otherwise use prose or a small Markdown table. Get the schema and examples from tool discovery. Paths are workspace-relative; Kandev owns layout, axes, legends, and tooltips. Label series with units.
`
Options struct
type KandevContextOptions struct {
	RequiresCompletionSignal       bool
	IncludeCoordinatorTaskControls bool
	IncludeTaskTitleTool           bool
	IncludeCanvasGuidance          bool
	Autopilot                      bool
	IncludeUserQuestionTool        bool
	IncludeParentQuestionTool      bool
}
Conditional injection
	canvasGuidance := ""
	if options.IncludeCanvasGuidance {
		canvasGuidance = canvasGuidanceSection
	}
	return Resolve("kandev-context", map[string]string{
		"task_id":                          taskID,
		"session_id":                       sessionID,
		"step_complete_section":            section,
		"task_title_section":               taskTitle,
		"coordinator_task_control_section": coordinatorControls,
		"canvas_guidance_section":          canvasGuidance,
		"rich_output_section":              richOutputSection,
		"autopilot_section":                autopilot,
		"question_tool_section":            questionTool,
	})

MCP profile owns canvas capability

apps/backend/internal/orchestrator/executor/executor_execute.go

The executor adds CapabilityCanvas only for kanban tasks when the global canvases flag is on, and exposes the profile for prompt producers.

Profile resolver
func (e *Executor) resolveTaskSessionMCPProfile(ctx context.Context, taskID string, session *models.TaskSession, allowTitleTool bool) (mcpprofile.Context, error) {
	if isConfigModeSession(session) {
		capabilities := []mcpprofile.Capability{mcpprofile.CapabilityUserQuestion}
		if session.IsPassthrough {
			capabilities = nil
		}
		return e.withCanvasCapability(mcpprofile.New(mcpprofile.SurfaceConfiguration, capabilities, nil)), nil
	}
	// ... kanban / office / automation branches ...
	return e.withCanvasCapability(mcpprofile.New(surface, capabilities, nil)), nil
}
Canvas gate
func (e *Executor) withCanvasCapability(profile mcpprofile.Context) mcpprofile.Context {
	if e != nil && e.canvasesEnabled && profile.Surface == mcpprofile.SurfaceKanbanTask {
		return profile.WithCapability(mcpprofile.CapabilityCanvas)
	}
	return profile
}
Read-only query
func (e *Executor) ResolveTaskSessionMCPProfile(ctx context.Context, taskID string, session *models.TaskSession, allowTitleTool bool) (mcpprofile.Context, error) {
	return e.resolveTaskSessionMCPProfile(ctx, taskID, session, allowTitleTool)
}

Prompt producers follow the profile

apps/backend/internal/orchestrator/task_operations.go

Every launch path resolves canvas guidance from the same profile and passes the same value to both the stored and dispatched prompt.

Created session launch
	includeCanvasGuidance := false
	if (effectivePrompt != "" || len(attachments) > 0) && !isOfficeTask && !session.IsPassthrough && !configMode {
		if options.canvasGuidanceResolved {
			includeCanvasGuidance = options.includeCanvasGuidance
		} else {
			includeCanvasGuidance, err = s.taskSessionCanvasGuidanceEnabled(ctx, taskID, session, true)
			if err != nil {
				return nil, fmt.Errorf("failed to resolve canvas prompt capability: %w", err)
			}
		}
	}
	if effectivePrompt != "" || len(attachments) > 0 {
		effectivePrompt = s.wrapCreatedSessionPrompt(ctx, effectivePrompt, taskID, sessionID, session, dbTask, isOfficeTask, configMode, titleOwner, includeCanvasGuidance, references, promptReferenceContext)
	}
Workflow auto-start
func (s *Service) resolveAutoStartPromptContext(ctx context.Context, taskID string, session *models.TaskSession) (bool, *models.Task, bool, bool, error) {
	// ... office / task load ...
	includeCanvasGuidance, err = s.taskSessionCanvasGuidanceEnabled(ctx, taskID, session, true)
	// ... return includeCanvasGuidance as fourth value ...
}
Injection
return sysprompt.InjectKandevContextWithOptions(taskID, sessionID, prompt, sysprompt.KandevContextOptions{
	RequiresCompletionSignal:       s.WorkflowStepRequiresCompletionSignal(ctx, dbTask.WorkflowStepID),
	IncludeCoordinatorTaskControls: !configMode,
	IncludeTaskTitleTool:           !configMode && titleOwner,
	IncludeCanvasGuidance:          includeCanvasGuidance,
	Autopilot:                      dbTask.Autopilot,
	IncludeUserQuestionTool:        !dbTask.Autopilot && !session.IsPassthrough,
	IncludeParentQuestionTool:      dbTask.Autopilot && dbTask.ParentID != "",
}, referenceContext, promptReferenceContext, pullRequestTargetContext)

Direct message preserves the decision

apps/backend/internal/task/handlers/message_handlers.go

The WebSocket handler resolves canvas guidance once at admission and carries the projection to the launch, so the DB row and the agent prompt agree.

Resolve at admission
func (h *MessageHandlers) resolveCanvasGuidance(ctx context.Context, taskID, sessionID string) (bool, error) {
	resolver, ok := h.orchestrator.(taskCanvasGuidanceResolver)
	if !ok {
		return false, nil
	}
	return resolver.TaskSessionCanvasGuidanceEnabled(ctx, taskID, sessionID)
}
Admission path
		includeCanvasGuidance := false
		canvasGuidanceResolved := false
		if task != nil && !task.IsFromOffice && !sessionResp.Session.IsPassthrough && !configMode {
			canvasGuidanceResolved = true
			includeCanvasGuidance, resolveErr = h.resolveCanvasGuidance(ctx, req.TaskID, req.TaskSessionID)
		}
		storedContent = h.injectMessageContext(ctx, req, sessionResp, task, configMode, startCreatedSession, titleOwner, includeCanvasGuidance, storedContent, trustedPromptContext)
		req.canvasGuidanceResolved = canvasGuidanceResolved
		req.includeCanvasGuidance = includeCanvasGuidance
Forward with projection
if starter, ok := h.orchestrator.(orchestrator.DirectPromptStarterWithCanvasGuidance); ok && len(canvasGuidance) > 0 {
	_, err = starter.StartCreatedSessionWithPromptContextAndCanvasGuidance(ctx, taskID, sessionID, agentProfileID, content, true, planMode, false, attachments, references, trustedPromptContext, projection.resolved, projection.include)
}

Localized canvas creation preset

apps/web/src/locales/en/canvases.json

The preset now teaches discovery and the full create-read-publish workflow with exact tool names in every locale.

Preset prompt
"createCanvasTaskPrompt": "Create an interactive canvas inside Kandev for the application I describe. If the application goal is missing, ask what the canvas must show or do. Find the Kandev canvas MCP tools before writing application files. If they are not callable, use native tool search for \"kandev canvas\" or inspect the available MCP catalog. Create the draft in Kandev before writing application files. Call `create_canvas_kandev` to create the draft and obtain its source directory. Read `read_canvas_authoring_skill_kandev` once without a path. Build inside the returned directory and use authorized live Kandev data for domain views. Call `publish_canvas_kandev` and address any validation errors. Report the canvas identity and whether its release is active, awaits permission review, or was unsuccessful. If publication is unsuccessful, report the failure and do not claim that the canvas is published. If workspace access requires promotion, explain the user action that is still required. A local build alone does not publish a canvas inside Kandev. If the tools remain unavailable, report the limitation instead of claiming that workspace files are a Kandev canvas."

Risk

5 / 10 Medium
1 low5 medium10 high

Why this score

  • Prompt text changes affect every task agent; a missing barrier or title rule would change agent behavior broadly.
  • Canvas gating depends on the MCP profile resolver; a wrong surface check would hide or leak canvas tools.
  • Stored and dispatched prompts must agree; a mismatch would make the visible message differ from what the agent receives.

Trade-offs and review notes

Where to look first

  1. Check sysprompt.go: canvasGuidanceSection appears only when IncludeCanvasGuidance is true and richOutputSection no longer embeds inline JSON examples.
  2. Check executor_execute.go: withCanvasCapability gates on SurfaceKanbanTask and canvasesEnabled, and ResolveTaskSessionMCPProfile is the single source for prompt producers.
  3. Check task_operations.go and event_handlers_workflow.go: every launch and auto-start path uses the same resolved value for both recorded and dispatched text.
  4. Check message_handlers.go and adapters.go: the direct-message path resolves once at admission and forwards the projection via StartCreatedSessionWithPromptContextAndCanvasGuidance.
  5. Check canvases.json and canvas-task-prompt.test.ts: the preset contains all three canvas tools verbatim and the publication failure wording in every locale.