Common patterns and recipes for building MCP Apps with sunpeak — polling, chunked data, binary resources, fullscreen, model context, state persistence, and more.
This guide covers common patterns for building MCP Apps with sunpeak. Each recipe shows both the tool file (server-side) and resource component (client-side) where applicable.
Set visibility: ["app"] in a tool’s _meta.ui to make it callable only from the UI — hidden from the LLM. Use this for polling, pagination, form submissions, and other UI-driven server actions.
Copy
// src/tools/refresh-data.tsimport { z } from 'zod';import type { AppToolConfig } from 'sunpeak/mcp';export const tool: AppToolConfig = { resource: 'dashboard', title: 'Refresh Data', description: 'Refresh dashboard data', _meta: { ui: { visibility: ['app'] } },};export const schema = {};export default async function () { const data = await fetchLatestMetrics(); return { structuredContent: data };}
The model never sees this tool. The resource component calls it directly with useCallServerTool.
Some hosts have size limits on tool call responses. Use an app-only tool with chunked responses to load large files (PDFs, images) without hitting limits.Server-side — return data in chunks with pagination:
Use useToolData’s inputPartial to show a preview while the LLM is still generating tool arguments. This lowers perceived latency for tools with large inputs like code or structured data.
Partial arguments are “healed” JSON — the host closes unclosed brackets to produce valid JSON. Objects may be incomplete (e.g., the last array item may be truncated). Use inputPartial only for preview UI, never for critical operations.
useUpdateModelContext gives you full control over what and when to send. Best for large or structured context, or when you want to batch updates.
Copy
const updateContext = useUpdateModelContext();// Use YAML frontmatter for structured data the model can parseawait updateContext({ content: [{ type: 'text', text: `---item-count: ${items.length}total-cost: ${total}currency: USD---User is viewing their shopping cart with ${items.length} items:${items.map(i => `- ${i.name}`).join('\n')}`, }],});
updateModelContext is deferred — the model sees the updated context on its next turn, not immediately. It does not trigger a model response. Use useSendMessage to trigger a response.
For recoverable state (current page, camera position, scroll position), use localStorage with a server-provided viewUUID.Server-side — include a viewUUID in the tool result _meta: