diff --git a/skills/README.md b/skills/README.md new file mode 100644 index 0000000..5195868 --- /dev/null +++ b/skills/README.md @@ -0,0 +1,45 @@ +# MCPEngine Skills + +These are agent skills (SKILL.md files) that guide AI agents through the MCP development pipeline. Each skill encodes the exact process, patterns, and standards for a specific phase of MCP server/app development. + +**These are the "secret sauce" — the encoded knowledge that lets agents build production-quality MCP servers autonomously.** + +--- + +## Pipeline Skills (in order) + +| # | Skill | Size | Purpose | +|---|-------|------|---------| +| 1 | **mcp-api-analyzer** | 43KB | Analyze API docs → structured analysis doc. Always the FIRST step. | +| 2 | **mcp-server-builder** | 88KB | Build a complete MCP server from the analysis doc. Every pattern and template. | +| 3 | **mcp-server-development** | 31KB | TypeScript MCP server patterns, best practices, error handling. | +| 4 | **mcp-app-designer** | 85KB | Design and build visual HTML apps for each MCP server. | +| 5 | **mcp-apps-integration** | 20KB | Add rich UI (structuredContent) to MCP tool results. | +| 6 | **mcp-apps-official** | 48KB | Official MCP Apps SDK patterns and host integration. | +| 7 | **mcp-apps-merged** | 39KB | Combined/merged MCP Apps reference. | +| 8 | **mcp-localbosses-integrator** | 61KB | Wire MCP servers + apps into LocalBosses Next.js app. | +| 9 | **mcp-qa-tester** | 113KB | Full QA framework — protocol, visual, functional, live API testing. | +| 10 | **mcp-deployment** | 17KB | Package and deploy — Docker, Railway, GitHub, production. | + +## Utility Skills + +| Skill | Purpose | +|-------|---------| +| **mcp-skill** | Exa MCP integration (web search, deep research) | + +--- + +## How agents use these + +1. Agent receives task (e.g., "build a Stripe MCP server") +2. Agent reads `mcp-api-analyzer/SKILL.md` → produces analysis doc +3. Agent reads `mcp-server-builder/SKILL.md` → builds the server +4. Agent reads `mcp-app-designer/SKILL.md` → builds UI apps +5. Agent reads `mcp-qa-tester/SKILL.md` → runs full test suite +6. Agent reads `mcp-deployment/SKILL.md` → packages for production + +Each skill is self-contained — an agent can pick up any step independently. + +--- + +## Total encoded knowledge: ~550KB of structured agent instructions diff --git a/skills/mcp-api-analyzer/SKILL.md b/skills/mcp-api-analyzer/SKILL.md new file mode 100644 index 0000000..7c6434c --- /dev/null +++ b/skills/mcp-api-analyzer/SKILL.md @@ -0,0 +1,869 @@ +# MCP API Analyzer — Phase 1: API Discovery & Analysis + +**When to use this skill:** You have API documentation (URLs, OpenAPI specs, user guides) for a service and need to produce a structured analysis document that feeds into the MCP Factory pipeline. This is always the FIRST step before building anything. + +**What this covers:** Reading API docs efficiently, cataloging endpoints, designing tool groups, naming tools, identifying app candidates, documenting auth flows and rate limits. Output is a single `{service}-api-analysis.md` file. + +**Pipeline position:** Phase 1 of 6 → Output feeds into `mcp-server-builder` (Phase 2) and `mcp-app-designer` (Phase 3) + +--- + +## 1. Inputs + +| Input | Required | Description | +|-------|----------|-------------| +| API documentation URL(s) | **Yes** | Primary reference docs | +| OpenAPI/Swagger spec | Preferred | Machine-readable endpoint catalog | +| User guides / tutorials | Nice-to-have | Helps understand real-world usage | +| Marketing / pricing page | Nice-to-have | Tier limits, feature gates | +| Existing SDK examples | Nice-to-have | Reveals common patterns | + +## 2. Output + +A single file: **`{service}-api-analysis.md`** + +Place it in the workspace root or alongside the future server directory: +``` +~/.clawdbot/workspace/{service}-api-analysis.md +``` + +This file is the sole input for Phase 2 (server build) and Phase 3 (app design). + +--- + +## 3. How to Read API Docs Efficiently + +### Step 0: API Style Detection + +**Identify the API style FIRST.** This determines how you read the docs and how tools are designed. + +| Style | Detection Signals | Tool Mapping | +|-------|-------------------|--------------| +| **REST** | Multiple URL paths, standard HTTP verbs (GET/POST/PUT/DELETE), resource-oriented URLs | 1 endpoint → 1 tool (standard) | +| **GraphQL** | Single `/graphql` endpoint, `query`/`mutation` in request body, schema introspection | Queries → read tools, Mutations → write tools, Subscriptions → skip (note for future) | +| **SOAP/XML** | WSDL file, XML request/response, `Content-Type: text/xml`, `.asmx` endpoints | Each WSDL operation → 1 tool, note XML→JSON transform needed | +| **gRPC** | `.proto` files, binary protocol, service/method definitions | Each RPC method → 1 tool, note HTTP/gRPC gateway if available | +| **WebSocket** | `ws://` or `wss://` URLs, persistent connections, event-based messaging | Message types → tools, note connection lifecycle management | + +**Adaptation notes for non-REST APIs:** + +- **GraphQL:** Download the schema (`{ __schema { types { name fields { name } } } }`). Group by query vs mutation. Each meaningful query/mutation becomes a tool. Combine related queries if they share variables. The server's API client sends POST requests with `{ query, variables }` — document the query string per tool. +- **SOAP:** Locate the WSDL. Each `` maps to a tool. Note the SOAPAction header. The server must transform XML responses to JSON — document the response mapping per tool. +- **gRPC:** Check for an HTTP/JSON gateway (many gRPC services expose one). If available, treat as REST. If not, the server needs a gRPC client — document the `.proto` service and method names. +- **WebSocket:** These are usually event-driven, not request/response. Map "send message" events to write tools. For incoming events, note them for future resource/subscription support. The server must manage a persistent connection. + +### What to READ (priority order): + +1. **Authentication page** — Read FIRST, completely. Auth determines everything. + - What type? (OAuth2, API key, JWT, session token, basic auth) + - Where does the token go? (Authorization header, query param, cookie) + - Token refresh flow? (Expiry, refresh tokens, re-auth) + - Scopes/permissions model? + +2. **Rate limits page** — Read SECOND. This constrains tool design. + - Requests per minute/hour/day? + - Per-endpoint limits vs global limits? + - Burst allowance? + - Rate limit headers? (X-RateLimit-Remaining, Retry-After) + +3. **API overview / getting started** — Skim for architecture patterns. + - REST vs GraphQL vs RPC? + - Base URL pattern (versioned? regional?) + - Common response envelope (data wrapper, pagination shape) + - Error response format + +4. **Endpoint reference** — Systematic scan, don't deep-dive yet. + - Group endpoints by resource/domain (contacts, deals, invoices, etc.) + - Note HTTP methods per endpoint (GET=read, POST=create, PUT=update, DELETE=delete) + - Flag endpoints with complex input (nested objects, file uploads, webhooks) + - Count total endpoints per group + +5. **Pagination docs** — Find the pagination pattern. + - Cursor-based vs offset-based vs page-based? + - What params? (page, limit, offset, cursor, startAfter) + - Max page size? + - How to detect "no more pages"? + +6. **Webhooks / events** — Note but don't deep-dive. + - Available webhook events (for future reference) + - Delivery format + +7. **Version & deprecation info** — Check for sunset timelines. + - Current stable version + - Any deprecated endpoints still in use + - Version header requirements (e.g., `API-Version: 2024-01-01`) + - Breaking changes in recent versions + +### What to SKIP (or skim very lightly): + +- SDK-specific guides (Python, Ruby, etc.) — we build our own client +- UI/dashboard tutorials — we only care about the API +- Community forums / blog posts — too noisy +- Deprecated endpoints — unless no replacement exists +- Webhook setup instructions — we consume the API, not webhooks (usually) + +### Speed technique for large APIs (50+ endpoints): + +1. If OpenAPI spec exists, download it and parse programmatically +2. Extract all paths + methods into a spreadsheet/list +3. Group by URL prefix (e.g., `/contacts/*`, `/deals/*`, `/invoices/*`) +4. Count endpoints per group +5. Read the 2-3 most important endpoints per group in detail +6. Note the pattern — most groups follow identical CRUD patterns + +### Pagination Pattern Catalog + +Different APIs use different pagination strategies. Identify which pattern(s) the API uses and document per the table below. + +| Pattern | How It Works | Request Next Page | Detect Last Page | Total Count | Example APIs | +|---------|-------------|-------------------|------------------|-------------|-------------| +| **Offset/Limit** | Skip N records, return M | `?offset=25&limit=25` | Results < limit, or offset ≥ total | Usually available | Most REST APIs | +| **Page Number** | Request page N of size M | `?page=2&pageSize=25` | Empty results, or page ≥ totalPages | Usually available | GHL, HubSpot | +| **Cursor (opaque)** | Server returns an opaque cursor string | `?cursor=abc123&limit=25` | Cursor is null/absent in response | Rarely available | Slack, Facebook | +| **Keyset (Stripe-style)** | Use last item's ID as boundary | `?starting_after=obj_xxx&limit=25` | `has_more: false` in response | Rarely available | Stripe, Intercom | +| **Link Header** | Server returns `Link: ; rel="next"` in headers | Follow the `rel="next"` URL directly | No `rel="next"` link in response | Sometimes via `rel="last"` | GitHub, many REST APIs | +| **Scroll/Search-After** | Server returns a sort-value array to continue from | `?search_after=[timestamp, id]` | Empty results | Via separate count query | Elasticsearch | +| **Composite Cursor** | Base64-encoded JSON with multiple sort fields | `?cursor=eyJpZCI6MTIzLCJ...}` | Decoded cursor has `done: true`, or results empty | Rarely available | Internal APIs, GraphQL relay | +| **Token-Based (AWS-style)** | Server returns a `NextToken` / `NextContinuationToken` | Pass `NextToken` in next request body/params | `NextToken` is absent in response | Sometimes via separate field | AWS (S3, DynamoDB, SQS) | + +**For each pattern, document:** +- How to request the next page +- How to detect the last page (no more data) +- Whether total count is available +- Whether backwards pagination is supported +- Max page size allowed + +--- + +## 4. Analysis Document Template + +Use this EXACT template. Every section is required. + +````markdown +# {Service Name} — MCP API Analysis + +**Date:** {YYYY-MM-DD} +**API Version:** {version} +**Base URL:** `{base_url}` +**Documentation:** {docs_url} +**OpenAPI Spec:** {spec_url or "Not available"} + +--- + +## 1. Service Overview + +**What it does:** {1-2 sentence description} +**Target users:** {Who uses this product} +**Pricing tiers:** {Free / Starter / Pro / Enterprise — note API access level per tier} +**API access:** {Which tiers include API access, any costs per call} + +--- + +## 2. Authentication + +**Method:** {OAuth2 / API Key / JWT / Basic Auth / Custom} + +### Auth Flow: +``` +{Step-by-step auth flow} +1. {First step} +2. {Second step} +3. {How to get/refresh token} +``` + +### OAuth2 Details (if applicable): +- **Grant type:** {authorization_code / client_credentials / PKCE / device_code} +- **Authorization URL:** `{url}` +- **Token URL:** `{url}` +- **Redirect URI requirements:** {localhost allowed? specific paths?} +- **Scopes required:** {list scopes and what they grant} +- **PKCE required?** {yes/no — required for public clients} + +### Headers: +``` +Authorization: {Bearer {token} / Basic {base64} / X-API-Key: {key}} +Content-Type: application/json +{Any other required headers, e.g., X-Account-ID} +``` + +### Environment Variables Needed: +```bash +{SERVICE}_API_KEY= +{SERVICE}_API_SECRET= # If OAuth2 +{SERVICE}_BASE_URL= # If configurable/sandbox +{SERVICE}_ACCOUNT_ID= # If multi-tenant +``` + +### Token Lifecycle: +- **Token type:** {access token / API key / JWT} +- **Expiry:** {duration or "never" for API keys} +- **Refresh mechanism:** {refresh token endpoint / re-auth / N/A} +- **Refresh token expiry:** {duration or "never"} +- **Caching strategy:** {Cache token, refresh 5 min before expiry} +- **Storage for long-running server:** {Token stored in memory, refresh before expiry. For OAuth2 auth code flow: initial token obtained via browser flow, server stores refresh token and auto-refreshes.} + +### Key Rotation / Compromise: +- **Rotation procedure:** {How to generate new keys/secrets} +- **Revocation endpoint:** {URL to revoke compromised tokens, or "manual via dashboard"} +- **Grace period:** {Does old key continue working after rotation? For how long?} + +--- + +## 3. API Patterns + +**Style:** {REST / GraphQL / SOAP / gRPC / WebSocket} +**Non-REST adaptation notes:** {If non-REST, note how tools map — see API Style Detection above} +**Response envelope:** +```json +{ + "data": [...], + "meta": { "total": 100, "page": 1, "pageSize": 25 } +} +``` + +**Pagination:** +- **Type:** {cursor / offset / page-based / keyset / link-header / token-based} +- **Parameters:** {page, pageSize / limit, offset / cursor, limit / starting_after} +- **Max page size:** {number} +- **End detection:** {empty array / hasMore field / next cursor is null / no Link rel="next"} +- **Total count available:** {yes — in meta.total / no / separate count endpoint} +- **Backwards pagination:** {supported / not supported} + +**Error format:** +```json +{ + "error": { "code": "NOT_FOUND", "message": "Resource not found" } +} +``` + +**Rate limits:** +- **Global:** {X requests per Y} +- **Per-endpoint:** {Any specific limits} +- **Burst allowance:** {Token bucket / leaky bucket / simple counter} +- **Rate limit scope:** {per-key / per-endpoint / per-user} +- **Exceeded penalty:** {429 response / temporary ban / throttled response} +- **Headers:** {X-RateLimit-Remaining, Retry-After} +- **Strategy:** {Exponential backoff / fixed delay / queue} + +**Sandbox / Test Environment:** +- **Available:** {yes / no} +- **Sandbox base URL:** `{sandbox_url or "N/A"}` +- **How to access:** {Separate API key / toggle in dashboard / different subdomain} +- **Limitations:** {Rate limits differ? Data resets? Feature parity with production?} +- **QA impact:** {Can QA use sandbox for live API testing? Any endpoints unavailable in sandbox?} + +> **Why this matters:** If a sandbox exists, QA testing (Phase 5) can run against it safely without affecting production data. If no sandbox, QA must use mocks or test carefully with real data. Document this early — it directly affects the testing strategy. + +--- + +## 4. Version & Deprecation + +- **Current stable version:** {e.g., v2, 2024-01-01} +- **Version mechanism:** {URL path (/v2/), header (API-Version: 2024-01-01), query param} +- **Version header requirements:** {Required header name and format, if any} +- **Deprecation timeline:** {Any endpoints or versions being sunset — with dates} +- **Breaking changes in recent versions:** {Notable changes that affect tool design} +- **Changelog URL:** {Link to changelog/migration guide for reference} + +--- + +## 5. Endpoint Catalog + +### Group: {Domain Name} ({count} endpoints) + +| Method | Path | Description | Notes | +|--------|------|-------------|-------| +| GET | `/resource` | List resources | Paginated, filterable | +| GET | `/resource/{id}` | Get single resource | | +| POST | `/resource` | Create resource | Required: name, email | +| PUT | `/resource/{id}` | Update resource | Partial update supported | +| DELETE | `/resource/{id}` | Delete resource | Soft delete | + +{Repeat for each domain group} + +### Group: {Next Domain} ({count} endpoints) +... + +**Total endpoints:** {count} + +--- + +## 6. Tool Groups (for Lazy Loading) + +Tools are organized into groups that load on-demand. Each group maps to a domain. + +| Group Name | Tools | Load Trigger | Description | +|------------|-------|--------------|-------------| +| `contacts` | {count} | User asks about contacts | Contact CRUD, search, tags | +| `deals` | {count} | User asks about deals/pipeline | Deal management, stages | +| `invoicing` | {count} | User asks about invoices/payments | Invoice CRUD, payments | +| `calendar` | {count} | User asks about scheduling | Appointments, availability | +| `analytics` | {count} | User asks for reports/metrics | Dashboards, KPIs | +| `admin` | {count} | User asks about settings/config | Users, permissions, webhooks | + +**Target:** 5-15 groups, 3-15 tools per group. No group should exceed 20 tools. + +--- + +## 7. Tool Inventory + +### Group: {group_name} + +#### `list_{resources}` +- **Title:** List {Resources} +- **Icon:** `{service-cdn-url}/list-icon.svg` *(or omit if no suitable icon — SVG preferred)* +- **Description:** List {resources} with optional filters and pagination. Returns `{key_field_1, key_field_2, key_field_3, status}` for each {resource}. Use when the user wants to browse, filter, or get an overview of multiple {resources}. Do NOT use when searching by specific keyword (use `search_{resources}` instead) or for getting full details of one {resource} (use `get_{resource}` instead). +- **HTTP:** GET `/resource` +- **Annotations:** `readOnlyHint: true`, `destructiveHint: false`, `idempotentHint: true`, `openWorldHint: false` +- **Parameters:** + | Param | Type | Required | Description | + |-------|------|----------|-------------| + | page | number | No | Page number (default 1) | + | pageSize | number | No | Results per page (default 25, max 100) | + | query | string | No | Search by name, email, or phone | + | status | string | No | Filter: active, inactive, all | + | sortBy | string | No | Sort field: created, updated, name | +- **Output Schema:** `{ data: Resource[], meta: { total: number, page: number, pageSize: number } }` +- **Content Annotations:** `audience: ["user", "assistant"]`, `priority: 0.7` +- **Response shape:** `{ data: Resource[], meta: { total, page, pageSize } }` + +#### `get_{resource}` +- **Title:** Get {Resource} Details +- **Icon:** `{service-cdn-url}/detail-icon.svg` *(optional)* +- **Description:** Get complete details for a single {resource} by ID. Returns all fields including `{notable_field_1, notable_field_2, notable_field_3}`. Use when the user references a specific {resource} by name/ID or needs detailed information about one {resource}. Do NOT use when the user wants to browse multiple {resources} (use `list_{resources}` instead). +- **HTTP:** GET `/resource/{id}` +- **Annotations:** `readOnlyHint: true`, `destructiveHint: false`, `idempotentHint: true`, `openWorldHint: false` +- **Parameters:** + | Param | Type | Required | Description | + |-------|------|----------|-------------| + | {resource}_id | string | **Yes** | {Resource} ID | +- **Output Schema:** `Resource` (full object with all fields) +- **Content Annotations:** `audience: ["user"]`, `priority: 0.8` +- **Response shape:** `Resource` + +#### `create_{resource}` +- **Title:** Create New {Resource} +- **Icon:** `{service-cdn-url}/create-icon.svg` *(optional)* +- **Description:** Create a new {resource}. Returns the created {resource} with its assigned ID. Use when the user wants to add, create, or set up a new {resource}. Do NOT use when updating an existing {resource} (use `update_{resource}` instead). Side effect: creates a permanent record in the system. +- **HTTP:** POST `/resource` +- **Annotations:** `readOnlyHint: false`, `destructiveHint: false`, `idempotentHint: false`, `openWorldHint: false` +- **Parameters:** + | Param | Type | Required | Description | + |-------|------|----------|-------------| + | name | string | **Yes** | {Resource} name | + | email | string | No | Email address | + | {etc.} | | | | +- **Output Schema:** `Resource` (created object with ID) +- **Content Annotations:** `audience: ["user"]`, `priority: 0.9` +- **Response shape:** `Resource` + +#### `update_{resource}` +- **Title:** Update {Resource} +- **Icon:** `{service-cdn-url}/edit-icon.svg` *(optional)* +- **Description:** Update an existing {resource}. Only include fields to change — omitted fields remain unchanged. Returns the updated {resource}. Use when the user wants to modify, change, or edit a {resource}. Do NOT use when creating a new {resource} (use `create_{resource}` instead). Side effect: modifies the existing record. +- **HTTP:** PUT `/resource/{id}` +- **Annotations:** `readOnlyHint: false`, `destructiveHint: false`, `idempotentHint: true`, `openWorldHint: false` +- **Parameters:** + | Param | Type | Required | Description | + |-------|------|----------|-------------| + | {resource}_id | string | **Yes** | {Resource} ID | + | {fields...} | | No | Fields to update | +- **Output Schema:** `Resource` (updated object) +- **Content Annotations:** `audience: ["user"]`, `priority: 0.9` +- **Response shape:** `Resource` + +#### `delete_{resource}` +- **Title:** Delete {Resource} +- **Icon:** `{service-cdn-url}/delete-icon.svg` *(optional)* +- **Description:** Delete a {resource} permanently. This cannot be undone. Use only when the user explicitly asks to delete or remove a {resource}. Do NOT use for archiving, deactivating, or hiding (use `update_{resource}` with status change instead, if available). Side effect: permanently removes the record. +- **HTTP:** DELETE `/resource/{id}` +- **Annotations:** `readOnlyHint: false`, `destructiveHint: true`, `idempotentHint: true`, `openWorldHint: false` +- **Parameters:** + | Param | Type | Required | Description | + |-------|------|----------|-------------| + | {resource}_id | string | **Yes** | {Resource} ID | +- **Output Schema:** `{ success: boolean }` +- **Content Annotations:** `audience: ["user"]`, `priority: 1.0` +- **Response shape:** `{ success: true }` + +{Repeat for each tool in each group} + +### Disambiguation Table (per group) + +For each tool group, produce a disambiguation matrix to guide tool routing: + +| User says... | Correct tool | Why not others | +|---|---|---| +| "Show me all {resources}" | `list_{resources}` | Not `search_` (no keyword), not `get_` (not one specific item) | +| "Find {name}" | `search_{resources}` | Not `list_` (specific name = search), not `get_` (no ID provided) | +| "What's {name}'s email?" | `get_{resource}` | Not `list_`/`search_` (asking about a specific known {resource}) | +| "Add a new {resource}" | `create_{resource}` | Not `update_` (new, not existing) | +| "Change {name}'s phone number" | `update_{resource}` | Not `create_` (modifying existing) | +| "Remove {name}" | `delete_{resource}` | Not `update_` (user said remove/delete, not deactivate) | + +### Common User Intent Clustering + +For each disambiguation entry, consider **diverse phrasings** real users would type. Cluster by intent to ensure the tool description handles all variants: + +| Intent | Common Phrasings | Target Tool | +|--------|-----------------|-------------| +| Browse/overview | "show me", "list", "what are my", "pull up", "let me see", "give me all" | `list_{resources}` | +| Search/find | "find", "search for", "look up", "where is", "do I have a" | `search_{resources}` | +| Detail/inspect | "tell me about", "what's the status of", "show me details for", "more info on" | `get_{resource}` | +| Create/add | "add", "create", "new", "set up", "register", "make a" | `create_{resource}` | +| Modify/edit | "change", "update", "edit", "modify", "fix", "set X to Y" | `update_{resource}` | +| Remove/delete | "delete", "remove", "get rid of", "cancel", "drop" | `delete_{resource}` | + +> **Tip:** When writing tool descriptions, ensure the "When to use" clause covers the most common phrasings for that intent. The "When NOT to use" clause should address the top misrouting risk (e.g., `list_` vs `search_` is the most common confusion). + +--- + +## 8. App Candidates + +### Dashboard Apps +| App ID | Name | Data Source Tools | Description | +|--------|------|-------------------|-------------| +| `{svc}-dashboard` | {Service} Dashboard | `get_analytics`, `list_*` | Overview KPIs, recent activity | + +### Data Grid Apps +| App ID | Name | Data Source Tools | Description | +|--------|------|-------------------|-------------| +| `{svc}-contact-grid` | Contacts | `list_contacts`, `search_contacts` | Searchable contact list | + +### Detail Card Apps +| App ID | Name | Data Source Tools | Description | +|--------|------|-------------------|-------------| +| `{svc}-contact-card` | Contact Card | `get_contact` | Single contact deep-dive | + +### Form/Wizard Apps +| App ID | Name | Data Source Tools | Description | +|--------|------|-------------------|-------------| +| `{svc}-contact-creator` | New Contact | `create_contact` | Contact creation form | + +### Specialized Apps +| App ID | Name | Type | Data Source Tools | Description | +|--------|------|------|-------------------|-------------| +| `{svc}-calendar` | Calendar | calendar | `list_appointments` | Appointment calendar | +| `{svc}-pipeline` | Pipeline | funnel | `list_deals` | Deal pipeline kanban | +| `{svc}-timeline` | Activity | timeline | `get_activity` | Activity feed | + +--- + +## 9. Elicitation Candidates + +Identify flows where the MCP server should request user input mid-operation using the MCP Elicitation capability (`elicitation/create`). These are interactions where the server needs information or confirmation from the user before proceeding. + +### When to flag a flow for elicitation: + +- **OAuth account selection** — API supports multiple connected accounts; server needs user to choose which one +- **Destructive operation confirmation** — DELETE or irreversible actions should confirm before executing +- **Ambiguous input resolution** — User says "delete the contact" but there are 3 matches; server asks which one +- **Multi-step wizards** — Creating a complex resource that requires sequential input (e.g., create event → pick calendar → set time → invite attendees) +- **Scope/permission escalation** — Action requires additional OAuth scopes the user hasn't granted +- **Payment/billing actions** — Any action that costs money should confirm amount and target + +### Elicitation Candidate Template: + +| Flow | Trigger | Elicitation Type | User Input Needed | Fallback (if elicitation unsupported) | +|------|---------|-----------------|--------------------|-----------------------------------------| +| Delete {resource} | `delete_{resource}` called | Confirmation | "Confirm delete {name}? (yes/no)" | Return warning text, require second call | +| Connect account | First API call with OAuth | Selection | "Which account? (list options)" | Use default/first account | +| Bulk action | `bulk_update` with >10 items | Confirmation | "Update {N} records? (yes/no)" | Cap at 10, warn about limit | +| {Describe flow} | {What triggers it} | {Confirmation / Selection / Form} | {What the user sees} | {What happens if client doesn't support elicitation} | + +**Important:** Always plan a fallback for clients that don't support elicitation. The server should still function — it just may require the user to provide the information in their original message or via a follow-up tool call. + +--- + +## 10. Task Candidates (Async Operations) + +Identify tools where the operation may take >10 seconds and should be executed asynchronously using MCP Tasks (spec 2025-11-25, experimental SEP-1686). + +### When to flag a tool for async/task support: +- **Report generation** — compiling analytics, PDFs, exports +- **Bulk operations** — updating 100+ records, mass imports +- **External processing** — waiting on third-party webhooks, payment processing +- **Data migration** — moving large datasets between systems +- **File generation** — creating CSVs, spreadsheets, archives + +### Task Candidate Template: + +| Tool | Typical Duration | Task Support | Recommended Polling Interval | +|------|-----------------|-------------|------------------------------| +| `export_report` | 30-120s | required | 5000ms | +| `bulk_update` | 10-60s | optional | 3000ms | +| `generate_invoice_pdf` | 5-15s | optional | 2000ms | +| `{tool_name}` | {duration} | {required/optional/forbidden} | {interval} | + +> **Note:** Most tools should be `forbidden` for task support — only flag tools that genuinely need async execution. If the operation completes in <5 seconds, don't use tasks. + +--- + +## 11. Data Shape Contracts + +For each app candidate, define the exact mapping from tool `outputSchema` to what the app's `render()` function expects. This contract prevents silent data shape mismatches. + +### Contract Template: + +| App | Source Tool | Tool outputSchema Key Fields | App Expected Fields | Transform Notes | +|-----|------------|------------------------------|---------------------|-----------------| +| `{svc}-contact-grid` | `list_contacts` | `data[].{name,email,status}`, `meta.{total,page,pageSize}` | `data[].{name,email,status}`, `meta.{total,page,pageSize}` | Direct pass-through | +| `{svc}-dashboard` | `get_analytics` | `{revenue,contacts,deals}` | `metrics.{revenue,contacts,deals}`, `recent[]` | LLM restructures into metrics + recent | +| `{svc}-{type}` | `{tool}` | `{fields}` | `{fields}` | `{notes}` | + +### Contract Rules: +1. **Direct pass-through** — When tool output matches app input exactly. Preferred. +2. **LLM transform** — When the LLM must restructure data (via APP_DATA). Document the mapping explicitly so system prompts can reference it. +3. **Aggregation** — When an app needs data from multiple tools. List all source tools and how their outputs combine. + +### Validation: +- The builder should set `outputSchema` to match the contract +- The designer should set `validateData()` to check for the contracted fields +- The integrator's `systemPromptAddon` should reference these contracts for APP_DATA generation + +--- + +## 12. Naming Conventions + +### Tool names: `{verb}_{noun}` +- `list_contacts`, `get_contact`, `create_contact`, `update_contact`, `delete_contact` +- `search_contacts` (if separate from list) +- `send_message`, `schedule_appointment`, `export_report` + +### Semantic Clustering — Verb Prefix Conventions + +Use consistent verb prefixes to signal intent. This helps the LLM distinguish between tools with related names and reduces misrouting. + +| Prefix | Intent | Maps to HTTP | Examples | +|--------|--------|-------------|----------| +| `browse_` or `list_` | List/overview of multiple items | GET (collection) | `list_contacts`, `browse_invoices` | +| `inspect_` or `get_` | Deep-dive into a single item | GET (single) | `get_contact`, `inspect_deal` | +| `modify_` or `create_` / `update_` | Create or change a resource | POST / PUT | `create_contact`, `update_deal` | +| `remove_` or `delete_` | Delete a resource | DELETE | `delete_contact`, `remove_tag` | +| `search_` | Full-text or keyword search | GET (with query) | `search_contacts` | +| `send_` | Dispatch a message/notification | POST (side effect) | `send_email`, `send_sms` | +| `export_` | Generate a report/file | GET or POST | `export_report` | + +**Guidelines:** +- Pick ONE prefix style per server and be consistent (either `list_`/`get_` or `browse_`/`inspect_`, not both) +- The standard `list_`/`get_`/`create_`/`update_`/`delete_` is recommended for most APIs +- Use `browse_`/`inspect_`/`modify_`/`remove_` only if you need to avoid ambiguity with existing tool names or if the API's language uses these verbs naturally +- For mutually exclusive tools, add "INSTEAD OF" notes in descriptions (e.g., "Use `search_contacts` INSTEAD OF `list_contacts` when the user provides a keyword") + +### App IDs: `{service}-{type}-{optional-qualifier}` +- `{svc}-dashboard`, `{svc}-contact-grid`, `{svc}-contact-card` +- `{svc}-pipeline-kanban`, `{svc}-calendar-view`, `{svc}-activity-timeline` + +### Tool group names: lowercase, domain-based +- `contacts`, `deals`, `invoicing`, `calendar`, `analytics`, `admin` + +--- + +## 13. Quirks & Gotchas + +{List any API-specific issues discovered during analysis} + +- {e.g., "Delete endpoint returns 200 with empty body, not 204"} +- {e.g., "Pagination starts at 0, not 1"} +- {e.g., "Date fields use Unix timestamps, not ISO 8601"} +- {e.g., "Rate limit resets at midnight UTC, not rolling window"} +- {e.g., "Sandbox environment has different base URL"} + +--- + +## 14. Implementation Priority + +### Phase 1 (Core — build first): +1. {most-used-group} — {why} +2. {second-group} — {why} + +### Phase 2 (Important — build second): +3. {third-group} — {why} +4. {fourth-group} — {why} + +### Phase 3 (Nice-to-have — build if time): +5. {remaining-groups} + +### App Priority: +1. {svc}-dashboard — Always build the dashboard first +2. {svc}-{most-used-grid} — Most common data view +3. {svc}-{most-used-detail} — Detail for most common entity + +--- + +## 5. Tool Description Best Practices + +Tool descriptions are the #1 factor in whether an LLM correctly routes to the right tool. Follow these rules: + +### The Description Formula (6-part): + +``` +{What it does}. {What it returns — include 2-3 key field names}. +{When to use it — specific user intents}. {When NOT to use it — disambiguation}. +{Side effects — if any}. +``` + +Every tool description MUST include the "When NOT to use" clause. Research shows this single addition reduces tool misrouting by ~30%. + +### Before/After Example: + +**❌ BEFORE (too vague, no disambiguation):** +``` +"List contacts with optional filters. Returns paginated results including name, email, phone, +and status. Use when the user wants to see, search, or browse their contact list." +``` + +**✅ AFTER (specific, disambiguated, actionable):** +``` +"List contacts with optional filters and pagination. Returns {name, email, phone, status, +created_date} for each contact, plus {total, page, pageSize} metadata. Use when the user +wants to browse, filter, or get an overview of multiple contacts. Do NOT use when searching +by specific keyword (use search_contacts instead) or for getting full details of one contact +(use get_contact instead). Read-only, no side effects." +``` + +### For similar tools, differentiate clearly: +``` +list_contacts: "...browse, filter, or get an overview of multiple contacts. + Do NOT use when searching by keyword (use search_contacts) or looking up one contact (use get_contact)." +search_contacts: "...full-text search across all contact fields by keyword. + Do NOT use when browsing without a search term (use list_contacts) or when the user has a specific ID (use get_contact)." +get_contact: "...get complete details for one contact by ID. + Do NOT use when the user wants multiple contacts (use list_contacts) or is searching by name (use search_contacts)." +``` + +### Token Budget Awareness + +Tool descriptions consume context window tokens. Every tool definition averages 50-200 tokens depending on schema complexity. With 50+ tools, this is 10,000+ tokens before any work begins. + +**Targets:** +- **Total tool definition tokens per server:** Under 5,000 tokens +- **Per-tool target:** ~200 tokens (description + schema combined) +- **Active tools per interaction:** Cap at 15-20 via lazy loading + +**Optimization techniques:** +- Be concise — every word must earn its place +- Eliminate redundant descriptions between the tool description and parameter descriptions +- Use field name lists (`{name, email, phone}`) instead of prose descriptions of return values +- Combine overlapping tools when the distinction is minor (e.g., `list_contacts` with optional `query` param instead of separate `list_contacts` + `search_contacts`) + +### Tool Count Optimization + +If a tool group exceeds 15 tools, consider combining: + +| Instead of... | Combine into... | How | +|---------------|-----------------|-----| +| `list_contacts` + `search_contacts` | `list_contacts` with optional `query` param | Add `query` as optional filter | +| `get_contact_email` + `get_contact_phone` + `get_contact_address` | `get_contact` (returns all fields) | Single tool, all fields returned | +| `create_contact` + `create_lead` + `create_prospect` | `create_contact` with `type` param | Use enum parameter for type | +| `get_report_daily` + `get_report_weekly` + `get_report_monthly` | `get_report` with `period` param | Use enum parameter for period | + +**Rule of thumb:** If two tools share >80% of their parameters and the same endpoint pattern, they should be one tool with a distinguishing parameter. + +--- + +## 6. MCP Annotation Rules + +Every tool MUST have annotations. Use this decision tree: + +``` +Is it a GET/read operation? + → readOnlyHint: true, destructiveHint: false + +Is it a DELETE operation? + → readOnlyHint: false, destructiveHint: true + +Is it a POST/create operation? + → readOnlyHint: false, destructiveHint: false, idempotentHint: false + +Is it a PUT/upsert operation? + → readOnlyHint: false, destructiveHint: false, idempotentHint: true + +Does it affect external systems outside this API? + → openWorldHint: true (rare — most API tools are openWorldHint: false) +``` + +--- + +## 7. Content Annotations Planning + +MCP content blocks can carry `audience` and `priority` annotations that control how tool outputs are routed. Plan these during analysis — they feed directly into the server builder. + +### Audience Annotation: +- `["user"]` — Output is for the end user (show in UI/app, don't feed back to LLM for reasoning) +- `["assistant"]` — Output is for the LLM (feed into context for multi-step reasoning, don't show to user) +- `["user", "assistant"]` — Both (show to user AND available for LLM reasoning — the default) + +### Priority Annotation (0.0 to 1.0): +- `1.0` — Critical, always show prominently (destructive operation results, errors, confirmations) +- `0.7-0.9` — Important, show normally (most tool results) +- `0.3-0.6` — Supplementary, can be collapsed/summarized (metadata, pagination info) +- `0.0-0.2` — Low priority, assistant-only (debug info, internal state) + +### Planning Guidelines: + +| Tool Type | Audience | Priority | Rationale | +|-----------|----------|----------|-----------| +| `list_*` | `["user", "assistant"]` | 0.7 | User sees data, LLM may use for follow-up | +| `get_*` | `["user"]` | 0.8 | Primarily for user display | +| `create_*` / `update_*` | `["user"]` | 0.9 | User needs confirmation of changes | +| `delete_*` | `["user"]` | 1.0 | Critical — user must see result | +| `search_*` | `["user", "assistant"]` | 0.7 | User sees results, LLM may refine | +| Analytics/aggregation | `["user"]` | 0.8 | Dashboard-type data, primarily visual | +| Internal/helper tools | `["assistant"]` | 0.3 | LLM uses for reasoning, user doesn't need to see | + +--- + +## 8. App Candidate Selection Criteria + +Not every endpoint deserves an app. Use this checklist: + +### BUILD an app when: +- ✅ The data is a **list** that benefits from search/filter UI (data grid) +- ✅ The data is **complex** with many fields (detail card) +- ✅ There are **aggregate metrics** or KPIs (dashboard) +- ✅ The data is **date-based** and benefits from calendar layout (calendar) +- ✅ The data has **stages/phases** (funnel/kanban) +- ✅ The data is **chronological events** (timeline) +- ✅ There's a **multi-step creation flow** (form/wizard) + +### SKIP an app when: +- ❌ It's a simple CRUD with 2-3 fields (just use the tool directly) +- ❌ The response is a simple success/fail (no visual benefit) +- ❌ It's a settings/config endpoint (rarely needed in UI) +- ❌ It's a batch/background operation (status check is enough) + +### App count targets: +- **Small API (10-20 endpoints):** 3-5 apps +- **Medium API (20-50 endpoints):** 5-10 apps +- **Large API (50+ endpoints):** 10-20 apps +- **Never exceed 25 apps** for a single service — diminishing returns + +--- + +## 9. Quality Gate Checklist + +Before passing the analysis doc to Phase 2, verify: + +### Core Completeness: +- [ ] **API style identified** — REST/GraphQL/SOAP/gRPC/WebSocket documented with adaptation notes if non-REST +- [ ] **Every endpoint is cataloged** — no missing endpoints from the API reference +- [ ] **Tool groups are balanced** — no group with 50+ tools, aim for 3-15 per group +- [ ] **Active tool count is manageable** — total tools ≤ 60, each lazy-loaded group ≤ 20, active per interaction ≤ 15-20 + +### Tool Quality: +- [ ] **Tool descriptions follow 6-part formula** — What / Returns (field names) / When to use / When NOT to use / Side effects +- [ ] **Every tool has a `title` field** — Human-readable display name separate from machine name +- [ ] **Every tool has an `outputSchema` planned** — Expected response structure documented +- [ ] **Every tool has annotations planned** — readOnlyHint, destructiveHint, idempotentHint, openWorldHint +- [ ] **Content annotations planned** — audience and priority assigned per tool type +- [ ] **Disambiguation tables exist** — For each tool group with similar tools, "User says X → Correct tool → Why not others" +- [ ] **Semantic verb prefixes are consistent** — list_/get_/create_/update_/delete_ (or chosen alternative) used uniformly + +### Auth & Infrastructure: +- [ ] **Auth flow is complete** — Step-by-step, env vars listed, refresh strategy documented +- [ ] **OAuth2 subtype identified** — If OAuth2: grant type, PKCE, scopes, token lifetime documented +- [ ] **Token lifecycle documented** — Expiry, refresh, storage strategy for long-running server, key rotation procedure +- [ ] **Pagination pattern identified** — Type, params, max size, end detection, total count availability +- [ ] **Rate limits are documented** — Global + per-endpoint, burst behavior, scope, penalty + +### Planning: +- [ ] **Version & deprecation documented** — Current version, sunset timelines, version header requirements +- [ ] **App candidates have clear data sources** — Each app maps to specific tool(s) +- [ ] **Data shape contracts defined** — Tool outputSchema → app expected input mapped per app candidate +- [ ] **Elicitation candidates identified** — Destructive operations, ambiguous inputs, multi-step flows, account selection +- [ ] **Task candidates identified** — Long-running operations flagged with polling intervals +- [ ] **Icon planning noted per tool** — SVG preferred, at least noted even if deferred +- [ ] **Sandbox/test environment documented** — Availability, URL, QA impact +- [ ] **Error format is documented** — Response shape, common error codes +- [ ] **Naming follows conventions** — verb_noun tools, service-type app IDs, consistent verb prefixes +- [ ] **User intent clustering done** — Diverse phrasings per disambiguation entry +- [ ] **Quirks & gotchas captured** — API-specific oddities that affect implementation + +--- + +## 10. Example: Completed Analysis (abbreviated) + +```markdown +# Calendly — MCP API Analysis + +**Date:** 2026-02-04 +**API Version:** v2 +**Base URL:** `https://api.calendly.com` +**Documentation:** https://developer.calendly.com/api-docs + +## 1. Service Overview +**What it does:** Scheduling automation platform +**API Style:** REST + +## 2. Authentication +**Method:** OAuth2 (Personal Access Token also available) +**OAuth2 Grant Type:** authorization_code (PKCE recommended for public clients) +**Token Expiry:** 2 hours (refresh token: 30 days) +Headers: `Authorization: Bearer {token}` + +## 4. Version & Deprecation +**Current Version:** v2 (v1 sunset: 2024-06-01) +**Version Mechanism:** URL path (/api/v2/) + +## 6. Tool Groups +| Group | Tools | Description | +|-------|-------|-------------| +| `scheduling` | 8 | Event types, scheduling links | +| `events` | 6 | Scheduled events, invitees | +| `users` | 4 | User profiles, org membership | +| `webhooks` | 3 | Webhook subscriptions | + +## 7. Tool Inventory (example tool) +### `list_events` +- **Title:** List Scheduled Events +- **Description:** List scheduled events with date range and status filters. Returns {name, start_time, end_time, status, invitee_count} per event. Use when user wants to see upcoming or past events. Do NOT use for event type management (use list_event_types) or single event details (use get_event). Read-only. +- **Output Schema:** `{ collection: Event[], pagination: { count, next_page_token } }` +- **Content Annotations:** `audience: ["user", "assistant"]`, `priority: 0.7` + +## 8. App Candidates +- calendly-dashboard (Dashboard) — event counts, upcoming schedule +- calendly-event-grid (Data Grid) — list scheduled events +- calendly-event-detail (Detail Card) — single event with invitee info +- calendly-calendar (Calendar) — visual calendar of events +- calendly-availability (Form) — set availability preferences + +## 9. Elicitation Candidates +| Flow | Trigger | Type | User Input | Fallback | +|------|---------|------|------------|----------| +| Cancel event | `cancel_event` | Confirmation | "Cancel event with {invitee}?" | Require explicit confirmation in message | +| Connect calendar | Initial setup | Selection | "Which calendar provider?" | Default to primary calendar | +``` + +--- + +## 11. Execution Workflow + +``` +1. Receive API docs URL(s) from user +2. Identify API style (REST/GraphQL/SOAP/gRPC/WebSocket) +3. Read auth page → Document auth flow (including OAuth2 subtype, token lifecycle, key rotation) +4. Read rate limits → Document constraints (including burst, scope, penalty) +5. Check sandbox/test environment → Document availability, URL, and QA impact +6. Check version/deprecation → Document current version and sunset timelines +7. Scan all endpoints → Build endpoint catalog +8. Group endpoints by domain → Define tool groups (cap at 15-20 active per interaction) +9. Name each tool → Write 6-part descriptions with annotations, title, outputSchema, content annotations, icon +10. Build disambiguation tables with user intent clustering for each tool group +11. Identify elicitation candidates (destructive ops, ambiguous inputs, multi-step flows) +12. Identify task candidates (long-running operations >10s) +13. Identify app candidates → Map to data source tools +14. Define data shape contracts (tool outputSchema → app expected input) +15. Document quirks/gotchas +16. Set implementation priority +17. Run quality gate checklist +18. Output: {service}-api-analysis.md +``` + +**Estimated time:** 30-60 minutes for small APIs, 1-2 hours for large APIs (50+ endpoints) + +**Agent model recommendation:** Opus — requires deep reading comprehension and strategic judgment for tool grouping and app candidate selection. + +--- + +*This skill is Phase 1 of the MCP Factory pipeline. The analysis document it produces is the single source of truth for all subsequent phases.* diff --git a/skills/mcp-app-designer/SKILL.md b/skills/mcp-app-designer/SKILL.md new file mode 100644 index 0000000..b35fd05 --- /dev/null +++ b/skills/mcp-app-designer/SKILL.md @@ -0,0 +1,2170 @@ +# MCP App Designer — Phase 3: Design & Build HTML Apps + +**When to use this skill:** You have a `{service}-api-analysis.md` (specifically the App Candidates section) and optionally a built MCP server, and need to create the visual HTML apps that render in LocalBosses. Each app is a single self-contained HTML file. + +**What this covers:** Dark theme design specs, 9 app type patterns (including Interactive Data Grid), data visualization primitives, accessibility fundamentals, micro-interactions, bidirectional communication, the exact HTML template with data reception, responsive design, three-state rendering (loading/empty/data), and data flow architecture. + +**Pipeline position:** Phase 3 of 6 → Input from `mcp-api-analyzer` (Phase 1), can run parallel with `mcp-server-builder` (Phase 2). Output feeds `mcp-localbosses-integrator` (Phase 4). + +--- + +## 1. Inputs & Outputs + +**Inputs:** +- `{service}-api-analysis.md` — App Candidates section (which apps to build, data sources) +- Tool definitions (from Phase 2 server or analysis doc) — what data shapes to expect + +**Output:** HTML app files in `{service}-mcp/app-ui/`: +``` +{service}-mcp/ +└── app-ui/ + ├── dashboard.html + ├── contact-grid.html + ├── contact-card.html + ├── contact-creator.html + ├── calendar-view.html + ├── pipeline-kanban.html + ├── activity-timeline.html + ├── data-explorer.html ← Interactive Data Grid (new) + └── ... +``` + +Each file is a **single, self-contained HTML file** with all CSS and JS inline. Zero external dependencies. + +--- + +## 2. Design System — LocalBosses Dark Theme + +### Color Palette + +> **WCAG AA Compliance Note:** All text colors must maintain a minimum contrast ratio of **4.5:1** against their background for normal text (under 18px/14px bold), and **3:1** for large text. The secondary text color `#b0b2b8` achieves **5.0:1** on `#1a1d23` and **4.3:1** on `#2b2d31`, meeting AA for normal text. The previous value `#96989d` (3.7:1) failed this requirement and must not be used. + +| Token | Hex | Usage | +|-------|-----|-------| +| `--bg-primary` | `#1a1d23` | Page/body background | +| `--bg-secondary` | `#2b2d31` | Cards, panels, containers | +| `--bg-tertiary` | `#232529` | Nested elements, table rows alt | +| `--bg-hover` | `#35373c` | Hover states on interactive elements | +| `--bg-input` | `#1e2024` | Form inputs, text areas | +| `--accent` | `#ff6d5a` | Primary accent, buttons, active states | +| `--accent-hover` | `#ff8574` | Accent hover state | +| `--accent-subtle` | `rgba(255, 109, 90, 0.15)` | Accent backgrounds, badges | +| `--text-primary` | `#dcddde` | Primary text | +| `--text-secondary` | `#b0b2b8` | Muted/secondary text, labels (WCAG AA 5.0:1 on #1a1d23) | +| `--text-heading` | `#ffffff` | Headings, emphasis | +| `--border` | `#3a3c41` | Borders, dividers | +| `--success` | `#43b581` | Success states, positive metrics | +| `--warning` | `#faa61a` | Warning states, caution | +| `--danger` | `#f04747` | Error states, destructive actions | +| `--info` | `#5865f2` | Info states, links | + +### Typography + +```css +font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; +``` + +| Element | Size | Weight | Color | +|---------|------|--------|-------| +| Page title | 18px | 700 | #ffffff | +| Section heading | 14px | 600 | #ffffff | +| Body text | 13px | 400 | #dcddde | +| Small/muted | 12px | 400 | #b0b2b8 | +| Metric value | 24px | 700 | #ff6d5a | +| Table header | 11px | 600 | #b0b2b8 (uppercase, letter-spacing: 0.5px) | + +### Spacing & Layout + +| Token | Value | Usage | +|-------|-------|-------| +| `--gap-xs` | 4px | Tight spacing (icon + label) | +| `--gap-sm` | 8px | Compact spacing | +| `--gap-md` | 12px | Standard spacing | +| `--gap-lg` | 16px | Section spacing | +| `--gap-xl` | 24px | Major section breaks | +| `--radius-sm` | 4px | Small elements (badges, chips) | +| `--radius-md` | 8px | Cards, panels | +| `--radius-lg` | 12px | Large containers, modals | + +### Components + +#### Cards +```css +.card { + background: #2b2d31; + border-radius: 8px; + padding: 16px; + border: 1px solid #3a3c41; +} +``` + +#### Buttons +```css +.btn-primary { + background: #ff6d5a; + color: #ffffff; + border: none; + padding: 8px 16px; + border-radius: 6px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: background 0.15s; +} +.btn-primary:hover { background: #ff8574; } +.btn-primary:focus-visible { outline: 2px solid #ff6d5a; outline-offset: 2px; } + +.btn-secondary { + background: transparent; + color: #dcddde; + border: 1px solid #3a3c41; + padding: 8px 16px; + border-radius: 6px; + font-size: 13px; + cursor: pointer; + transition: all 0.15s; +} +.btn-secondary:hover { background: #35373c; border-color: #4a4c51; } +.btn-secondary:focus-visible { outline: 2px solid #ff6d5a; outline-offset: 2px; } +``` + +#### Status badges +```css +.badge { padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 600; } +.badge-success { background: rgba(67, 181, 129, 0.15); color: #43b581; } +.badge-warning { background: rgba(250, 166, 26, 0.15); color: #faa61a; } +.badge-danger { background: rgba(240, 71, 71, 0.15); color: #f04747; } +.badge-info { background: rgba(88, 101, 242, 0.15); color: #5865f2; } +.badge-accent { background: rgba(255, 109, 90, 0.15); color: #ff6d5a; } +.badge-neutral { background: rgba(176, 178, 184, 0.15); color: #b0b2b8; } +``` + +--- + +## 3. Data Visualization Primitives + +All visualizations use pure CSS/SVG — zero external dependencies. Copy these snippets into any app template. + +### 3.1 Line / Area Chart (SVG Polyline) + +```html + + + + + + + + + + + + + + + +``` + +**JS helper to generate points from data:** +```javascript +function makeLinePoints(data, width, height) { + const max = Math.max(...data.map(d => d.value), 1); + const step = width / Math.max(data.length - 1, 1); + return data.map((d, i) => `${i * step},${height - (d.value / max) * (height - 10)}`).join(' '); +} +// Usage: +``` + +### 3.2 Donut / Pie Chart (SVG Circle) + +```html + + + + + + + + + + 72% + +``` + +**JS helper for multi-segment donut:** +```javascript +function makeDonutSegments(segments, radius) { + const circumference = 2 * Math.PI * radius; + let offset = 25; // Start from top (25% offset = 12 o'clock) + return segments.map(seg => { + const dashArray = `${seg.percent} ${100 - seg.percent}`; + const html = ``; + offset -= seg.percent; + return html; + }).join(''); +} +``` + +### 3.3 Sparklines (Inline SVG) + +```html + + + + + + + + + +``` + +### 3.4 Progress Bars (CSS-Only) + +```html + +
+
+
+ + +
+ Conversion +
+
+
+ 45% +
+``` + +### 3.5 Horizontal Bar Charts (CSS Flexbox) + +```html + +
+
+ Email +
+
+ 82% +
+
+
+
+ Social +
+
+ 54% +
+
+
+
+ Direct +
+
+ 31% +
+
+
+
+``` + +**JS helper for horizontal bars from data:** +```javascript +function renderHorizontalBars(items, colorFn) { + const max = Math.max(...items.map(d => d.value), 1); + return items.map(d => { + const pct = Math.round((d.value / max) * 100); + const color = colorFn ? colorFn(d) : '#ff6d5a'; + return ` +
+ ${escapeHtml(d.label)} +
+
+ ${formatNumber(d.value)} +
+
+
`; + }).join(''); +} +``` + +--- + +## 4. Data Flow: How Data Gets to the App + +### Architecture + +``` +User sends message in thread + │ + ▼ +AI calls MCP tool → tool returns result + │ + ├─── structuredContent (MCP protocol) ← typed JSON data from tool + └─── content (text fallback) ← human-readable text + │ + ▼ +AI generates response + APP_DATA block + │ + ▼ + + │ + ▼ +LocalBosses chat/route.ts parses APP_DATA + │ + ▼ +Stores in app-data endpoint & sends via postMessage + │ + ▼ +iframe receives data → app renders +``` + +### MCP `structuredContent` Context + +> **Important distinction:** The `APP_DATA` block format (``) is a **LocalBosses-specific** pattern for passing structured data from the AI's text response to the app iframe. It is NOT part of the MCP protocol. +> +> In the MCP protocol (spec 2025-06-18+), tools return typed data via `structuredContent` alongside a text fallback in `content`. The flow is: +> +> 1. **MCP tool** returns `{ content: [...], structuredContent: { data: [...], meta: {...} } }` +> 2. **LocalBosses** receives the tool result — the `structuredContent` is the typed data +> 3. **AI** uses `structuredContent` to generate the `APP_DATA` block in its response text +> 4. **LocalBosses route.ts** parses `APP_DATA` from the AI's response and sends it to the iframe +> +> The app itself doesn't interact with MCP directly — it receives data via `postMessage` or polling, regardless of whether the data originally came from `structuredContent` or was generated by the AI. The apps are a pure rendering layer. + +### Two data reception methods (apps MUST support both): + +1. **postMessage** — Primary. Host sends data to iframe. +2. **Polling** — Fallback. App fetches from `/api/app-data` with exponential backoff. + +--- + +## 5. The HTML App Template + +This is the EXACT base template for every app. Copy and customize. + +```html + + + + + + + {App Name} + + + +
+ +
+ Loading content, please wait… +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + +
+ + + + +``` + +--- + +## 6. App Type Templates + +### 6.1 Dashboard + +**Use when:** Aggregate KPIs, overview metrics, recent activity summary. + +**Expected data shape:** `{ title?, timeFrame?, metrics: { [key]: number }, recent?: { title, description?, date }[] }` + +**Empty state:** "Ask me for a performance overview, KPIs, or a metrics summary." + +```javascript +function render(data) { + showState('data'); + const el = document.getElementById('content'); + + // Validate expected shape + validateData(data, ['metrics']); + + const metrics = data.metrics || {}; + const recentItems = Array.isArray(data.recent) ? data.recent : []; + + el.innerHTML = ` +
+
+
${escapeHtml(data.title || '{Service} Dashboard')}
+
${escapeHtml(data.timeFrame || 'Last 30 days')}
+
+
+ +
+ ${Object.entries(metrics).map(([key, val]) => ` +
+
${escapeHtml(key.replace(/_/g, ' '))}
+
${typeof val === 'number' && key.includes('revenue') ? formatCurrency(val) : formatNumber(val)}
+
+ `).join('')} +
+ + ${recentItems.length > 0 ? ` +
+
Recent Activity
+ ${recentItems.slice(0, 10).map((item, i) => ` +
+
+
${escapeHtml(item.title || item.name || '—')}
+
${escapeHtml(item.description || item.type || '')}
+
+
${formatDateTime(item.date || item.createdAt)}
+
+ `).join('')} +
+ ` : ''} + `; + + // Animate metric numbers + el.querySelectorAll('.metric-value[data-count]').forEach(el => { + const target = parseFloat(el.dataset.count); + if (!isNaN(target)) { + const isCurrency = el.textContent.startsWith('$'); + animateCount(el, target, 600, isCurrency ? formatCurrency : formatNumber); + } + }); +} +``` + +**Dashboard empty state customization:** +```html + +``` + +### 6.2 Data Grid + +**Use when:** Searchable/filterable lists, table views. + +**Expected data shape:** `{ title?, data|items|contacts|results: object[], meta?: { total, page, pageSize } }` + +**Empty state:** "Try 'show me all active contacts' or 'list recent invoices.'" + +```javascript +function render(data) { + showState('data'); + const el = document.getElementById('content'); + + const items = Array.isArray(data) ? data : (data.data || data.items || data.contacts || data.results || []); + const total = data.meta?.total || data.total || items.length; + + // Validate + if (!Array.isArray(items)) { + console.warn('[DataGrid] Expected array for items, got:', typeof items); + } + + // Auto-detect columns from first item + const columns = items.length > 0 + ? Object.keys(items[0]).filter(k => !['id', '_id', '__v'].includes(k)).slice(0, 6) + : []; + + el.innerHTML = ` +
+
+
${escapeHtml(data.title || 'Results')}
+
${total} record${total !== 1 ? 's' : ''}
+
+
+ +
+ + + ${columns.map(col => ``).join('')} + + + ${items.map((item, i) => ` + + ${columns.map(col => { + const val = item[col]; + if (col === 'status' || col === 'state') { + return ``; + } + if (typeof val === 'number' && (col.includes('amount') || col.includes('revenue') || col.includes('price'))) { + return ``; + } + if (typeof val === 'string' && val.match(/^\d{4}-\d{2}-\d{2}/)) { + return ``; + } + return ``; + }).join('')} + + `).join('')} + +
${escapeHtml(col.replace(/_/g, ' '))}
Status: ${escapeHtml(String(val || '—'))}${formatCurrency(val)}${formatDate(val)}${escapeHtml(String(val ?? '—'))}
+
+ `; +} +``` + +**Data Grid empty state customization:** +```html + +``` + +### 6.3 Detail Card + +**Use when:** Single entity deep-dive (contact, invoice, appointment). + +**Expected data shape:** `{ data|contact|item: { name?, title?, email?, status?, ...fields } }` + +**Empty state:** "Ask about a specific record by name or ID to see its details." + +```javascript +function render(data) { + showState('data'); + const el = document.getElementById('content'); + + // Flatten data — support nested formats + const item = data.data || data.contact || data.item || data; + const fields = Object.entries(item).filter(([k]) => !['id', '_id', '__v'].includes(k)); + + // Validate + validateData(item, ['name']); + + el.innerHTML = ` +
+
+
${escapeHtml(item.name || item.title || 'Details')}
+
${escapeHtml(item.email || item.type || item.status || '')}
+
+ ${item.status ? `Status: ${escapeHtml(item.status)}` : ''} +
+ +
+ ${fields.map(([key, val], i) => { + if (val == null || val === '') return ''; + if (typeof val === 'object') val = JSON.stringify(val); + return ` +
+ ${escapeHtml(key.replace(/_/g, ' '))} + ${escapeHtml(String(val))} +
+ `; + }).join('')} +
+ `; +} +``` + +**Detail Card empty state customization:** +```html + +``` + +### 6.4 Form / Wizard + +**Use when:** Multi-step creation or edit flows. + +**Expected data shape:** `{ title?, description?, fields: { name, label?, type?, required?, placeholder?, options?: {value, label}[] }[] }` + +**Empty state:** "Tell me what you'd like to create and I'll set up the form." + +```javascript +function render(data) { + showState('data'); + const el = document.getElementById('content'); + + // Validate + validateData(data, ['fields']); + + const fields = data.fields || []; + const title = data.title || 'Create New'; + + el.innerHTML = ` +
+
+
${escapeHtml(title)}
+
${escapeHtml(data.description || 'Fill in the details below')}
+
+
+ +
+
+ ${fields.map((field, i) => ` +
+ + ${field.type === 'select' ? ` + + ` : field.type === 'textarea' ? ` + + ` : ` + + `} +
+ `).join('')} + +
+
+ `; +} + +// Form submit handler — collects values, validates required fields, sends to host +function submitForm() { + const form = document.getElementById('appForm'); + if (!form) return; + const formData = {}; + const fields = form.querySelectorAll('input, select, textarea'); + + // Reset field borders + fields.forEach(f => { f.style.borderColor = '#3a3c41'; }); + + // Collect values + fields.forEach(field => { + if (field.name) formData[field.name] = field.value; + }); + + // Validate required fields + const missing = [...fields].filter(f => f.required && !f.value); + if (missing.length > 0) { + missing.forEach(f => { f.style.borderColor = '#f04747'; }); + missing[0].focus(); + return; + } + + // Send to host for tool execution + sendToHost('tool_call', { + tool: 'create_' + APP_ID.split('-').pop(), + args: formData + }); + + // Show confirmation state + showState('empty'); + document.querySelector('#empty .empty-state-icon').textContent = '✅'; + document.querySelector('#empty .empty-state-title').textContent = 'Submitted!'; + document.querySelector('#empty .empty-state-text').textContent = 'Your request has been sent. Check the chat for confirmation.'; +} +``` + +**Form empty state customization:** +```html + +``` + +### 6.5 Timeline + +**Use when:** Chronological events, activity feeds, audit logs. + +**Expected data shape:** `{ title?, events|activities|timeline: { title, description?, date|timestamp, user|actor? }[] }` + +**Empty state:** "Ask to see recent activity, event history, or an audit log." + +```javascript +function render(data) { + showState('data'); + const el = document.getElementById('content'); + + const events = Array.isArray(data) ? data : (data.events || data.activities || data.timeline || []); + + // Validate + if (events.length > 0) validateData(events[0], ['title']); + + el.innerHTML = ` +
+
+
${escapeHtml(data.title || 'Activity Timeline')}
+
${events.length} event${events.length !== 1 ? 's' : ''}
+
+
+ +
+ + ${events.map((event, i) => ` +
+ +
+
+
+
${escapeHtml(event.title || event.type || event.action || '—')}
+
${escapeHtml(event.description || event.details || '')}
+
+
${formatDateTime(event.date || event.timestamp || event.createdAt)}
+
+ ${event.user || event.actor ? `
by ${escapeHtml(event.user || event.actor)}
` : ''} +
+
+ `).join('')} +
+ `; +} +``` + +**Timeline empty state customization:** +```html + +``` + +### 6.6 Funnel / Pipeline + +**Use when:** Stage-based progression (sales pipeline, deal stages). + +**Expected data shape:** `{ title?, stages|pipeline: { name|title, items|deals: { name|title, value|amount?, contact|company? }[] }[] }` + +**Empty state:** "Ask to see your sales pipeline or a specific deal stage." + +```javascript +function render(data) { + showState('data'); + const el = document.getElementById('content'); + + const stages = Array.isArray(data) ? data : (data.stages || data.pipeline || []); + + // Validate + if (stages.length > 0) validateData(stages[0], ['name']); + + el.innerHTML = ` +
+
+
${escapeHtml(data.title || 'Pipeline')}
+
${escapeHtml(data.subtitle || '')}
+
+
+ +
+ ${stages.map((stage, i) => { + const items = stage.items || stage.deals || stage.opportunities || []; + return ` +
+
+ ${escapeHtml(stage.name || stage.title)} + ${items.length} +
+
+ ${items.map((item, j) => ` +
+
${escapeHtml(item.name || item.title)}
+ ${item.value || item.amount ? `
${formatCurrency(item.value || item.amount)}
` : ''} + ${item.contact || item.company ? `
${escapeHtml(item.contact || item.company)}
` : ''} +
+ `).join('')} + ${items.length === 0 ? '
No items
' : ''} +
+
+ `; + }).join('')} +
+ `; +} +``` + +**Pipeline empty state customization:** +```html + +``` + +### 6.7 Calendar + +**Use when:** Date-based data (appointments, events, schedules). + +**Expected data shape:** `{ title?, events|appointments: { title|name, date|start|startTime, description?, location?, attendee|contact?, status? }[] }` + +**Empty state:** "Ask to see upcoming appointments, scheduled events, or your calendar." + +```javascript +function render(data) { + showState('data'); + const el = document.getElementById('content'); + + const events = Array.isArray(data) ? data : (data.events || data.appointments || []); + const today = new Date(); + + // Validate + if (events.length > 0) validateData(events[0], ['title']); + + // Group events by date + const byDate = {}; + events.forEach(evt => { + const dateStr = new Date(evt.date || evt.start || evt.startTime).toISOString().split('T')[0]; + if (!byDate[dateStr]) byDate[dateStr] = []; + byDate[dateStr].push(evt); + }); + + const sortedDates = Object.keys(byDate).sort(); + + el.innerHTML = ` +
+
+
${escapeHtml(data.title || 'Calendar')}
+
${events.length} event${events.length !== 1 ? 's' : ''}
+
+
+ +
+ ${sortedDates.map(dateStr => { + const d = new Date(dateStr + 'T12:00:00'); + const isToday = dateStr === today.toISOString().split('T')[0]; + return ` +
+
+ ${isToday ? '📍 Today — ' : ''}${d.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })} +
+ ${byDate[dateStr].map((evt, i) => ` +
+
+ ${formatTime(evt.start || evt.startTime || evt.date)} +
+
+
${escapeHtml(evt.title || evt.name || '—')}
+ ${evt.description || evt.location ? `
${escapeHtml(evt.description || evt.location || '')}
` : ''} + ${evt.attendee || evt.contact ? `
👤 ${escapeHtml(evt.attendee || evt.contact)}
` : ''} +
+ ${evt.status ? `Status: ${escapeHtml(evt.status)}` : ''} +
+ `).join('')} +
+ `; + }).join('')} +
+ `; +} + +function formatTime(dateStr) { + if (!dateStr) return ''; + try { + return new Date(dateStr).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }); + } catch { return ''; } +} +``` + +**Calendar empty state customization:** +```html + +``` + +### 6.8 Analytics / Chart + +**Use when:** Data visualization, trends, comparisons. Pure CSS charts (no external libs). + +**Expected data shape:** `{ title?, subtitle|timeFrame?, metrics?: { [key]: number }, chart|series: { label|name, value|count }[], chartTitle? }` + +**Empty state:** "Ask for analytics, performance trends, or a breakdown of your data." + +```javascript +function render(data) { + showState('data'); + const el = document.getElementById('content'); + + // Validate + validateData(data, ['chart']); + + const chartData = data.chart || data.series || []; + const maxVal = Math.max(...chartData.map(d => d.value || d.count || 0), 1); + + el.innerHTML = ` +
+
+
${escapeHtml(data.title || 'Analytics')}
+
${escapeHtml(data.subtitle || data.timeFrame || '')}
+
+
+ + ${data.metrics ? ` +
+ ${Object.entries(data.metrics).map(([key, val]) => ` +
+
${escapeHtml(key.replace(/_/g, ' '))}
+
${formatNumber(val)}
+
+ `).join('')} +
+ ` : ''} + +
+
${escapeHtml(data.chartTitle || 'Overview')}
+
+ ${chartData.map((d, i) => { + const pct = ((d.value || d.count || 0) / maxVal) * 100; + return ` +
+
${formatNumber(d.value || d.count)}
+
+
${escapeHtml(d.label || d.name || '')}
+
+ `; + }).join('')} +
+
+ `; + + // Animate metric numbers + el.querySelectorAll('.metric-value[data-count]').forEach(el => { + const target = parseFloat(el.dataset.count); + if (!isNaN(target)) animateCount(el, target); + }); +} +``` + +**Analytics empty state customization:** +```html + +``` + +### 6.9 Interactive Data Grid + +**Use when:** Data tables that need client-side sorting, filtering, searching, copy-to-clipboard, expand/collapse, or bulk selection. Use this instead of the basic Data Grid (6.2) when users need to interact with the data beyond reading it. + +**Expected data shape:** `{ title?, data|items: object[], columns?: { key, label, sortable?, copyable? }[], meta?: { total } }` + +**Empty state:** "Try 'show me all contacts' or 'list invoices from this month.'" + +This template includes all 5 interactive patterns. Include only the patterns your app needs. + +```html + + +``` + +```javascript +// ═══ Interactive Data Grid — Full Implementation ═══ + +let gridState = { + items: [], + filteredItems: [], + sortCol: null, + sortDir: 'asc', + searchQuery: '', + selectedIds: new Set(), + expandedIds: new Set() +}; + +function render(data) { + showState('data'); + const el = document.getElementById('content'); + + // Parse items from various data shapes + const rawItems = Array.isArray(data) ? data : (data.data || data.items || data.contacts || data.results || []); + gridState.items = rawItems.map((item, i) => ({ ...item, _idx: i, _id: item.id || item._id || `row-${i}` })); + gridState.filteredItems = [...gridState.items]; + + // Auto-detect columns (or use provided columns config) + const columnConfig = data.columns || (rawItems.length > 0 + ? Object.keys(rawItems[0]) + .filter(k => !['id', '_id', '__v', '_idx'].includes(k)) + .slice(0, 6) + .map(k => ({ key: k, label: k.replace(/_/g, ' '), sortable: true, copyable: k === 'email' || k === 'id' })) + : []); + + const total = data.meta?.total || data.total || rawItems.length; + + el.innerHTML = ` +
+
+
${escapeHtml(data.title || 'Data Explorer')}
+
${total} record${total !== 1 ? 's' : ''}
+
+ +
+ + +
+ +
+ + + + + +
+ + + + + ${columnConfig.map(col => ` + + `).join('')} + + + + + +
+ ${escapeHtml(col.label)} + Expand
+
+ `; + + // Store column config for re-renders + gridState.columns = columnConfig; + renderRows(); +} + +function renderRows() { + const tbody = document.getElementById('grid-body'); + if (!tbody) return; + + const items = gridState.filteredItems; + const cols = gridState.columns; + + tbody.innerHTML = items.map((item, i) => { + const isSelected = gridState.selectedIds.has(item._id); + const isExpanded = gridState.expandedIds.has(item._id); + + return ` + + + ${cols.map(col => { + const val = item[col.key]; + let cellContent; + + if (col.key === 'status' || col.key === 'state') { + cellContent = `Status: ${escapeHtml(String(val || '—'))}`; + } else if (col.copyable) { + cellContent = `${escapeHtml(String(val ?? '—'))}`; + } else if (typeof val === 'number' && (col.key.includes('amount') || col.key.includes('revenue') || col.key.includes('price'))) { + cellContent = formatCurrency(val); + } else if (typeof val === 'string' && val.match(/^\d{4}-\d{2}-\d{2}/)) { + cellContent = formatDate(val); + } else { + cellContent = escapeHtml(String(val ?? '—')); + } + + return `${cellContent}`; + }).join('')} + + + + + + +
+ ${Object.entries(item).filter(([k]) => !k.startsWith('_')).map(([k, v]) => ` +
+ ${escapeHtml(k.replace(/_/g, ' '))}
+ ${escapeHtml(String(v ?? '—'))} +
+ `).join('')} +
+ + + `; + }).join(''); + + // Update count + const countEl = document.getElementById('grid-count'); + if (countEl) countEl.textContent = items.length; +} + +// ── Apply Sort (without toggling direction) ── +// Extracted so handleSearch can re-apply the current sort without side effects +function applySort() { + const colKey = gridState.sortCol; + if (!colKey) return; + gridState.filteredItems.sort((a, b) => { + let aVal = a[colKey], bVal = b[colKey]; + if (aVal == null) return 1; + if (bVal == null) return -1; + if (typeof aVal === 'number' && typeof bVal === 'number') { + return gridState.sortDir === 'asc' ? aVal - bVal : bVal - aVal; + } + aVal = String(aVal).toLowerCase(); + bVal = String(bVal).toLowerCase(); + const cmp = aVal.localeCompare(bVal); + return gridState.sortDir === 'asc' ? cmp : -cmp; + }); +} + +// ── Sorting (user clicks column header) ── +function handleSort(colKey) { + if (gridState.sortCol === colKey) { + gridState.sortDir = gridState.sortDir === 'asc' ? 'desc' : 'asc'; + } else { + gridState.sortCol = colKey; + gridState.sortDir = 'asc'; + } + + // Update header classes + document.querySelectorAll('.sortable').forEach(th => th.classList.remove('asc', 'desc')); + const activeHeader = document.getElementById(`col-${colKey}`); + if (activeHeader) activeHeader.classList.add(gridState.sortDir); + + applySort(); + renderRows(); +} + +// ── Filtering / Search ── +function handleSearch(query) { + gridState.searchQuery = query.toLowerCase().trim(); + if (!gridState.searchQuery) { + gridState.filteredItems = [...gridState.items]; + } else { + gridState.filteredItems = gridState.items.filter(item => + Object.values(item).some(v => + v != null && String(v).toLowerCase().includes(gridState.searchQuery) + ) + ); + } + // Re-apply current sort without toggling direction + if (gridState.sortCol) { + applySort(); + } + renderRows(); +} + +// ── Bulk Selection ── +function toggleSelect(id, checked) { + if (checked) { + gridState.selectedIds.add(id); + } else { + gridState.selectedIds.delete(id); + } + updateBulkBar(); +} + +function toggleSelectAll(checked) { + if (checked) { + gridState.filteredItems.forEach(item => gridState.selectedIds.add(item._id)); + } else { + gridState.selectedIds.clear(); + } + // Update all checkboxes + document.querySelectorAll('#grid-body .grid-check').forEach(cb => cb.checked = checked); + updateBulkBar(); +} + +function clearSelection() { + gridState.selectedIds.clear(); + document.querySelectorAll('.grid-check').forEach(cb => cb.checked = false); + updateBulkBar(); +} + +function updateBulkBar() { + const bar = document.getElementById('bulk-bar'); + const count = gridState.selectedIds.size; + if (bar) { + bar.style.display = count > 0 ? 'flex' : 'none'; + document.getElementById('bulk-count').textContent = count; + } +} + +function handleBulkAction(action) { + const selectedItems = gridState.items.filter(item => gridState.selectedIds.has(item._id)); + sendToHost('tool_call', { action, items: selectedItems.map(i => ({ ...i, _idx: undefined, _id: undefined })) }); +} + +// ── Expand/Collapse ── +function toggleExpand(id) { + if (gridState.expandedIds.has(id)) { + gridState.expandedIds.delete(id); + } else { + gridState.expandedIds.add(id); + } + const detailRow = document.getElementById(`detail-${id}`); + const icon = document.querySelector(`tr[data-id="${id}"] .expand-icon`); + if (detailRow) detailRow.classList.toggle('open'); + if (icon) { + icon.classList.toggle('open'); + icon.setAttribute('aria-expanded', gridState.expandedIds.has(id)); + } +} +``` + +> **Performance Note (100+ rows):** For datasets over 100 rows, the full DOM render becomes slow. Two mitigation strategies: +> 1. **Client-side pagination:** Render 50 rows at a time with prev/next controls. All data is already loaded — just slice the array. +> 2. **Virtual scrolling:** Only render visible rows + a buffer zone (±10 rows). Recalculate on scroll. More complex but handles 10K+ rows. +> +> For most MCP apps, client-side pagination is sufficient. The tool's `meta.pageSize` already limits server-side results to 25-50 rows. + +**Interactive Data Grid empty state customization:** +```html + +``` + +--- + +## 7. Bidirectional Communication Patterns + +Apps can send actions back to the LocalBosses host using `sendToHost()`. The host listens for `mcp_app_action` messages on the iframe's parent window. + +### Pattern 1: Request Data Refresh + +```javascript +// User clicks a "Refresh" button in the app +document.getElementById('refreshBtn').addEventListener('click', () => { + sendToHost('refresh', {}); + showState('loading'); // Show loading while refresh happens +}); +``` + +### Pattern 2: Navigate to Another App (Drill-Down) + +```javascript +// User clicks a contact name → open their detail card +function openContact(contactId, contactName) { + sendToHost('navigate', { + app: 'contact-card', + params: { id: contactId, name: contactName } + }); +} + +// In a table row: +// ${escapeHtml(item.name)} +``` + +> **App-to-App Navigation (Drill-Down):** The `sendToHost('navigate', ...)` pattern enables interconnected apps. Example flows: +> - **Data Grid → Detail Card:** Click a contact name in the grid → host opens the contact-card app with that contact's data +> - **Dashboard → Data Grid:** Click a metric card → host opens the grid filtered to that metric +> - **Detail Card → Form:** Click "Edit" → host opens the form pre-filled with the entity's data +> +> The host must listen for `mcp_app_action` messages with `action: 'navigate'` and handle the app switch (see `mcp-localbosses-integrator` Phase 4 for host-side wiring). + +### Pattern 3: Trigger a Tool Call + +```javascript +// User clicks "Delete" on a row +function deleteItem(itemId) { + if (confirm('Are you sure you want to delete this item?')) { + sendToHost('tool_call', { + tool: 'delete_contact', + args: { id: itemId } + }); + } +} +``` + +--- + +## 8. Responsive Design Requirements + +Apps must work from **280px to 800px width**. + +### Breakpoints: + +| Width | Behavior | +|-------|----------| +| 280-399px | Single column. Compact padding. Smaller fonts. Horizontal scroll for tables. | +| 400-599px | Two columns for metrics. Standard padding. | +| 600-800px | Full layout. Three+ metric columns. Tables without scroll. | + +### Required CSS: +```css +@media (max-width: 400px) { + body { padding: 12px; } + .metrics-row { grid-template-columns: repeat(2, 1fr); gap: 8px; } + .app-title { font-size: 16px; } + .data-table { font-size: 12px; } +} +@media (max-width: 300px) { + .metrics-row { grid-template-columns: 1fr; } + body { padding: 8px; } +} +``` + +### Key rules: +- Use `grid-template-columns: repeat(auto-fit, minmax(Xpx, 1fr))` for adaptive grids +- Tables get `overflow-x: auto` on the container +- Pipeline columns scroll horizontally on narrow screens +- All text uses `word-break: break-word` or `text-overflow: ellipsis` + +--- + +## 9. Three Required States + +Every app MUST implement all three: + +### 1. Loading State (visible on page load) +- Use CSS skeleton animations (shimmer effect) +- Match the layout of the data state (skeletons should look like the content) +- Default state — visible when page first loads +- Must include `role="status"` and `aria-label="Loading content"` for screen readers +- Must include `Loading content, please wait…` +- Skeleton animation respects `prefers-reduced-motion` (degrades to static background) + +### 2. Empty State (when data is null or empty) +- Center-aligned with large icon, title, and description +- **Context-specific prompt per app type** (NOT generic "Ask me a question"): + - Dashboard: "Ask me for a performance overview, KPIs, or a metrics summary." + - Data Grid: "Try 'show me all active contacts' or 'list recent invoices.'" + - Detail Card: "Ask about a specific record by name or ID to see its details." + - Form: "Tell me what you'd like to create and I'll set up the form." + - Timeline: "Ask to see recent activity, event history, or an audit trail." + - Pipeline: "Ask to see your sales pipeline or a specific deal stage." + - Calendar: "Ask to see upcoming appointments or your calendar for a date range." + - Analytics: "Ask for analytics, performance trends, or a data breakdown." + - Interactive Grid: "Try 'show me all contacts' to load data you can sort and explore." +- Friendly, not error-like + +### 3. Data State (when data is received) +- Full app rendering with `aria-live="polite"` on the content container +- Handle missing/null fields gracefully (show "—" not "undefined") +- Handle unexpected data shapes (arrays where objects expected, etc.) +- Validate data shape with `validateData()` before rendering +- Apply staggered row entrance animations where appropriate +- Focus moves to content container when data loads + +--- + +## 10. Rules & Constraints + +### MUST: +- [x] Single HTML file — all CSS/JS inline +- [x] Zero external dependencies — no CDN links, no fetch to external URLs +- [x] Dark theme matching LocalBosses palette +- [x] All three states (loading, empty, data) +- [x] Both data reception methods (postMessage + polling with exponential backoff) +- [x] HTML escaping on all user data (`escapeHtml()`) +- [x] Responsive from 280px to 800px +- [x] Graceful with missing fields (never show "undefined") +- [x] Error boundary — `window.onerror` handler, try/catch in render +- [x] WCAG AA contrast — secondary text `#b0b2b8` (5.0:1), never `#96989d` +- [x] Accessibility — ARIA attributes, keyboard navigation, focus management +- [x] Data validation — `validateData()` before rendering +- [x] Context-specific empty state prompts per app type +- [x] `prefers-reduced-motion` respected for all animations +- [x] File size under 50KB per app (ideally under 30KB) — budget enforced during QA + +### MUST NOT: +- [ ] No external CSS/JS files +- [ ] No CDN links (Chart.js, D3, etc.) +- [ ] No `