=== NEW === - studio/ — MCPEngine Studio scaffold (Next.js monorepo, build plan) - docs/FACTORY-V2.md — Factory v2 architecture doc - docs/CALENDLY_MCP_BUILD_SUMMARY.md — Calendly MCP build report === UPDATED SERVERS === - fieldedge: Added jobs-tools, UI build script, main entry update - lightspeed: Updated main + server entry points - squarespace: Added collection-browser + page-manager apps - toast: Added main + server entry points === INFRA === - infra/command-center/state.json — Updated pipeline state - infra/command-center/FACTORY-V2.md — Factory v2 operator playbook
1544 lines
60 KiB
Markdown
1544 lines
60 KiB
Markdown
# MCP LocalBosses Integrator — Phase 4: Wire Into LocalBosses
|
||
|
||
**When to use this skill:** You have a built MCP server (Phase 2) and HTML apps (Phase 3) and need to wire them into the LocalBosses Next.js app so they appear as a channel in the sidebar with working apps, threads, and AI interactions.
|
||
|
||
**What this covers:** Exact files to update, channel configuration, app registration, route mapping, system prompt engineering, APP_DATA block format, thread lifecycle integration, integration validation, and rollback strategy.
|
||
|
||
**Pipeline position:** Phase 4 of 6 → Input from Phases 2 & 3, output feeds `mcp-qa-tester` (Phase 5).
|
||
|
||
---
|
||
|
||
## 1. Inputs & Outputs
|
||
|
||
**Inputs:**
|
||
- Built MCP server in `{service}-mcp/` (from Phase 2)
|
||
- HTML app files in `{service}-mcp/app-ui/` (from Phase 3)
|
||
- `{service}-api-analysis.md` (from Phase 1 — for tool names and app IDs)
|
||
|
||
**Output:** A fully wired LocalBosses channel where:
|
||
- Channel appears in sidebar under correct category
|
||
- All apps appear in the app toolbar
|
||
- Clicking an app opens a thread with an intake question
|
||
- AI responses include APP_DATA blocks that update the visual app
|
||
- Thread lifecycle (create → interact → delete) works end-to-end
|
||
|
||
**LocalBosses app location:** `localbosses-app/` (Next.js app)
|
||
|
||
---
|
||
|
||
## 2. Files to Update (Checklist)
|
||
|
||
| # | File | Purpose |
|
||
|---|------|---------|
|
||
| 1 | `src/lib/channels.ts` | Add channel definition (sidebar entry) |
|
||
| 2 | `src/lib/appNames.ts` | Add display names + icons for all apps |
|
||
| 3 | `src/lib/app-intakes.ts` | Add intake questions for each app |
|
||
| 4 | `src/app/api/mcp-apps/route.ts` | Add app ID → filename mapping + directory |
|
||
| 5 | `src/app/api/chat/route.ts` | Add tool routing + system prompt for channel |
|
||
|
||
---
|
||
|
||
## 3. File 1: `src/lib/channels.ts`
|
||
|
||
### What it does:
|
||
Defines the channel that appears in the LocalBosses sidebar. Controls name, icon, category, description, system prompt, default app, and available apps.
|
||
|
||
### Template:
|
||
|
||
```typescript
|
||
{
|
||
id: "{service}",
|
||
name: "{service}",
|
||
icon: "🔥", // Single emoji
|
||
category: "BUSINESS OPS", // "BUSINESS OPS" | "MARKETING" | "TOOLS" | "SYSTEM"
|
||
description: "{One-line description of what this channel does}",
|
||
systemPrompt: `You are the {Service Name} Specialist for LocalBosses AI.
|
||
|
||
Your expertise:
|
||
- {Capability 1 — what the user can do}
|
||
- {Capability 2}
|
||
- {Capability 3}
|
||
- {Capability 4}
|
||
|
||
TOOL SELECTION RULES:
|
||
- SEE/BROWSE/LIST multiple items → use list_* tools
|
||
- ONE specific item by name/ID → use get_* tools
|
||
- CREATE/ADD/NEW → use create_* tools
|
||
- CHANGE/UPDATE/MODIFY → use update_* tools
|
||
- DELETE/REMOVE → use delete_* tools (always confirm first)
|
||
- STATS/METRICS/OVERVIEW → use analytics tools
|
||
|
||
Before calling any tool, briefly state which tool you're choosing and why.
|
||
|
||
MULTI-INTENT MESSAGES:
|
||
- If the user asks for multiple things in one message, address them sequentially.
|
||
- State which you're handling first and that you'll get to the others.
|
||
- Complete one action before starting the next.
|
||
|
||
CORRECTIONS:
|
||
- If the user says "actually", "wait", "no I meant", "the other one", treat this as a correction to your previous action.
|
||
- If they reference "the other one" or "that one", check previous results in the conversation and clarify if needed.
|
||
- Never repeat the same action — understand what changed.
|
||
|
||
Do NOT call tools when the user asks general questions about best practices, strategy, or how-to advice. Respond from your expertise instead.
|
||
|
||
Be concise, practical, and action-oriented. When presenting data, always include an APP_DATA block so the visual app updates.`,
|
||
defaultApp: "{service}-dashboard", // Optional: auto-opens on channel entry. Omit if no dashboard.
|
||
mcpApps: [
|
||
// List ALL app IDs registered for this channel
|
||
"{service}-dashboard",
|
||
"{service}-contact-grid",
|
||
"{service}-contact-card",
|
||
"{service}-contact-creator",
|
||
"{service}-calendar-view",
|
||
"{service}-pipeline-kanban",
|
||
"{service}-activity-timeline",
|
||
// ... all apps from Phase 3
|
||
],
|
||
},
|
||
```
|
||
|
||
### Placement:
|
||
Add the new channel object to the `channels` array. Place it in the appropriate category section.
|
||
|
||
### Real example (from automations channel):
|
||
|
||
```typescript
|
||
{
|
||
id: "automations",
|
||
name: "automations",
|
||
icon: "⚡",
|
||
category: "BUSINESS OPS",
|
||
description: "Build n8n workflows with natural language",
|
||
systemPrompt: `You are the Automations Specialist for LocalBosses AI, powered by n8n workflow automation.
|
||
|
||
Your expertise:
|
||
- Building n8n workflows from natural language descriptions
|
||
- Connecting 1,084+ integrations (apps, APIs, databases)
|
||
- Automation best practices (error handling, scheduling, data transformation)
|
||
- Workflow optimization and debugging
|
||
- Common automation patterns (lead capture, email sequences, data sync, notifications)
|
||
|
||
TOOL SELECTION RULES:
|
||
- SEE/BROWSE/LIST workflows → use list_workflows
|
||
- ONE specific workflow by ID → use get_workflow
|
||
- CREATE/ADD/NEW workflow → use create_workflow
|
||
- CHANGE/UPDATE/MODIFY → use update_workflow
|
||
- DELETE/REMOVE → use delete_workflow (always confirm first)
|
||
- STATS/EXECUTION HISTORY → use list_executions
|
||
|
||
Before calling any tool, briefly state which tool you're choosing and why.
|
||
|
||
Do NOT call tools when users ask about automation best practices, n8n concepts, or workflow design patterns. Respond from your expertise instead.
|
||
|
||
When users describe what they want to automate:
|
||
1. Break it down into workflow steps
|
||
2. Identify which n8n nodes to use
|
||
3. Explain the data flow
|
||
4. Suggest error handling approaches
|
||
|
||
Always be practical and implementation-focused. If a workflow would be complex, break it into phases.`,
|
||
defaultApp: "n8n-workflow-builder",
|
||
mcpApps: [
|
||
"n8n-workflow-builder",
|
||
"n8n-execution-monitor",
|
||
"n8n-workflow-templates",
|
||
"n8n-node-config",
|
||
"n8n-health-monitor",
|
||
"n8n-webhook-tester",
|
||
"n8n-workflow-detail",
|
||
],
|
||
},
|
||
```
|
||
|
||
---
|
||
|
||
## 4. File 2: `src/lib/appNames.ts`
|
||
|
||
### What it does:
|
||
Maps app IDs to human-friendly display names and emoji icons. Used by the app toolbar and anywhere apps are shown by name.
|
||
|
||
### Template:
|
||
|
||
```typescript
|
||
// In the APP_DISPLAY_NAMES object, add one entry per app:
|
||
|
||
// ═══════════════════════════════════════════
|
||
// {Service Name} Apps
|
||
// ═══════════════════════════════════════════
|
||
"{service}-dashboard": { name: "Dashboard", icon: "📊" },
|
||
"{service}-contact-grid": { name: "Contacts", icon: "👥" },
|
||
"{service}-contact-card": { name: "Contact Card", icon: "👤" },
|
||
"{service}-contact-creator": { name: "New Contact", icon: "➕" },
|
||
"{service}-calendar-view": { name: "Calendar", icon: "📆" },
|
||
"{service}-pipeline-kanban": { name: "Pipeline", icon: "📈" },
|
||
"{service}-activity-timeline": { name: "Activity", icon: "📅" },
|
||
```
|
||
|
||
### Icon guidelines:
|
||
- Use a single emoji that represents the app type
|
||
- 📊 for dashboards/analytics
|
||
- 👥 for contact lists, 👤 for single contact
|
||
- ➕ for creation forms
|
||
- 📆 for calendars
|
||
- 📈 for pipeline/funnel
|
||
- 📅 for timeline/activity
|
||
- 🔍 for search
|
||
- 📋 for lists
|
||
- 📄 for detail views
|
||
- 💰 for financial/invoice
|
||
- ⚙️ for settings/config
|
||
|
||
---
|
||
|
||
## 5. File 3: `src/lib/app-intakes.ts`
|
||
|
||
### What it does:
|
||
Defines the intake question shown when a user clicks an app. This creates a conversational thread where the AI generates data for the app.
|
||
|
||
### Interface:
|
||
|
||
```typescript
|
||
export interface AppIntake {
|
||
category: string; // Grouping category for similar apps
|
||
question: string; // The question shown to the user in the thread
|
||
skipLabel?: string; // If defined, shows a "skip" button with this label
|
||
systemPromptAddon: string; // Extra AI instructions for generating APP_DATA
|
||
}
|
||
```
|
||
|
||
### Intake Question Quality Criteria
|
||
|
||
Every intake question MUST meet these standards:
|
||
|
||
| Criterion | Requirement | Example |
|
||
|-----------|-------------|---------|
|
||
| **Input format hint** | Suggest what to provide | "Provide a name, email, or ID" |
|
||
| **skipLabel** | Most common default action | `"All upcoming events"` |
|
||
| **Length** | Under 20 words | ✓ "What contacts? Filter by name, status, or tag." |
|
||
| **Action-oriented** | Tell what to DO, not ASK | ✓ "Filter contacts by name, status, or tag" ✗ "What would you like to see?" |
|
||
| **Context-specific** | Tied to this app's data | ✓ "Which pipeline stage?" ✗ "What data do you want?" |
|
||
|
||
**Bad examples:**
|
||
- ❌ "What would you like to see?" — too vague, no format hint
|
||
- ❌ "Please tell me what you're looking for in this application" — too long, not action-oriented
|
||
- ❌ "Enter your query" — no context, no format hint
|
||
|
||
**Good examples:**
|
||
- ✅ "Filter contacts by name, status, or tag — or say 'show all'." (skipLabel: "All contacts")
|
||
- ✅ "Which date range? e.g., 'this week', 'Feb 2026', 'next 7 days'" (skipLabel: "This week")
|
||
- ✅ "Which contact? Provide a name, email, or ID."
|
||
|
||
> **Note on MCP Elicitation:** The intake question pattern maps conceptually to MCP's `elicitation/create` capability (spec 2025-06-18). In the future, intake questions could be served as MCP elicitation requests rather than hardcoded in `app-intakes.ts`, enabling servers to dynamically request user input mid-flow. This would also support mid-conversation elicitation (e.g., "Which account?" during an OAuth flow, or "Confirm delete?" for destructive operations).
|
||
|
||
### Template per app type:
|
||
|
||
#### Dashboard apps:
|
||
```typescript
|
||
"{service}-dashboard": {
|
||
category: "dashboard",
|
||
question: "What time frame? e.g., last 7 days, this month, last quarter",
|
||
skipLabel: "Last 30 days",
|
||
systemPromptAddon: `The user is viewing the {Service} Dashboard. Generate APP_DATA with these fields:
|
||
{
|
||
"title": "Service Dashboard",
|
||
"timeFrame": "Last 30 days",
|
||
"metrics": {
|
||
"total_contacts": 1234,
|
||
"active_deals": 56,
|
||
"revenue": 78900,
|
||
"appointments_today": 3
|
||
},
|
||
"recent": [
|
||
{ "title": "Event name", "description": "Details", "date": "2026-02-04T10:30:00Z", "type": "event_type" }
|
||
]
|
||
}`,
|
||
},
|
||
```
|
||
|
||
#### Data grid apps:
|
||
```typescript
|
||
"{service}-contact-grid": {
|
||
category: "data-grid",
|
||
question: "Filter contacts by name, status, or tag — or say 'show all'.",
|
||
skipLabel: "All contacts",
|
||
systemPromptAddon: `The user is viewing the contact grid. Generate APP_DATA with an array of contacts:
|
||
{
|
||
"title": "Contacts",
|
||
"data": [
|
||
{ "name": "John Smith", "email": "john@example.com", "phone": "(555) 123-4567", "status": "active", "created": "2026-01-15" }
|
||
],
|
||
"meta": { "total": 150, "page": 1, "pageSize": 25 }
|
||
}
|
||
Include 5-10 realistic records. Match any filters the user requested.`,
|
||
},
|
||
```
|
||
|
||
#### Detail card apps:
|
||
```typescript
|
||
"{service}-contact-card": {
|
||
category: "detail-card",
|
||
question: "Which contact? Provide a name, email, or ID.",
|
||
systemPromptAddon: `The user wants to view a specific contact's details. Generate APP_DATA with full contact info:
|
||
{
|
||
"name": "John Smith",
|
||
"email": "john@example.com",
|
||
"phone": "(555) 123-4567",
|
||
"status": "active",
|
||
"company": "Acme Inc",
|
||
"tags": ["vip", "lead"],
|
||
"created": "2026-01-15",
|
||
"lastActivity": "2026-02-03T14:30:00Z",
|
||
"notes": "Key decision maker"
|
||
}`,
|
||
},
|
||
```
|
||
|
||
#### Form/wizard apps:
|
||
```typescript
|
||
"{service}-contact-creator": {
|
||
category: "form",
|
||
question: "Describe the new contact — I'll pre-fill the form for you.",
|
||
systemPromptAddon: `The user wants to create a new contact. Generate APP_DATA defining the form fields:
|
||
{
|
||
"title": "Create New Contact",
|
||
"description": "Fill in the contact details",
|
||
"fields": [
|
||
{ "name": "name", "label": "Full Name", "type": "text", "required": true, "placeholder": "John Smith" },
|
||
{ "name": "email", "label": "Email", "type": "email", "required": false, "placeholder": "john@example.com" },
|
||
{ "name": "phone", "label": "Phone", "type": "tel", "required": false, "placeholder": "(555) 123-4567" },
|
||
{ "name": "status", "label": "Status", "type": "select", "options": ["active", "inactive", "lead"] }
|
||
]
|
||
}
|
||
Pre-fill any values the user mentioned.`,
|
||
},
|
||
```
|
||
|
||
#### Calendar apps:
|
||
```typescript
|
||
"{service}-calendar-view": {
|
||
category: "calendar",
|
||
question: "Which date range? e.g., this week, Feb 2026, next month",
|
||
skipLabel: "This week",
|
||
systemPromptAddon: `The user is viewing the calendar. Generate APP_DATA with events:
|
||
{
|
||
"title": "Calendar",
|
||
"events": [
|
||
{ "title": "Meeting with John", "start": "2026-02-04T10:00:00Z", "end": "2026-02-04T11:00:00Z", "contact": "John Smith", "status": "confirmed", "location": "Office" }
|
||
]
|
||
}
|
||
Include events for the requested time range.`,
|
||
},
|
||
```
|
||
|
||
#### Timeline apps:
|
||
```typescript
|
||
"{service}-activity-timeline": {
|
||
category: "timeline",
|
||
question: "Whose activity? Provide a contact name, or say 'all recent'.",
|
||
skipLabel: "All recent activity",
|
||
systemPromptAddon: `The user is viewing an activity timeline. Generate APP_DATA with events:
|
||
{
|
||
"title": "Activity Timeline",
|
||
"events": [
|
||
{ "title": "Email sent", "description": "Follow-up email to John", "date": "2026-02-04T10:30:00Z", "type": "email", "user": "Jake" },
|
||
{ "title": "Call completed", "description": "15 min call discussing proposal", "date": "2026-02-03T16:00:00Z", "type": "call", "user": "Jake" }
|
||
]
|
||
}
|
||
Order events from newest to oldest.`,
|
||
},
|
||
```
|
||
|
||
#### Pipeline/funnel apps:
|
||
```typescript
|
||
"{service}-pipeline-kanban": {
|
||
category: "pipeline",
|
||
question: "Which pipeline? e.g., 'sales pipeline', 'hiring pipeline'",
|
||
skipLabel: "Main pipeline",
|
||
systemPromptAddon: `The user is viewing the pipeline board. Generate APP_DATA with stages and deals:
|
||
{
|
||
"title": "Sales Pipeline",
|
||
"stages": [
|
||
{
|
||
"name": "New Leads",
|
||
"items": [
|
||
{ "name": "Acme Deal", "value": 25000, "contact": "John Smith" }
|
||
]
|
||
},
|
||
{
|
||
"name": "Qualified",
|
||
"items": [
|
||
{ "name": "Beta Contract", "value": 50000, "contact": "Jane Doe" }
|
||
]
|
||
},
|
||
{ "name": "Proposal", "items": [] },
|
||
{ "name": "Closed Won", "items": [] }
|
||
]
|
||
}`,
|
||
},
|
||
```
|
||
|
||
---
|
||
|
||
## 6. File 4: `src/app/api/mcp-apps/route.ts`
|
||
|
||
### What it does:
|
||
Maps app IDs to their HTML filenames and tells the server where to find the files.
|
||
|
||
### Changes needed:
|
||
|
||
#### A. Add to `APP_NAME_MAP`:
|
||
|
||
```typescript
|
||
const APP_NAME_MAP: Record<string, string> = {
|
||
// ... existing entries ...
|
||
|
||
// {Service Name} apps
|
||
"{service}-dashboard": "dashboard",
|
||
"{service}-contact-grid": "contact-grid",
|
||
"{service}-contact-card": "contact-card",
|
||
"{service}-contact-creator": "contact-creator",
|
||
"{service}-calendar-view": "calendar-view",
|
||
"{service}-pipeline-kanban": "pipeline-kanban",
|
||
"{service}-activity-timeline": "activity-timeline",
|
||
};
|
||
```
|
||
|
||
**Rule:** Left side is the app ID (used in channels.ts, appNames.ts, intakes). Right side is the HTML filename WITHOUT the `.html` extension.
|
||
|
||
#### B. Add to `APP_DIRS`:
|
||
|
||
```typescript
|
||
const APP_DIRS = [
|
||
// ... existing directories ...
|
||
|
||
// {Service Name} apps
|
||
join(process.cwd(), "../{service}-mcp/app-ui"),
|
||
// OR if using dist: join(process.cwd(), "../{service}-mcp/dist/app-ui"),
|
||
];
|
||
```
|
||
|
||
**Rule:** Order matters — first match wins. Add new directories at the bottom unless they need priority.
|
||
|
||
### How file resolution works:
|
||
```
|
||
1. User requests app ID "{service}-dashboard"
|
||
2. APP_NAME_MAP maps it to filename "dashboard"
|
||
3. For each directory in APP_DIRS:
|
||
a. Check: {dir}/dashboard.html (flat format)
|
||
b. Check: {dir}/dashboard/index.html (subdirectory format)
|
||
4. First match wins, HTML is returned
|
||
```
|
||
|
||
---
|
||
|
||
## 7. File 5: `src/app/api/chat/route.ts`
|
||
|
||
### What it does:
|
||
The chat route handles AI conversations. For app threads, it injects system prompts that tell the AI to include APP_DATA blocks in responses.
|
||
|
||
### The APP_DATA Block Format:
|
||
|
||
> **Important:** APP_DATA is a **LocalBosses-specific** convention for embedding structured data in LLM responses. It is NOT part of the MCP protocol. MCP's native equivalent is `structuredContent` on tool results (see Section 14 for the bridge roadmap).
|
||
|
||
The AI response includes a hidden block that gets parsed by the frontend and sent to the app:
|
||
|
||
```
|
||
Your visible text response here...
|
||
|
||
<!--APP_DATA:{"key":"value","data":[...]}:END_APP_DATA-->
|
||
```
|
||
|
||
**Rules:**
|
||
1. EVERY response in an app thread MUST include exactly one APP_DATA block
|
||
2. The JSON must be valid and on a SINGLE LINE (no line breaks inside)
|
||
3. Place it AFTER the text explanation
|
||
4. The block is automatically parsed and hidden from the user
|
||
5. When the user refines, generate completely NEW APP_DATA (replace, don't append)
|
||
|
||
### APP_DATA Failure Modes & Parsing Guidelines
|
||
|
||
LLMs don't always produce perfect APP_DATA. Document and handle these known failure modes:
|
||
|
||
| Failure Mode | Example | Fix |
|
||
|---|---|---|
|
||
| **Line breaks in JSON** | `<!--APP_DATA:{\n"key":"val"\n}:END_APP_DATA-->` | Strip all `\n` and `\r` before JSON.parse |
|
||
| **Wrapped in code block** | ````json\n<!--APP_DATA:...-->` `` | Strip `` ```json `` and `` ``` `` wrappers before extracting |
|
||
| **Invalid JSON** | Missing closing brace, trailing comma | Try JSON.parse, on failure try to fix common issues (trailing commas, unquoted keys) |
|
||
| **Text after END_APP_DATA** | `...END_APP_DATA--> more text here` | Only extract between delimiters; ignore trailing content |
|
||
| **No APP_DATA at all** | LLM just responds with plain text | Fallback: scan for JSON objects in the response heuristically |
|
||
| **Multiple APP_DATA blocks** | Two blocks in one response | Use the LAST block (most likely the refined one) |
|
||
|
||
#### Recommended Parser Pattern:
|
||
|
||
```typescript
|
||
function parseAppData(response: string): Record<string, unknown> | null {
|
||
// 1. Try exact match first
|
||
const exactMatch = response.match(/<!--APP_DATA:(.*?):END_APP_DATA-->/s);
|
||
if (exactMatch) {
|
||
const jsonStr = exactMatch[1].replace(/[\n\r]/g, '').trim();
|
||
try { return JSON.parse(jsonStr); } catch {}
|
||
// Try fixing common issues
|
||
try {
|
||
const fixed = jsonStr.replace(/,\s*([}\]])/g, '$1'); // trailing commas
|
||
return JSON.parse(fixed);
|
||
} catch {}
|
||
}
|
||
|
||
// 2. Try stripping code block wrappers
|
||
const stripped = response.replace(/```(?:json)?\s*/g, '').replace(/```/g, '');
|
||
const codeBlockMatch = stripped.match(/<!--APP_DATA:(.*?):END_APP_DATA-->/s);
|
||
if (codeBlockMatch) {
|
||
try { return JSON.parse(codeBlockMatch[1].replace(/[\n\r]/g, '').trim()); } catch {}
|
||
}
|
||
|
||
// 3. Heuristic fallback: find largest JSON object in response
|
||
const jsonMatches = response.match(/\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g);
|
||
if (jsonMatches) {
|
||
const largest = jsonMatches.sort((a, b) => b.length - a.length)[0];
|
||
try { return JSON.parse(largest); } catch {}
|
||
}
|
||
|
||
return null; // All parsing failed
|
||
}
|
||
```
|
||
|
||
**Track success rate:** If APP_DATA parsing fails more than 10% of the time for a channel, the system prompt needs revision — add more explicit formatting examples or stronger instructions.
|
||
|
||
### APP_DATA Schema Validation
|
||
|
||
After parsing APP_DATA, validate it against the app's expected data shape **before** sending to the iframe. This catches silent data shape mismatches (e.g., tool returns `{contacts: [...]}` but app expects `{data: [...]}`).
|
||
|
||
```typescript
|
||
// Schema contracts per app type — shared between integrator and designer
|
||
const APP_SCHEMAS: Record<string, { required: string[]; arrayFields?: string[] }> = {
|
||
'dashboard': { required: ['metrics'], arrayFields: ['recent'] },
|
||
'data-grid': { required: ['data', 'meta'], arrayFields: ['data'] },
|
||
'detail-card': { required: ['name'] },
|
||
'form': { required: ['fields'], arrayFields: ['fields'] },
|
||
'calendar': { required: ['events'], arrayFields: ['events'] },
|
||
'timeline': { required: ['events'], arrayFields: ['events'] },
|
||
'pipeline': { required: ['stages'], arrayFields: ['stages'] },
|
||
};
|
||
|
||
function validateAppData(data: Record<string, unknown>, appType: string): { valid: boolean; errors: string[] } {
|
||
const schema = APP_SCHEMAS[appType];
|
||
if (!schema) return { valid: true, errors: [] };
|
||
|
||
const errors: string[] = [];
|
||
for (const field of schema.required) {
|
||
if (!(field in data) || data[field] == null) {
|
||
errors.push(`Missing required field: "${field}"`);
|
||
}
|
||
}
|
||
for (const field of schema.arrayFields || []) {
|
||
if (field in data && !Array.isArray(data[field])) {
|
||
errors.push(`Expected array for "${field}", got ${typeof data[field]}`);
|
||
}
|
||
}
|
||
return { valid: errors.length === 0, errors };
|
||
}
|
||
```
|
||
|
||
**Usage:** Call `validateAppData()` after `parseAppData()`. If validation fails, log the errors and either attempt auto-fix (wrap non-array in array) or show a diagnostic empty state in the app.
|
||
|
||
### The Thread System Prompt (already exists in chat/route.ts):
|
||
|
||
```typescript
|
||
const THREAD_SYSTEM_PROMPT = `
|
||
|
||
## MANDATORY: APP_DATA BLOCK (DO NOT SKIP)
|
||
|
||
You are in an APP THREAD. Every response you give MUST include a hidden APP_DATA block that updates the visual app above the conversation. This is NOT optional.
|
||
|
||
FORMAT (place at the VERY END of your response):
|
||
<!--APP_DATA:{"key":"value"}:END_APP_DATA-->
|
||
|
||
RULES:
|
||
1. EVERY response MUST have exactly one APP_DATA block — no exceptions
|
||
2. The JSON must be valid and on a SINGLE LINE (no line breaks inside)
|
||
3. Place it AFTER your text explanation
|
||
4. Generate REALISTIC data matching what the user requested
|
||
5. Include 5-10 records for lists, complete details for single items
|
||
6. The block is automatically parsed and hidden from the user
|
||
7. Also write a brief natural language explanation before the block
|
||
8. When the user refines, generate completely NEW APP_DATA (replace, don't append)
|
||
|
||
If you forget the APP_DATA block, the visual app won't update and the user will see stale data. ALWAYS include it.`;
|
||
```
|
||
|
||
### What you MAY need to add to chat/route.ts:
|
||
|
||
Usually the existing THREAD_SYSTEM_PROMPT + the intake's `systemPromptAddon` is sufficient. But if your service needs special tool routing or a channel-specific system prompt override, you may need to add logic:
|
||
|
||
```typescript
|
||
// Example: If the channel has MCP server tools that need explicit routing
|
||
if (channelId === '{service}') {
|
||
// Add service-specific context to the system prompt
|
||
systemPrompt += `\n\nYou have access to the following {Service} tools:\n${toolList}`;
|
||
}
|
||
```
|
||
|
||
### For workflow-type apps (like n8n):
|
||
|
||
Use `WORKFLOW_JSON` format instead of `APP_DATA`:
|
||
```
|
||
<!--WORKFLOW_JSON:{"name":"...","nodes":[...]}:END_WORKFLOW-->
|
||
```
|
||
|
||
This is only for n8n-style workflow builders. All other apps use `APP_DATA`.
|
||
|
||
### APP_DATA Output Formatting — Required Fields Per App Type
|
||
|
||
When writing `systemPromptAddon` instructions, be explicit about exact required fields. Vague instructions produce inconsistent data:
|
||
|
||
| App Type | Required APP_DATA Fields | Notes |
|
||
|----------|--------------------------|-------|
|
||
| **Dashboard** | `title`, `timeFrame`, `metrics` (object with 3-6 key/value pairs), `recent` (array of 3-5 items with `title`, `date`, `type`) | Metrics keys should match the dashboard's render function |
|
||
| **Data Grid** | `title`, `data` (array of objects — each MUST have the same keys), `meta` (`total`, `page`, `pageSize`) | Every object in `data` must have identical field names |
|
||
| **Detail Card** | All entity fields as top-level keys (no wrapping `data` object), must include `name` or `title` | Include `status`, `created`, `lastActivity` for consistency |
|
||
| **Form** | `title`, `description`, `fields` (array with `name`, `label`, `type`, `required`) | Pre-fill values in `value` field when user provides info |
|
||
| **Calendar** | `title`, `events` (array with `title`, `start` ISO, `end` ISO, `status`) | Always use ISO 8601 dates |
|
||
| **Timeline** | `title`, `events` (array with `title`, `description`, `date` ISO, `type`) | Order newest → oldest |
|
||
| **Pipeline** | `title`, `stages` (array with `name`, `items` array — each item has `name`, `value`) | Include 4-6 stages even if some are empty |
|
||
| **Analytics** | `title`, `timeFrame`, `metrics`, `chartData` (array with `label`, `value`) | Values should be realistic percentages or counts |
|
||
|
||
---
|
||
|
||
## 7b. Host-Side Handler for App Actions (sendToHost)
|
||
|
||
The App Designer's `sendToHost()` function posts `mcp_app_action` messages to the parent window. **The host (LocalBosses) must listen for these messages** — otherwise navigate, refresh, and tool_call actions from apps are dead features.
|
||
|
||
### Implementation (in the iframe wrapper component):
|
||
|
||
```typescript
|
||
// In the component that renders the app iframe
|
||
useEffect(() => {
|
||
function handleAppAction(event: MessageEvent) {
|
||
if (event.data?.type !== 'mcp_app_action') return;
|
||
|
||
const { action, payload, appId } = event.data;
|
||
|
||
switch (action) {
|
||
case 'navigate':
|
||
// App-to-app drill-down: open a different app with params
|
||
// e.g., click contact in grid → open contact-card
|
||
openApp(payload.app, payload.params);
|
||
break;
|
||
|
||
case 'refresh':
|
||
// Re-send the last tool call to get fresh data
|
||
resendLastToolCall(appId);
|
||
break;
|
||
|
||
case 'tool_call':
|
||
// App triggered a tool call (e.g., form submit, bulk action)
|
||
// Inject as a message into the thread so the AI executes it
|
||
sendMessageToThread(
|
||
`[Action] Call ${payload.tool} with: ${JSON.stringify(payload.args)}`,
|
||
{ hidden: true } // Don't show raw JSON to user
|
||
);
|
||
break;
|
||
|
||
default:
|
||
console.warn('[Host] Unknown app action:', action);
|
||
}
|
||
}
|
||
|
||
window.addEventListener('message', handleAppAction);
|
||
return () => window.removeEventListener('message', handleAppAction);
|
||
}, []);
|
||
```
|
||
|
||
### Key behaviors:
|
||
- **`navigate`** — Opens the target app in a new thread (or switches to existing). Pass `payload.params` as initial context so the AI knows what data to fetch.
|
||
- **`refresh`** — Re-executes the last tool call for that app's thread. The AI regenerates APP_DATA with fresh data.
|
||
- **`tool_call`** — Injects a tool invocation into the thread. The AI sees the request, calls the MCP tool, and returns updated APP_DATA. Used by form submits, bulk actions, and in-app buttons.
|
||
|
||
### Sending 'user_message_sent' to apps:
|
||
|
||
When the user sends a new message in a thread, notify the app so it can show the "updating" overlay:
|
||
|
||
```typescript
|
||
// In the chat message send handler
|
||
function onUserMessageSent() {
|
||
const iframe = document.querySelector(`iframe[data-app-id="${activeAppId}"]`);
|
||
if (iframe?.contentWindow) {
|
||
iframe.contentWindow.postMessage({ type: 'user_message_sent' }, '*');
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 8. System Prompt Engineering Guidelines
|
||
|
||
The channel system prompt is the most critical piece. It determines:
|
||
- What the AI knows about the service
|
||
- When it uses tools vs just responds
|
||
- How it formats data for apps
|
||
- The tone and expertise level
|
||
|
||
### Prompt Budget Targets
|
||
|
||
Keep prompts lean. Every token in the system prompt is consumed on every single user message.
|
||
|
||
| Prompt Component | Budget Target | Why |
|
||
|---|---|---|
|
||
| Channel system prompt | **< 500 tokens** | Loaded on every message in the channel |
|
||
| systemPromptAddon (per app intake) | **< 300 tokens** | Only loaded in that app's thread |
|
||
| THREAD_SYSTEM_PROMPT (shared) | ~200 tokens (fixed) | Already written; don't expand |
|
||
| **Total per-thread context** | **< 1,000 tokens** | System prompt + addon + thread prompt |
|
||
|
||
**Measure:** Paste your system prompt into a token counter. If it exceeds the budget, cut capability descriptions to single lines and remove examples from the channel prompt (put them in the addon instead).
|
||
|
||
### Structure:
|
||
|
||
```
|
||
1. IDENTITY — "You are the {Service} Specialist for LocalBosses AI" (1 line)
|
||
2. EXPERTISE — Bullet list of capabilities (4-6 bullets, < 15 words each)
|
||
3. TOOL ROUTING — Structured decision tree (always include)
|
||
4. NEGATIVE INSTRUCTIONS — When NOT to use tools (2-3 lines)
|
||
5. MULTI-INTENT — How to handle multiple requests in one message
|
||
6. CORRECTIONS — How to handle "actually/wait/no I meant" messages
|
||
7. RATIONALE REQUIREMENT — "State which tool and why before calling"
|
||
8. BEHAVIOR — How to respond (1-2 lines)
|
||
```
|
||
|
||
### Multi-Intent Handling (ALWAYS include):
|
||
|
||
```
|
||
MULTI-INTENT MESSAGES:
|
||
- If the user asks for multiple things in one message, address them sequentially.
|
||
- State which you're handling first and that you'll get to the others.
|
||
- Complete one action before starting the next.
|
||
```
|
||
|
||
### Correction Handling (ALWAYS include):
|
||
|
||
```
|
||
CORRECTIONS:
|
||
- If the user says "actually", "wait", "no I meant", "the other one",
|
||
treat this as a correction to your previous action.
|
||
- If they reference "the other one" or "that one", check previous results
|
||
in the conversation and clarify if needed.
|
||
- Never repeat the same action — understand what changed.
|
||
```
|
||
|
||
### Tool Routing Rules (ALWAYS include in channel system prompt):
|
||
|
||
This is the single highest-impact section. Research shows structured decision trees reduce tool misrouting by ~30%.
|
||
|
||
```
|
||
TOOL SELECTION RULES:
|
||
- SEE/BROWSE/LIST multiple items → use list_* tools
|
||
- ONE specific item by name/ID → use get_* tools
|
||
- CREATE/ADD/NEW → use create_* tools
|
||
- CHANGE/UPDATE/MODIFY → use update_* tools
|
||
- DELETE/REMOVE → use delete_* tools (always confirm first)
|
||
- STATS/METRICS/OVERVIEW → use analytics tools
|
||
|
||
Before calling any tool, briefly state which tool you're choosing and why.
|
||
```
|
||
|
||
**Customize the routing rules per service.** Replace `list_*` with actual tool names when the channel has few enough tools (< 15):
|
||
|
||
```
|
||
TOOL SELECTION RULES:
|
||
- SEE/BROWSE events → use list_scheduled_events
|
||
- ONE specific event → use get_event
|
||
- CREATE new event type → use create_event_type
|
||
- CANCEL/RESCHEDULE → use cancel_event (always confirm first)
|
||
- SCHEDULING METRICS → use get_scheduling_analytics
|
||
```
|
||
|
||
### Negative Instructions (ALWAYS include):
|
||
|
||
```
|
||
Do NOT call tools when the user asks:
|
||
- General questions about best practices or strategy
|
||
- How-to advice that doesn't require their specific data
|
||
- Clarifying questions about what they want (ask them back instead)
|
||
- About features that don't exist in the system
|
||
|
||
Do NOT use list tools when the user clearly knows which specific record they want — use the get tool instead.
|
||
```
|
||
|
||
### Rationale Requirement:
|
||
|
||
Add this line to every channel system prompt:
|
||
```
|
||
Before calling any tool, briefly state which tool you're choosing and why.
|
||
```
|
||
|
||
This reduces misrouting by forcing the LLM to reason about tool selection before acting.
|
||
|
||
### Tool description in system prompts:
|
||
|
||
DON'T list raw tool names. DO describe capabilities in natural language:
|
||
|
||
```
|
||
❌ BAD:
|
||
"Tools: list_contacts, get_contact, create_contact, update_contact, delete_contact"
|
||
|
||
✅ GOOD:
|
||
"You can manage contacts — search and browse the contact list, view detailed contact profiles,
|
||
create new contacts, update existing information, and remove contacts when needed."
|
||
```
|
||
|
||
### When to use tools:
|
||
|
||
Tell the AI explicitly when to use tools vs respond from knowledge:
|
||
|
||
```
|
||
When the user asks about their specific data (contacts, deals, appointments), use the appropriate
|
||
MCP tools to fetch real data. When they ask general questions about CRM best practices or strategy,
|
||
respond from your expertise without making API calls.
|
||
```
|
||
|
||
### Few-Shot Examples (include 2-3 in systemPromptAddon):
|
||
|
||
Few-shot examples are the single most effective technique for consistent tool routing and APP_DATA generation. Include them in the `systemPromptAddon`, NOT the channel prompt (to stay within token budget).
|
||
|
||
#### Example 1: List operation → Data Grid
|
||
|
||
```
|
||
USER: "Show me all active contacts"
|
||
|
||
ASSISTANT: I'll use list_contacts to pull up your active contacts, filtered by status.
|
||
|
||
[calls list_contacts with status="active"]
|
||
|
||
Here are your active contacts. You have 23 active contacts in your system.
|
||
|
||
<!--APP_DATA:{"title":"Active Contacts","data":[{"name":"John Smith","email":"john@example.com","phone":"(555) 123-4567","status":"active","created":"2026-01-15"},{"name":"Jane Doe","email":"jane@example.com","phone":"(555) 987-6543","status":"active","created":"2026-01-20"}],"meta":{"total":23,"page":1,"pageSize":25}}:END_APP_DATA-->
|
||
```
|
||
|
||
#### Example 2: Specific item → Detail Card
|
||
|
||
```
|
||
USER: "What's John Smith's email?"
|
||
|
||
ASSISTANT: I'll use get_contact to look up John Smith's full profile.
|
||
|
||
[calls get_contact with name="John Smith"]
|
||
|
||
John Smith's email is john@example.com. Here's his full profile:
|
||
|
||
<!--APP_DATA:{"name":"John Smith","email":"john@example.com","phone":"(555) 123-4567","status":"active","company":"Acme Inc","tags":["vip"],"created":"2026-01-15","lastActivity":"2026-02-03T14:30:00Z"}:END_APP_DATA-->
|
||
```
|
||
|
||
#### Example 3: General question → NO tool call
|
||
|
||
```
|
||
USER: "What's the best way to organize my contacts?"
|
||
|
||
ASSISTANT: Great question! Here are some CRM best practices for contact organization:
|
||
|
||
1. **Use tags consistently** — Create a standard set of tags (lead, customer, vip, partner) and apply them to every contact.
|
||
2. **Keep statuses updated** — Move contacts through your pipeline stages promptly.
|
||
3. **Add notes after every interaction** — Even a one-liner helps you remember context.
|
||
4. **Regular cleanup** — Review inactive contacts quarterly and archive or re-engage.
|
||
|
||
Would you like me to pull up your current contacts so we can start organizing?
|
||
```
|
||
|
||
> **Note on MCP Prompts:** Channel system prompts could be registered as MCP Prompt resources (via the server's `prompts` capability) for discoverability and versioning. Instead of hardcoding prompts in `route.ts`, servers could expose them as `prompts/list` entries, allowing clients to discover available interaction modes and enabling prompt versioning through the MCP protocol.
|
||
|
||
> **Note on MCP Roots:** MCP Roots let clients inform servers about workspace/project scope. For LocalBosses, roots could scope which data is relevant per channel — e.g., a "CRM" root that tells the server to only expose CRM-related tools and data. This would enable dynamic tool filtering based on channel context rather than static system prompts.
|
||
|
||
---
|
||
|
||
## 9. Thread Lifecycle & State Management
|
||
|
||
### How threads work:
|
||
|
||
```
|
||
1. User clicks app in toolbar
|
||
2. App intake question appears (from app-intakes.ts)
|
||
3. User responds (or clicks "skip" if skipLabel exists)
|
||
4. AI receives: channel system prompt + THREAD_SYSTEM_PROMPT + intake systemPromptAddon + user message
|
||
5. AI generates response + APP_DATA block
|
||
6. Frontend parses APP_DATA, sends to iframe via postMessage
|
||
7. App renders the data
|
||
8. User can continue chatting to refine
|
||
9. Each AI response generates new APP_DATA (replaces old)
|
||
```
|
||
|
||
### Thread-specific behavior:
|
||
- Each thread is tied to ONE app — the app stays open above the chat
|
||
- The AI always includes APP_DATA in thread responses
|
||
- When user refines ("show me only active contacts"), AI generates NEW APP_DATA
|
||
- Thread can be closed/deleted without affecting the app or other threads
|
||
|
||
### Thread State Management
|
||
|
||
Threads use **localStorage** for persistence. Be aware of these operational constraints:
|
||
|
||
| Concern | Details | Mitigation |
|
||
|---------|---------|------------|
|
||
| **Storage mechanism** | `localStorage` in the browser — key-value, synchronous, per-origin | Thread data is JSON-serialized per thread ID |
|
||
| **Persistence** | Survives page reload and browser restart. Cleared on cache clear or incognito close. | Not a permanent store — don't rely on it for critical data |
|
||
| **Expiry / Cleanup** | No automatic expiry. Old threads accumulate indefinitely. | Implement cleanup: delete threads older than 30 days on app load |
|
||
| **Max thread count** | No hard limit, but performance degrades with 100+ threads in localStorage | Warn or auto-archive after 50 threads per channel. Archive = move to a compressed summary. |
|
||
| **Storage quota** | ~5-10 MB per origin (browser-dependent). Each thread with APP_DATA ≈ 2-20 KB. | At 5 MB limit: ~250-2,500 threads before quota exceeded. Handle `QuotaExceededError` gracefully. |
|
||
| **Quota exceeded handling** | `localStorage.setItem()` throws `QuotaExceededError` | Catch the error, delete oldest threads until space is available, notify user |
|
||
|
||
**Recommended cleanup pattern:**
|
||
```typescript
|
||
function cleanupOldThreads(maxAgeDays = 30, maxCount = 50) {
|
||
const threads = getAllThreads(); // from localStorage
|
||
const now = Date.now();
|
||
const cutoff = now - (maxAgeDays * 24 * 60 * 60 * 1000);
|
||
|
||
// Delete by age
|
||
threads.filter(t => t.lastActivity < cutoff).forEach(t => deleteThread(t.id));
|
||
|
||
// Delete by count (keep newest)
|
||
const remaining = getAllThreads().sort((a, b) => b.lastActivity - a.lastActivity);
|
||
if (remaining.length > maxCount) {
|
||
remaining.slice(maxCount).forEach(t => deleteThread(t.id));
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 10. Channel Configuration Rollback Strategy
|
||
|
||
Adding a channel requires editing 4 source files. If integration fails or QA reveals problems, you need a clean way to undo.
|
||
|
||
### Strategy 1: Git-Based Rollback (Recommended)
|
||
|
||
```bash
|
||
# BEFORE integration: create a checkpoint
|
||
git add -A && git commit -m "pre-integration checkpoint: {service}"
|
||
|
||
# DO the integration (edit all 4 files)
|
||
# ... edit channels.ts, appNames.ts, app-intakes.ts, route.ts ...
|
||
|
||
# TEST the integration
|
||
npm run build && npm run dev
|
||
# Run QA checks...
|
||
|
||
# IF QA PASSES:
|
||
git add -A && git commit -m "feat: add {service} channel integration"
|
||
|
||
# IF QA FAILS:
|
||
git checkout -- src/lib/channels.ts src/lib/appNames.ts src/lib/app-intakes.ts src/app/api/mcp-apps/route.ts
|
||
# Clean revert, no broken state
|
||
```
|
||
|
||
### Strategy 2: Feature-Flag Rollback
|
||
|
||
For production deployments, use a feature flag so new channels can be toggled without code changes:
|
||
|
||
```typescript
|
||
// In channels.ts:
|
||
{
|
||
id: "{service}",
|
||
name: "{service}",
|
||
enabled: process.env.ENABLE_SERVICE_CHANNEL === "true", // default: disabled
|
||
// ... rest of config
|
||
}
|
||
|
||
// Filter in sidebar rendering:
|
||
const visibleChannels = channels.filter(c => c.enabled !== false);
|
||
```
|
||
|
||
**Workflow:**
|
||
1. Integrate with `enabled: false` (or env var `ENABLE_SERVICE_CHANNEL=false`)
|
||
2. Deploy to production — channel is invisible
|
||
3. QA in production with `ENABLE_SERVICE_CHANNEL=true` in your session
|
||
4. If QA passes: set env var to `true` globally
|
||
5. If QA fails: leave disabled, fix, redeploy
|
||
|
||
### Strategy 3: Manifest-Based (Future)
|
||
|
||
Instead of editing 4 shared TypeScript files, each channel could be defined in a single JSON manifest file:
|
||
```
|
||
channels/{service}.json → contains all config (channel def, app names, intakes, route map)
|
||
```
|
||
Delete the file = remove the channel. This is the cleanest approach but requires refactoring the LocalBosses codebase.
|
||
|
||
---
|
||
|
||
## 11. Complete Example: Adding a New Service
|
||
|
||
Let's walk through adding "Calendly" as a complete example, applying all patterns from this guide:
|
||
|
||
### channels.ts:
|
||
```typescript
|
||
{
|
||
id: "calendly",
|
||
name: "calendly",
|
||
icon: "📅",
|
||
category: "BUSINESS OPS",
|
||
description: "Manage scheduling, appointments, and calendars",
|
||
systemPrompt: `You are the Scheduling Specialist for LocalBosses AI, powered by Calendly.
|
||
|
||
Your expertise:
|
||
- Managing event types and scheduling links
|
||
- Viewing and managing scheduled events
|
||
- Finding available time slots
|
||
- Scheduling analytics and insights
|
||
|
||
TOOL SELECTION RULES:
|
||
- SEE/BROWSE events → use list_scheduled_events
|
||
- ONE specific event by ID → use get_event
|
||
- VIEW event types → use list_event_types
|
||
- CANCEL/RESCHEDULE → use cancel_event (always confirm first)
|
||
- SCHEDULING METRICS → use get_scheduling_analytics
|
||
- AVAILABILITY → use get_availability
|
||
|
||
Before calling any tool, briefly state which tool you're choosing and why.
|
||
|
||
Do NOT call tools when users ask about scheduling best practices, time management tips, or general calendar advice. Respond from your expertise instead.
|
||
|
||
Be concise and action-oriented.`,
|
||
defaultApp: "calendly-dashboard",
|
||
mcpApps: [
|
||
"calendly-dashboard",
|
||
"calendly-event-grid",
|
||
"calendly-event-detail",
|
||
"calendly-calendar",
|
||
"calendly-availability",
|
||
],
|
||
},
|
||
```
|
||
|
||
### appNames.ts:
|
||
```typescript
|
||
"calendly-dashboard": { name: "Dashboard", icon: "📊" },
|
||
"calendly-event-grid": { name: "Events", icon: "📋" },
|
||
"calendly-event-detail": { name: "Event Detail", icon: "📄" },
|
||
"calendly-calendar": { name: "Calendar", icon: "📆" },
|
||
"calendly-availability": { name: "Availability", icon: "🕐" },
|
||
```
|
||
|
||
### app-intakes.ts:
|
||
```typescript
|
||
"calendly-dashboard": {
|
||
category: "dashboard",
|
||
question: "What time frame? e.g., this week, last month, Q1 2026",
|
||
skipLabel: "Last 30 days",
|
||
systemPromptAddon: `Generate APP_DATA for the Calendly dashboard.
|
||
|
||
Required fields:
|
||
- "title": descriptive (e.g., "Scheduling Dashboard — Last 30 Days")
|
||
- "timeFrame": string matching user request
|
||
- "metrics": { "total_events", "upcoming", "completed", "cancelled" }
|
||
- "recent": array of 3-5 recent events with { "title", "date" (ISO), "type" }
|
||
|
||
Example interaction:
|
||
USER: "Show me last week's stats"
|
||
→ Use get_scheduling_analytics with date range = last 7 days
|
||
→ Return APP_DATA with metrics and recent events from that period
|
||
|
||
<!--APP_DATA:{"title":"Scheduling Dashboard — Last 7 Days","timeFrame":"Last 7 days","metrics":{"total_events":12,"upcoming":3,"completed":8,"cancelled":1},"recent":[{"title":"Strategy Call","date":"2026-02-03T10:00:00Z","type":"completed"}]}:END_APP_DATA-->`,
|
||
},
|
||
"calendly-event-grid": {
|
||
category: "data-grid",
|
||
question: "Filter events by date, status, or type — or say 'all upcoming'.",
|
||
skipLabel: "All upcoming events",
|
||
systemPromptAddon: `Generate APP_DATA for the event grid.
|
||
|
||
Required fields:
|
||
- "title": descriptive (e.g., "Upcoming Events")
|
||
- "data": array of events, each with { "name", "email" (invitee), "date" (ISO), "status", "duration", "type" }
|
||
- "meta": { "total", "page", "pageSize" }
|
||
|
||
Include 5-10 realistic records matching the user's filters.`,
|
||
},
|
||
"calendly-event-detail": {
|
||
category: "detail-card",
|
||
question: "Which event? Provide a name, date, or invitee.",
|
||
systemPromptAddon: `Generate APP_DATA for a single event detail.
|
||
|
||
Required fields: "title", "name", "status", "start" (ISO), "end" (ISO), "attendee", "email", "eventType", "location", "notes"
|
||
|
||
All fields top-level (no wrapping data object).`,
|
||
},
|
||
"calendly-calendar": {
|
||
category: "calendar",
|
||
question: "Which date range? e.g., this week, February, next 14 days",
|
||
skipLabel: "This week",
|
||
systemPromptAddon: `Generate APP_DATA for the calendar view with events in the requested range.
|
||
|
||
Required fields:
|
||
- "title": descriptive
|
||
- "events": array with { "title", "start" (ISO), "end" (ISO), "contact", "status", "location" }`,
|
||
},
|
||
"calendly-availability": {
|
||
category: "form",
|
||
question: "Which schedule's availability? e.g., 'my default schedule'",
|
||
systemPromptAddon: `Generate APP_DATA with availability settings as form fields.
|
||
|
||
Required fields:
|
||
- "title", "description"
|
||
- "fields": array with { "name", "label", "type", "required" }`,
|
||
},
|
||
```
|
||
|
||
### mcp-apps/route.ts:
|
||
```typescript
|
||
// In APP_NAME_MAP:
|
||
"calendly-dashboard": "dashboard",
|
||
"calendly-event-grid": "event-grid",
|
||
"calendly-event-detail": "event-detail",
|
||
"calendly-calendar": "calendar-view",
|
||
"calendly-availability": "availability",
|
||
|
||
// In APP_DIRS:
|
||
join(process.cwd(), "../calendly-mcp/app-ui"),
|
||
```
|
||
|
||
---
|
||
|
||
## 12. Integration Validation Script
|
||
|
||
**Run this script after every integration to catch missing or orphaned entries across all 4 files.**
|
||
|
||
Save as `scripts/validate-integration.ts` and run with `npx ts-node scripts/validate-integration.ts` (or transpile and run with Node).
|
||
|
||
```typescript
|
||
#!/usr/bin/env ts-node
|
||
/**
|
||
* MCP LocalBosses Integration Validator
|
||
*
|
||
* Cross-references all 4 integration files to find:
|
||
* - Missing entries (app ID in channels.ts but not in other files)
|
||
* - Orphaned entries (app ID in appNames/intakes/route but not in any channel)
|
||
* - File resolution failures (APP_NAME_MAP entry doesn't resolve to an HTML file)
|
||
*
|
||
* Usage: npx ts-node scripts/validate-integration.ts
|
||
* or: node scripts/validate-integration.js (after compiling)
|
||
*
|
||
* Exit code: 0 = all good, 1 = errors found
|
||
*/
|
||
|
||
import * as fs from "fs";
|
||
import * as path from "path";
|
||
|
||
// ─── Configuration ───────────────────────────────────────────
|
||
const BASE_DIR = path.resolve(__dirname, "../src");
|
||
const CHANNELS_FILE = path.join(BASE_DIR, "lib/channels.ts");
|
||
const APP_NAMES_FILE = path.join(BASE_DIR, "lib/appNames.ts");
|
||
const APP_INTAKES_FILE = path.join(BASE_DIR, "lib/app-intakes.ts");
|
||
const ROUTE_FILE = path.join(BASE_DIR, "app/api/mcp-apps/route.ts");
|
||
|
||
// ─── Parsers ─────────────────────────────────────────────────
|
||
|
||
function readFile(filePath: string): string {
|
||
if (!fs.existsSync(filePath)) {
|
||
console.error(`❌ File not found: ${filePath}`);
|
||
process.exit(1);
|
||
}
|
||
return fs.readFileSync(filePath, "utf-8");
|
||
}
|
||
|
||
/**
|
||
* Extract all app IDs from channels.ts mcpApps arrays.
|
||
* Looks for patterns like: mcpApps: ["app-1", "app-2", ...]
|
||
* and string literals inside those arrays.
|
||
*/
|
||
function parseChannelApps(source: string): { channelId: string; apps: string[] }[] {
|
||
const channels: { channelId: string; apps: string[] }[] = [];
|
||
|
||
// Match channel blocks with id and mcpApps
|
||
const channelBlockRegex = /\{\s*(?:[^{}]*?)id:\s*["'`]([^"'`]+)["'`][^{}]*?mcpApps:\s*\[([\s\S]*?)\]/g;
|
||
let match: RegExpExecArray | null;
|
||
|
||
while ((match = channelBlockRegex.exec(source)) !== null) {
|
||
const channelId = match[1];
|
||
const appsArrayContent = match[2];
|
||
const appIds = [...appsArrayContent.matchAll(/["'`]([^"'`]+)["'`]/g)].map((m) => m[1]);
|
||
channels.push({ channelId, apps: appIds });
|
||
}
|
||
|
||
// Fallback: if regex didn't catch structured blocks, try simpler pattern
|
||
if (channels.length === 0) {
|
||
const simpleRegex = /mcpApps:\s*\[([\s\S]*?)\]/g;
|
||
while ((match = simpleRegex.exec(source)) !== null) {
|
||
const appIds = [...match[1].matchAll(/["'`]([^"'`]+)["'`]/g)].map((m) => m[1]);
|
||
if (appIds.length > 0) {
|
||
channels.push({ channelId: "unknown", apps: appIds });
|
||
}
|
||
}
|
||
}
|
||
|
||
return channels;
|
||
}
|
||
|
||
/**
|
||
* Extract all keys from appNames.ts APP_DISPLAY_NAMES object.
|
||
* Looks for patterns like: "app-id": { name: "...", icon: "..." }
|
||
*/
|
||
function parseAppNames(source: string): string[] {
|
||
const keys: string[] = [];
|
||
const regex = /["'`]([a-z0-9][\w-]*)["'`]\s*:\s*\{\s*name\s*:/g;
|
||
let match: RegExpExecArray | null;
|
||
while ((match = regex.exec(source)) !== null) {
|
||
keys.push(match[1]);
|
||
}
|
||
return keys;
|
||
}
|
||
|
||
/**
|
||
* Extract all keys from app-intakes.ts APP_INTAKES object.
|
||
* Looks for patterns like: "app-id": { category: "...", question: "..." }
|
||
*/
|
||
function parseAppIntakes(source: string): string[] {
|
||
const keys: string[] = [];
|
||
const regex = /["'`]([a-z0-9][\w-]*)["'`]\s*:\s*\{\s*(?:category|question)\s*:/g;
|
||
let match: RegExpExecArray | null;
|
||
while ((match = regex.exec(source)) !== null) {
|
||
keys.push(match[1]);
|
||
}
|
||
return keys;
|
||
}
|
||
|
||
/**
|
||
* Extract APP_NAME_MAP keys and values, plus APP_DIRS paths.
|
||
*/
|
||
function parseRouteFile(source: string): { nameMap: Map<string, string>; dirs: string[] } {
|
||
const nameMap = new Map<string, string>();
|
||
|
||
// Extract APP_NAME_MAP entries: "app-id": "filename"
|
||
const mapRegex = /["'`]([a-z0-9][\w-]*)["'`]\s*:\s*["'`]([^"'`]+)["'`]/g;
|
||
// Only match within APP_NAME_MAP block
|
||
const mapBlockMatch = source.match(/APP_NAME_MAP[^{]*\{([\s\S]*?)\}/);
|
||
if (mapBlockMatch) {
|
||
let match: RegExpExecArray | null;
|
||
const block = mapBlockMatch[1];
|
||
while ((match = mapRegex.exec(block)) !== null) {
|
||
nameMap.set(match[1], match[2]);
|
||
}
|
||
}
|
||
|
||
// Extract APP_DIRS paths
|
||
const dirs: string[] = [];
|
||
const dirsBlockMatch = source.match(/APP_DIRS\s*=\s*\[([\s\S]*?)\]/);
|
||
if (dirsBlockMatch) {
|
||
const pathRegex = /["'`]([^"'`]+)["'`]/g;
|
||
let match: RegExpExecArray | null;
|
||
while ((match = pathRegex.exec(dirsBlockMatch[1])) !== null) {
|
||
dirs.push(match[1]);
|
||
}
|
||
// Also handle join() patterns
|
||
const joinRegex = /join\s*\([^)]*["'`]([^"'`]+)["'`]\s*\)/g;
|
||
while ((match = joinRegex.exec(dirsBlockMatch[1])) !== null) {
|
||
dirs.push(match[1]);
|
||
}
|
||
}
|
||
|
||
return { nameMap, dirs };
|
||
}
|
||
|
||
/**
|
||
* Check if an HTML file exists for a given filename in any of the app directories.
|
||
*/
|
||
function resolveHtmlFile(filename: string, dirs: string[], projectRoot: string): string | null {
|
||
for (const dir of dirs) {
|
||
const resolvedDir = dir.startsWith("/") ? dir : path.resolve(projectRoot, dir);
|
||
const flatPath = path.join(resolvedDir, `${filename}.html`);
|
||
const indexPath = path.join(resolvedDir, filename, "index.html");
|
||
|
||
if (fs.existsSync(flatPath)) return flatPath;
|
||
if (fs.existsSync(indexPath)) return indexPath;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// ─── Main Validation ─────────────────────────────────────────
|
||
|
||
function validate() {
|
||
console.log("🔍 MCP LocalBosses Integration Validator\n");
|
||
console.log("═".repeat(60));
|
||
|
||
let errors = 0;
|
||
let warnings = 0;
|
||
|
||
// 1. Parse all files
|
||
const channelsSource = readFile(CHANNELS_FILE);
|
||
const appNamesSource = readFile(APP_NAMES_FILE);
|
||
const appIntakesSource = readFile(APP_INTAKES_FILE);
|
||
const routeSource = readFile(ROUTE_FILE);
|
||
|
||
const channelData = parseChannelApps(channelsSource);
|
||
const appNameKeys = new Set(parseAppNames(appNamesSource));
|
||
const appIntakeKeys = new Set(parseAppIntakes(appIntakesSource));
|
||
const { nameMap: routeNameMap, dirs: routeDirs } = parseRouteFile(routeSource);
|
||
|
||
// Collect ALL app IDs referenced in channels
|
||
const allChannelApps = new Set<string>();
|
||
for (const channel of channelData) {
|
||
for (const app of channel.apps) {
|
||
allChannelApps.add(app);
|
||
}
|
||
}
|
||
|
||
console.log(`\n📊 Parsed Summary:`);
|
||
console.log(` Channels: ${channelData.length}`);
|
||
console.log(` Channel app references: ${allChannelApps.size}`);
|
||
console.log(` appNames entries: ${appNameKeys.size}`);
|
||
console.log(` app-intakes entries: ${appIntakeKeys.size}`);
|
||
console.log(` route APP_NAME_MAP entries: ${routeNameMap.size}`);
|
||
console.log(` route APP_DIRS: ${routeDirs.length}`);
|
||
|
||
// 2. Cross-reference: every app in channels must exist in other 3 files
|
||
console.log(`\n${"─".repeat(60)}`);
|
||
console.log(`\n🔗 Cross-Reference: Apps in channels.ts → other files\n`);
|
||
|
||
for (const channel of channelData) {
|
||
for (const appId of channel.apps) {
|
||
const inNames = appNameKeys.has(appId);
|
||
const inIntakes = appIntakeKeys.has(appId);
|
||
const inRoute = routeNameMap.has(appId);
|
||
|
||
if (!inNames || !inIntakes || !inRoute) {
|
||
const missing: string[] = [];
|
||
if (!inNames) missing.push("appNames.ts");
|
||
if (!inIntakes) missing.push("app-intakes.ts");
|
||
if (!inRoute) missing.push("route.ts");
|
||
console.log(` ❌ "${appId}" (channel: ${channel.channelId}) — MISSING from: ${missing.join(", ")}`);
|
||
errors++;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (errors === 0) {
|
||
console.log(` ✅ All channel apps found in all 3 files`);
|
||
}
|
||
|
||
// 3. Find orphaned entries (in appNames/intakes/route but not in any channel)
|
||
console.log(`\n${"─".repeat(60)}`);
|
||
console.log(`\n🗑️ Orphaned Entries (in files but not in any channel)\n`);
|
||
|
||
let orphanCount = 0;
|
||
for (const key of appNameKeys) {
|
||
if (!allChannelApps.has(key)) {
|
||
console.log(` ⚠️ "${key}" in appNames.ts but not in any channel's mcpApps`);
|
||
warnings++;
|
||
orphanCount++;
|
||
}
|
||
}
|
||
for (const key of appIntakeKeys) {
|
||
if (!allChannelApps.has(key)) {
|
||
console.log(` ⚠️ "${key}" in app-intakes.ts but not in any channel's mcpApps`);
|
||
warnings++;
|
||
orphanCount++;
|
||
}
|
||
}
|
||
for (const key of routeNameMap.keys()) {
|
||
if (!allChannelApps.has(key)) {
|
||
console.log(` ⚠️ "${key}" in route.ts APP_NAME_MAP but not in any channel's mcpApps`);
|
||
warnings++;
|
||
orphanCount++;
|
||
}
|
||
}
|
||
if (orphanCount === 0) {
|
||
console.log(` ✅ No orphaned entries`);
|
||
}
|
||
|
||
// 4. Verify HTML file resolution
|
||
console.log(`\n${"─".repeat(60)}`);
|
||
console.log(`\n📁 HTML File Resolution (APP_NAME_MAP → actual files)\n`);
|
||
|
||
const projectRoot = path.resolve(__dirname, "..");
|
||
let resolutionFailures = 0;
|
||
|
||
for (const [appId, filename] of routeNameMap.entries()) {
|
||
const resolved = resolveHtmlFile(filename, routeDirs, projectRoot);
|
||
if (!resolved) {
|
||
console.log(` ❌ "${appId}" → "${filename}.html" — NOT FOUND in any APP_DIRS`);
|
||
errors++;
|
||
resolutionFailures++;
|
||
}
|
||
}
|
||
|
||
if (resolutionFailures === 0) {
|
||
console.log(` ✅ All APP_NAME_MAP entries resolve to HTML files`);
|
||
}
|
||
|
||
// 5. Summary
|
||
console.log(`\n${"═".repeat(60)}`);
|
||
console.log(`\n📋 RESULTS: ${errors} errors, ${warnings} warnings`);
|
||
|
||
if (errors > 0) {
|
||
console.log(`\n❌ VALIDATION FAILED — fix ${errors} error(s) before deploying`);
|
||
process.exit(1);
|
||
} else if (warnings > 0) {
|
||
console.log(`\n⚠️ VALIDATION PASSED with ${warnings} warning(s) — review orphaned entries`);
|
||
process.exit(0);
|
||
} else {
|
||
console.log(`\n✅ VALIDATION PASSED — all integrations are consistent`);
|
||
process.exit(0);
|
||
}
|
||
}
|
||
|
||
validate();
|
||
```
|
||
|
||
**Run in CI:**
|
||
```bash
|
||
# Add to package.json scripts:
|
||
"validate:integration": "ts-node scripts/validate-integration.ts"
|
||
|
||
# Or without ts-node (compile first):
|
||
"validate:integration": "tsc scripts/validate-integration.ts --outDir scripts/dist && node scripts/dist/validate-integration.js"
|
||
```
|
||
|
||
**Run before every deploy and as part of Phase 5 QA.**
|
||
|
||
---
|
||
|
||
## 13. Quality Gate Checklist
|
||
|
||
Before passing to Phase 5 (QA), verify:
|
||
|
||
- [ ] **Channel appears in sidebar** — under correct category with correct icon
|
||
- [ ] **All apps appear in toolbar** — when channel is selected
|
||
- [ ] **Default app auto-opens** — if defaultApp is configured
|
||
- [ ] **Clicking each app opens a thread** — with the intake question
|
||
- [ ] **"Skip" button works** — if skipLabel is defined
|
||
- [ ] **AI generates APP_DATA** — in every thread response
|
||
- [ ] **App receives data** — visual app updates when AI responds
|
||
- [ ] **Refinement works** — asking follow-up questions generates new APP_DATA
|
||
- [ ] **System prompt is comprehensive** — includes tool routing rules, negative instructions, rationale requirement
|
||
- [ ] **System prompt is under budget** — channel prompt < 500 tokens, addons < 300 tokens each
|
||
- [ ] **No 404s for app files** — all HTML files resolve in mcp-apps route
|
||
- [ ] **No missing entries** — every app ID appears in all 4 files (channels, appNames, intakes, route)
|
||
- [ ] **Validation script passes** — `npm run validate:integration` exits with code 0
|
||
- [ ] **Intake questions meet quality criteria** — format hints, skipLabels, under 20 words, action-oriented
|
||
- [ ] **Test fixtures generated** — `test-fixtures/tool-routing.json` baseline created for QA (see below)
|
||
|
||
### Per-Service Test Fixture Generation
|
||
|
||
The integrator should generate a `test-fixtures/tool-routing.json` baseline for the QA tester (Phase 5). This file maps natural-language user messages to expected tool calls, derived from the system prompt's tool routing rules:
|
||
|
||
```json
|
||
{
|
||
"service": "{service}",
|
||
"fixtures": [
|
||
{ "message": "show me all contacts", "expectedTool": "list_contacts", "expectedArgs": {} },
|
||
{ "message": "find John Smith", "expectedTool": "get_contact", "expectedArgs": { "name": "John Smith" } },
|
||
{ "message": "add a new contact named Sarah", "expectedTool": "create_contact", "expectedArgs": { "name": "Sarah" } },
|
||
{ "message": "delete the old lead", "expectedTool": null, "expectedBehavior": "should ask for confirmation and specifics" },
|
||
{ "message": "what's the best way to organize contacts?", "expectedTool": null, "expectedBehavior": "respond from expertise, no tool call" }
|
||
]
|
||
}
|
||
```
|
||
|
||
**Generate at least 20 fixtures per service** covering: list, get, create, update, delete, analytics, no-tool-needed, ambiguous queries, and multi-intent messages. Save to `{service}-mcp/test-fixtures/tool-routing.json`. The QA tester uses these for tool routing validation.
|
||
|
||
### Cross-reference check (critical):
|
||
Every app ID must appear in ALL of these:
|
||
1. `channels.ts` — in the `mcpApps` array
|
||
2. `appNames.ts` — in `APP_DISPLAY_NAMES`
|
||
3. `app-intakes.ts` — in `APP_INTAKES`
|
||
4. `mcp-apps/route.ts` — in `APP_NAME_MAP`
|
||
|
||
Missing from any one = broken experience. **Use the validation script (Section 12) to automate this check.**
|
||
|
||
---
|
||
|
||
## 14. MCP Protocol Bridge: structuredContent → APP_DATA
|
||
|
||
> This section documents how MCP's native `structuredContent` relates to LocalBosses' APP_DATA pattern, and the roadmap for convergence.
|
||
|
||
### The Two Layers
|
||
|
||
**MCP Protocol Layer** (standard):
|
||
- MCP tools return results with `content` (text fallback) and `structuredContent` (typed JSON)
|
||
- Tools declare `outputSchema` so clients know the data shape
|
||
- This is the standard way to send typed data from tools to clients
|
||
|
||
**LocalBosses Application Layer** (custom):
|
||
- The APP_DATA block (`<!--APP_DATA:...:END_APP_DATA-->`) embeds structured data in LLM-generated text
|
||
- The frontend parses APP_DATA and routes it to the appropriate iframe app via postMessage
|
||
- This is a LocalBosses-specific convention, NOT part of the MCP protocol
|
||
|
||
### How They Connect Today
|
||
|
||
```
|
||
MCP Tool → structuredContent (typed JSON)
|
||
↓
|
||
LLM receives tool result, generates response
|
||
↓
|
||
LLM embeds data as APP_DATA block in response text
|
||
↓
|
||
LocalBosses frontend parses APP_DATA
|
||
↓
|
||
Frontend sends data to app iframe via postMessage
|
||
```
|
||
|
||
The LLM is the bridge — it receives `structuredContent` from the tool and re-serializes it as APP_DATA. This works but is lossy (the LLM may modify, truncate, or malform the data).
|
||
|
||
### Roadmap
|
||
|
||
| Phase | Approach | Status |
|
||
|-------|----------|--------|
|
||
| **Short-term (current)** | APP_DATA pattern — LLM embeds JSON in response text, frontend parses | ✅ Implemented |
|
||
| **Medium-term** | Route `structuredContent` directly to apps — bypass LLM re-serialization. When a tool returns `structuredContent`, send it directly to the appropriate app without waiting for the LLM to echo it. | 🔜 Planned |
|
||
| **Long-term** | Adopt official MCP Apps protocol (launched Jan 2026) — tools declare `_meta.ui.resourceUri`, apps communicate via JSON-RPC over postMessage, bidirectional data flow. **⚠️ This is live NOW** — Claude, ChatGPT, VS Code, and Goose all support MCP Apps today. | 🔴 Live — Adopt ASAP |
|
||
|
||
### Medium-Term Architecture
|
||
|
||
```
|
||
MCP Tool returns structuredContent
|
||
↓
|
||
LocalBosses chat route intercepts structuredContent from tool result
|
||
↓
|
||
Routes directly to app iframe via postMessage (no LLM re-serialization)
|
||
↓
|
||
LLM still generates text explanation, but data is sourced from tool result, not LLM output
|
||
```
|
||
|
||
**Benefits:** No JSON parsing failures, no data loss from LLM re-serialization, schema-validated data.
|
||
|
||
### Long-Term → NOW: MCP Apps Protocol (⚠️ Live — Adopt ASAP)
|
||
|
||
> **Urgency:** The MCP Apps extension launched January 26, 2026 and is **already supported** by Claude, ChatGPT, VS Code, and Goose. This is NOT a future consideration — it's a live standard. Our APP_DATA pattern works only in LocalBosses; MCP Apps works in ANY MCP client.
|
||
|
||
The official MCP Apps extension defines:
|
||
- `_meta.ui.resourceUri` on tools — declares which UI resource to render
|
||
- `ui://` resource URIs served by the MCP server
|
||
- `@modelcontextprotocol/ext-apps` SDK — standardized App class with `ontoolresult`, `callServerTool`, `updateModelContext`
|
||
- JSON-RPC over postMessage for bidirectional app ↔ server communication
|
||
|
||
**Migration path:**
|
||
1. Add `_meta.ui.resourceUri` to tool definitions in the server builder
|
||
2. Register app HTML files as `ui://` resources in each MCP server
|
||
3. Update app template to use `@modelcontextprotocol/ext-apps` App class for data reception
|
||
4. Maintain backward compatibility with postMessage/APP_DATA for LocalBosses during transition
|
||
|
||
**Impact:** MCP tools work in ANY MCP client (Claude, ChatGPT, VS Code) — not just LocalBosses. Massive distribution multiplier.
|
||
|
||
---
|
||
|
||
## 15. Execution Workflow
|
||
|
||
```
|
||
1. Create git checkpoint: git add -A && git commit -m "pre-integration: {service}"
|
||
2. Read {service}-api-analysis.md — get app IDs and tool groups
|
||
3. Update channels.ts — add channel definition with system prompt (include tool routing rules)
|
||
4. Update appNames.ts — add display names and icons
|
||
5. Update app-intakes.ts — add intake questions (meeting quality criteria) and systemPromptAddons
|
||
6. Update mcp-apps/route.ts — add APP_NAME_MAP entries and APP_DIRS path
|
||
7. Verify chat/route.ts — ensure THREAD_SYSTEM_PROMPT works (usually no changes needed)
|
||
8. Run validation script: npx ts-node scripts/validate-integration.ts
|
||
9. Fix any errors/warnings from validation
|
||
10. Test: build LocalBosses, open channel, click app, verify thread + data flow
|
||
11. If QA passes: git add -A && git commit -m "feat: add {service} channel integration"
|
||
12. If QA fails: git checkout -- src/lib/channels.ts src/lib/appNames.ts src/lib/app-intakes.ts src/app/api/mcp-apps/route.ts
|
||
```
|
||
|
||
**Estimated time:** 30-60 minutes per channel.
|
||
|
||
**Agent model recommendation:** Sonnet — well-defined patterns, file editing. But system prompt crafting benefits from Opus for nuanced AI instruction design.
|
||
|
||
---
|
||
|
||
*This skill is Phase 4 of the MCP Factory pipeline. It wires the server and apps into LocalBosses so everything is accessible through the UI.*
|