Skills added: - mcp-api-analyzer (43KB) — Phase 1: API analysis - mcp-server-builder (88KB) — Phase 2: Server build - mcp-server-development (31KB) — TS MCP patterns - mcp-app-designer (85KB) — Phase 3: Visual apps - mcp-apps-integration (20KB) — structuredContent UI - mcp-apps-official (48KB) — MCP Apps SDK - mcp-apps-merged (39KB) — Combined apps reference - mcp-localbosses-integrator (61KB) — Phase 4: LocalBosses wiring - mcp-qa-tester (113KB) — Phase 5: Full QA framework - mcp-deployment (17KB) — Phase 6: Production deploy - mcp-skill (exa integration) These skills are the encoded knowledge that lets agents build production-quality MCP servers autonomously through the pipeline.
773 lines
20 KiB
Markdown
773 lines
20 KiB
Markdown
# MCP Apps Integration — Building Servers with Rich UI
|
|
|
|
**When to use this skill:** Adding rich UI components (structuredContent) to MCP servers. Use when tool results benefit from visual presentation beyond plain text/JSON.
|
|
|
|
**What this covers:** Integrating MCP Apps with server tools, based on 11 production GHL apps (Contact Grid, Pipeline Board, Calendar View, Invoice Preview, etc.).
|
|
|
|
---
|
|
|
|
## 1. What Are MCP Apps?
|
|
|
|
**MCP Apps = Tools that return `structuredContent`** (HTML-based UI components that render in Claude Desktop)
|
|
|
|
**Use cases:**
|
|
- **Data grids:** Contact lists, search results
|
|
- **Dashboards:** Stats, metrics, KPIs
|
|
- **Cards:** Opportunity cards, invoice previews
|
|
- **Timelines:** Activity feeds, history
|
|
- **Forms:** Quick actions embedded in UI
|
|
- **Visualizations:** Charts, graphs, calendars
|
|
|
|
**When to use apps vs regular tools:**
|
|
- ✅ Use apps: Visual data (grids, cards, timelines)
|
|
- ❌ Skip apps: Simple CRUD operations, plain JSON responses
|
|
|
|
---
|
|
|
|
## 2. Architecture Pattern
|
|
|
|
### Server + Apps Integration
|
|
```
|
|
mcp-server-myservice/
|
|
├── src/
|
|
│ ├── index.ts # Main server (or server.ts)
|
|
│ ├── clients/
|
|
│ │ └── api-client.ts # API client
|
|
│ ├── apps/
|
|
│ │ └── index.ts # Apps manager + tool definitions
|
|
│ ├── ui/
|
|
│ │ ├── contact-grid.html
|
|
│ │ ├── dashboard.html
|
|
│ │ └── ...
|
|
│ └── types/
|
|
│ └── index.ts # Shared TypeScript types
|
|
├── dist/
|
|
│ ├── index.js # Compiled server
|
|
│ ├── apps/
|
|
│ ├── app-ui/ # Compiled HTML files (copied during build)
|
|
│ └── ...
|
|
├── package.json
|
|
├── tsconfig.json
|
|
└── README.md
|
|
```
|
|
|
|
**Key points:**
|
|
- Apps manager lives in `src/apps/index.ts`
|
|
- HTML UI files live in `src/ui/` or `app-ui/`
|
|
- Compiled UI files must be accessible at runtime (copy during build)
|
|
|
|
---
|
|
|
|
## 3. Apps Manager Pattern
|
|
|
|
### Basic MCPAppsManager Class
|
|
|
|
```typescript
|
|
import { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
import { MyAPIClient } from '../clients/api-client.js';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
export interface AppToolResult {
|
|
content: Array<{ type: 'text'; text: string }>;
|
|
structuredContent?: Record<string, unknown>;
|
|
}
|
|
|
|
export interface AppResourceHandler {
|
|
uri: string;
|
|
mimeType: string;
|
|
getContent: () => string;
|
|
}
|
|
|
|
// ESM __dirname equivalent
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
function getUIBuildPath(): string {
|
|
// When compiled, this file is at dist/apps/index.js
|
|
// UI files are at dist/app-ui/
|
|
const fromDist = path.resolve(__dirname, '..', 'app-ui');
|
|
if (fs.existsSync(fromDist)) {
|
|
return fromDist;
|
|
}
|
|
// Fallback
|
|
return fromDist;
|
|
}
|
|
|
|
export class MCPAppsManager {
|
|
private apiClient: MyAPIClient;
|
|
private resourceHandlers: Map<string, AppResourceHandler> = new Map();
|
|
private uiBuildPath: string;
|
|
|
|
constructor(apiClient: MyAPIClient) {
|
|
this.apiClient = apiClient;
|
|
this.uiBuildPath = getUIBuildPath();
|
|
this.registerResourceHandlers();
|
|
}
|
|
|
|
/**
|
|
* Register all UI resource handlers
|
|
*/
|
|
private registerResourceHandlers(): void {
|
|
const resources: Array<{ uri: string; file: string }> = [
|
|
{ uri: 'ui://myservice/contact-grid', file: 'contact-grid.html' },
|
|
{ uri: 'ui://myservice/dashboard', file: 'dashboard.html' },
|
|
];
|
|
|
|
for (const resource of resources) {
|
|
this.resourceHandlers.set(resource.uri, {
|
|
uri: resource.uri,
|
|
mimeType: 'text/html;profile=mcp-app',
|
|
getContent: () => this.loadUIResource(resource.file),
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load UI resource from build directory
|
|
*/
|
|
private loadUIResource(filename: string): string {
|
|
const filePath = path.join(this.uiBuildPath, filename);
|
|
try {
|
|
return fs.readFileSync(filePath, 'utf-8');
|
|
} catch (error) {
|
|
console.error(`UI resource not found: ${filePath}`);
|
|
return this.getFallbackHTML(filename);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate fallback HTML when UI resource is not built
|
|
*/
|
|
private getFallbackHTML(filename: string): string {
|
|
const componentName = filename.replace('.html', '');
|
|
return `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>${componentName}</title>
|
|
</head>
|
|
<body>
|
|
<div style="text-align: center; padding: 20px; color: #666;">
|
|
<p>UI component "${componentName}" is loading...</p>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`.trim();
|
|
}
|
|
|
|
/**
|
|
* Get tool definitions for all app tools
|
|
*/
|
|
getToolDefinitions(): Tool[] {
|
|
return [
|
|
{
|
|
name: 'view_contact_grid',
|
|
description: 'Display contact search results in a data grid. Returns a visual UI component.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
query: { type: 'string', description: 'Search query' },
|
|
limit: { type: 'number', description: 'Max results (default: 25)' },
|
|
},
|
|
},
|
|
},
|
|
// ... more app tools
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get resource handlers (for server registration)
|
|
*/
|
|
getResourceHandlers(): Map<string, AppResourceHandler> {
|
|
return this.resourceHandlers;
|
|
}
|
|
|
|
/**
|
|
* Handle app tool calls
|
|
*/
|
|
async handleAppTool(name: string, args: Record<string, unknown>): Promise<AppToolResult> {
|
|
switch (name) {
|
|
case 'view_contact_grid':
|
|
return this.viewContactGrid(args);
|
|
|
|
default:
|
|
throw new Error(`Unknown app tool: ${name}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Example: Contact Grid App
|
|
*/
|
|
private async viewContactGrid(args: Record<string, unknown>): Promise<AppToolResult> {
|
|
const { query = '', limit = 25 } = args;
|
|
|
|
// Call API to get data
|
|
const contacts = await this.apiClient.searchContacts({ query, limit: Number(limit) });
|
|
|
|
// Return structuredContent pointing to UI resource
|
|
return {
|
|
content: [{ type: 'text', text: `Found ${contacts.length} contacts` }],
|
|
structuredContent: {
|
|
type: 'ui',
|
|
uri: 'ui://myservice/contact-grid',
|
|
data: {
|
|
contacts,
|
|
query,
|
|
timestamp: new Date().toISOString(),
|
|
},
|
|
},
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Server Integration
|
|
|
|
### In `src/index.ts` or `src/server.ts`
|
|
|
|
```typescript
|
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
import {
|
|
CallToolRequestSchema,
|
|
ListToolsRequestSchema,
|
|
ListResourcesRequestSchema,
|
|
ReadResourceRequestSchema,
|
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
import { MyAPIClient } from './clients/api-client.js';
|
|
import { MCPAppsManager } from './apps/index.js';
|
|
|
|
async function main() {
|
|
// Initialize API client
|
|
const apiClient = new MyAPIClient(process.env.API_KEY!);
|
|
|
|
// Initialize apps manager
|
|
const appsManager = new MCPAppsManager(apiClient);
|
|
|
|
// Create MCP server
|
|
const server = new Server(
|
|
{ name: 'myservice-mcp', version: '1.0.0' },
|
|
{ capabilities: { tools: {}, resources: {} } } // ✅ Enable resources
|
|
);
|
|
|
|
// List tools (regular tools + app tools)
|
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
const regularTools = [
|
|
// ... your regular tools
|
|
];
|
|
const appTools = appsManager.getToolDefinitions();
|
|
|
|
return {
|
|
tools: [...regularTools, ...appTools],
|
|
};
|
|
});
|
|
|
|
// Handle tool calls
|
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
const { name, arguments: args } = request.params;
|
|
|
|
try {
|
|
// Check if it's an app tool
|
|
const appTools = appsManager.getToolDefinitions().map(t => t.name);
|
|
if (appTools.includes(name)) {
|
|
return await appsManager.handleAppTool(name, args || {});
|
|
}
|
|
|
|
// Handle regular tools
|
|
const result = await handleRegularTool(apiClient, name, args || {});
|
|
return {
|
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
};
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
return {
|
|
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
isError: true,
|
|
};
|
|
}
|
|
});
|
|
|
|
// List resources (UI files)
|
|
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
const handlers = appsManager.getResourceHandlers();
|
|
const resources = Array.from(handlers.values()).map(h => ({
|
|
uri: h.uri,
|
|
mimeType: h.mimeType,
|
|
name: h.uri.split('/').pop() || h.uri,
|
|
}));
|
|
return { resources };
|
|
});
|
|
|
|
// Read resources (serve UI HTML)
|
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
const { uri } = request.params;
|
|
const handler = appsManager.getResourceHandlers().get(uri);
|
|
|
|
if (!handler) {
|
|
throw new Error(`Resource not found: ${uri}`);
|
|
}
|
|
|
|
return {
|
|
contents: [{
|
|
uri,
|
|
mimeType: handler.mimeType,
|
|
text: handler.getContent(),
|
|
}],
|
|
};
|
|
});
|
|
|
|
// Start server
|
|
const transport = new StdioServerTransport();
|
|
await server.connect(transport);
|
|
console.error('MyService MCP server with apps running on stdio');
|
|
}
|
|
|
|
main().catch(console.error);
|
|
```
|
|
|
|
**Key additions for apps:**
|
|
1. `capabilities: { tools: {}, resources: {} }` — Enable resources
|
|
2. `ListResourcesRequestSchema` handler — List UI files
|
|
3. `ReadResourceRequestSchema` handler — Serve UI HTML
|
|
4. Check if tool is an app tool before routing
|
|
|
|
---
|
|
|
|
## 5. HTML UI Component Template
|
|
|
|
### Example: Contact Grid (`src/ui/contact-grid.html`)
|
|
|
|
```html
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Contact Grid</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
padding: 20px;
|
|
background: #f5f5f5;
|
|
}
|
|
.grid-container {
|
|
background: white;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
.grid-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
padding-bottom: 15px;
|
|
border-bottom: 2px solid #e0e0e0;
|
|
}
|
|
.grid-title {
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
color: #333;
|
|
}
|
|
.grid-count {
|
|
font-size: 14px;
|
|
color: #666;
|
|
}
|
|
.contacts-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
.contacts-table th {
|
|
text-align: left;
|
|
padding: 12px;
|
|
background: #f8f9fa;
|
|
color: #555;
|
|
font-weight: 600;
|
|
font-size: 13px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
.contacts-table td {
|
|
padding: 12px;
|
|
border-bottom: 1px solid #e0e0e0;
|
|
}
|
|
.contacts-table tr:hover {
|
|
background: #f8f9fa;
|
|
}
|
|
.contact-name {
|
|
font-weight: 600;
|
|
color: #2563eb;
|
|
}
|
|
.contact-email {
|
|
color: #666;
|
|
font-size: 14px;
|
|
}
|
|
.contact-status {
|
|
display: inline-block;
|
|
padding: 4px 12px;
|
|
border-radius: 12px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
}
|
|
.status-active {
|
|
background: #d1fae5;
|
|
color: #065f46;
|
|
}
|
|
.status-inactive {
|
|
background: #fee2e2;
|
|
color: #991b1b;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="grid-container">
|
|
<div class="grid-header">
|
|
<div class="grid-title">Contacts</div>
|
|
<div class="grid-count" id="count"></div>
|
|
</div>
|
|
<table class="contacts-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Email</th>
|
|
<th>Phone</th>
|
|
<th>Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="contacts-tbody">
|
|
<!-- Populated by JavaScript -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<script>
|
|
// Listen for data from MCP
|
|
window.addEventListener('message', (event) => {
|
|
if (event.data?.type === 'mcp-app-init') {
|
|
const data = event.data.data;
|
|
renderContacts(data);
|
|
}
|
|
});
|
|
|
|
function renderContacts(data) {
|
|
const { contacts, query } = data;
|
|
|
|
// Update count
|
|
document.getElementById('count').textContent =
|
|
`${contacts.length} result${contacts.length !== 1 ? 's' : ''}`;
|
|
|
|
// Render table rows
|
|
const tbody = document.getElementById('contacts-tbody');
|
|
tbody.innerHTML = contacts.map(contact => `
|
|
<tr>
|
|
<td class="contact-name">${escapeHtml(contact.name)}</td>
|
|
<td class="contact-email">${escapeHtml(contact.email || 'N/A')}</td>
|
|
<td>${escapeHtml(contact.phone || 'N/A')}</td>
|
|
<td>
|
|
<span class="contact-status status-${contact.status || 'active'}">
|
|
${escapeHtml(contact.status || 'Active')}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
**Key patterns:**
|
|
- Self-contained (all CSS/JS inline)
|
|
- `window.addEventListener('message', ...)` to receive data
|
|
- `event.data.type === 'mcp-app-init'` to detect init
|
|
- `event.data.data` contains the structuredContent.data object
|
|
- Escape HTML to prevent XSS
|
|
- Clean, modern styling
|
|
|
|
---
|
|
|
|
## 6. Common UI Patterns
|
|
|
|
### 1. Data Grid (List View)
|
|
**Use for:** Contact lists, search results, transaction history
|
|
**Components:** Table, sorting, pagination indicators
|
|
**Example apps:** Contact Grid, Pipeline Board
|
|
|
|
### 2. Card View (Detail View)
|
|
**Use for:** Single item details, opportunity cards, invoices
|
|
**Components:** Card container, labeled fields, actions
|
|
**Example apps:** Opportunity Card, Invoice Preview
|
|
|
|
### 3. Dashboard (Stats/Metrics)
|
|
**Use for:** Analytics, KPIs, performance metrics
|
|
**Components:** Stat cards, charts (use Chart.js), progress bars
|
|
**Example apps:** Campaign Stats, Agent Stats
|
|
|
|
### 4. Timeline (Activity Feed)
|
|
**Use for:** History, activity logs, event streams
|
|
**Components:** Timeline with timestamps, event types, icons
|
|
**Example apps:** Contact Timeline, Workflow Status
|
|
|
|
### 5. Calendar View
|
|
**Use for:** Appointments, events, schedules
|
|
**Components:** Calendar grid, event markers, time slots
|
|
**Example apps:** Calendar View
|
|
|
|
---
|
|
|
|
## 7. Build Configuration
|
|
|
|
### package.json Scripts
|
|
|
|
```json
|
|
{
|
|
"scripts": {
|
|
"build": "npm run build:ts && npm run build:ui",
|
|
"build:ts": "tsc",
|
|
"build:ui": "node scripts/copy-ui.js",
|
|
"dev": "tsx src/index.ts",
|
|
"start": "node dist/index.js"
|
|
}
|
|
}
|
|
```
|
|
|
|
### scripts/copy-ui.js
|
|
|
|
```javascript
|
|
import fs from 'fs-extra';
|
|
import path from 'path';
|
|
|
|
const uiSource = path.join(process.cwd(), 'src', 'ui');
|
|
const uiDest = path.join(process.cwd(), 'dist', 'app-ui');
|
|
|
|
console.log('Copying UI files...');
|
|
console.log(`From: ${uiSource}`);
|
|
console.log(`To: ${uiDest}`);
|
|
|
|
// Ensure dist/app-ui exists
|
|
fs.ensureDirSync(uiDest);
|
|
|
|
// Copy all HTML files from src/ui to dist/app-ui
|
|
fs.copySync(uiSource, uiDest, { overwrite: true });
|
|
|
|
console.log('✅ UI files copied successfully');
|
|
```
|
|
|
|
**Install fs-extra:**
|
|
```bash
|
|
npm install --save-dev fs-extra @types/fs-extra
|
|
```
|
|
|
|
### tsconfig.json
|
|
|
|
```json
|
|
{
|
|
"compilerOptions": {
|
|
"target": "ES2022",
|
|
"module": "ES2022",
|
|
"moduleResolution": "node",
|
|
"outDir": "./dist",
|
|
"rootDir": "./src",
|
|
"strict": true,
|
|
"esModuleInterop": true,
|
|
"skipLibCheck": true,
|
|
"forceConsistentCasingInFileNames": true
|
|
},
|
|
"include": ["src/**/*"],
|
|
"exclude": ["node_modules", "dist", "src/ui"]
|
|
}
|
|
```
|
|
|
|
**Note:** Exclude `src/ui` from TypeScript compilation (HTML files don't need compiling)
|
|
|
|
---
|
|
|
|
## 8. Testing Apps
|
|
|
|
### 1. Build the server
|
|
```bash
|
|
npm run build
|
|
```
|
|
|
|
### 2. Add to Claude Desktop config
|
|
```json
|
|
{
|
|
"mcpServers": {
|
|
"myservice": {
|
|
"command": "node",
|
|
"args": ["/absolute/path/to/dist/index.js"],
|
|
"env": {
|
|
"API_KEY": "your_key_here"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. Restart Claude Desktop
|
|
|
|
### 4. Call an app tool
|
|
```
|
|
Can you show me the contact grid for "john"?
|
|
```
|
|
|
|
Claude will call `view_contact_grid` → Server returns `structuredContent` → UI renders in Claude Desktop
|
|
|
|
---
|
|
|
|
## 9. When to Use Apps vs Regular Tools
|
|
|
|
| Scenario | Use App | Use Regular Tool |
|
|
|----------|---------|------------------|
|
|
| Display contact list | ✅ Grid UI | ❌ JSON dump |
|
|
| Show dashboard stats | ✅ Dashboard UI | ❌ Plain numbers |
|
|
| Get single contact by ID | ❌ Overkill | ✅ JSON response |
|
|
| Create a new record | ❌ No UI needed | ✅ POST + return result |
|
|
| Search + display results | ✅ Grid UI | Maybe (depends on result size) |
|
|
| Calendar of appointments | ✅ Calendar UI | ❌ JSON dates hard to parse |
|
|
| Invoice details | ✅ Card UI | Maybe |
|
|
|
|
**Rule of thumb:** If the result benefits from visual formatting, use an app. If it's pure data/CRUD, use a regular tool.
|
|
|
|
---
|
|
|
|
## 10. Common Pitfalls
|
|
|
|
### ❌ UI files not copied to dist/
|
|
**Solution:** Add `build:ui` script that copies HTML from `src/ui/` to `dist/app-ui/`
|
|
|
|
### ❌ UI path resolution fails
|
|
**Solution:** Use `fileURLToPath` for ESM `__dirname` equivalent + check `fs.existsSync()`
|
|
|
|
### ❌ Data not showing in UI
|
|
**Solution:** Check `event.data.type === 'mcp-app-init'` and log `event.data.data` to console
|
|
|
|
### ❌ Resources not registered
|
|
**Solution:** Add `capabilities: { resources: {} }` and implement `ListResourcesRequestSchema` + `ReadResourceRequestSchema`
|
|
|
|
### ❌ HTML escaping issues
|
|
**Solution:** Always escape user data with `escapeHtml()` function
|
|
|
|
---
|
|
|
|
## 11. App Tool Naming Convention
|
|
|
|
**Pattern:** `view_` or `show_` prefix for app tools
|
|
|
|
- `view_contact_grid` → Display contact grid
|
|
- `show_dashboard` → Display dashboard
|
|
- `view_opportunity_card` → Display opportunity card
|
|
- `show_calendar` → Display calendar
|
|
|
|
**Why:**
|
|
- Differentiates app tools from regular tools
|
|
- Signals to Claude that result is visual
|
|
- Clear intent (viewing vs fetching)
|
|
|
|
---
|
|
|
|
## 12. Example: Complete App Tool
|
|
|
|
```typescript
|
|
{
|
|
name: 'view_pipeline_board',
|
|
description: 'Display sales pipeline board with opportunities grouped by stage. Returns an interactive visual component.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
pipelineId: {
|
|
type: 'string',
|
|
description: 'Pipeline ID (optional, defaults to main pipeline)'
|
|
},
|
|
includeWon: {
|
|
type: 'boolean',
|
|
description: 'Include won deals (default: false)'
|
|
},
|
|
},
|
|
},
|
|
}
|
|
```
|
|
|
|
```typescript
|
|
private async viewPipelineBoard(args: Record<string, unknown>): Promise<AppToolResult> {
|
|
const { pipelineId, includeWon = false } = args;
|
|
|
|
// Fetch pipeline data from API
|
|
const pipeline = await this.apiClient.getPipeline(pipelineId);
|
|
const opportunities = await this.apiClient.getOpportunities({
|
|
pipelineId,
|
|
status: includeWon ? 'all' : 'active',
|
|
});
|
|
|
|
// Group by stage
|
|
const groupedByStage = opportunities.reduce((acc, opp) => {
|
|
if (!acc[opp.stageId]) acc[opp.stageId] = [];
|
|
acc[opp.stageId].push(opp);
|
|
return acc;
|
|
}, {} as Record<string, any[]>);
|
|
|
|
return {
|
|
content: [{
|
|
type: 'text',
|
|
text: `Pipeline Board: ${pipeline.name} (${opportunities.length} opportunities)`,
|
|
}],
|
|
structuredContent: {
|
|
type: 'ui',
|
|
uri: 'ui://myservice/pipeline-board',
|
|
data: {
|
|
pipeline,
|
|
opportunities,
|
|
groupedByStage,
|
|
includeWon,
|
|
timestamp: new Date().toISOString(),
|
|
},
|
|
},
|
|
};
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 13. Resources
|
|
|
|
- **MCP Apps Docs:** https://modelcontextprotocol.io/docs/apps
|
|
- **Example Apps:** `/Users/jakeshore/.clawdbot/workspace/mcp-diagrams/ghl-mcp-apps-only/`
|
|
- **GHL MCP Server:** `/Users/jakeshore/.clawdbot/workspace/mcp-diagrams/GoHighLevel-MCP/`
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
**To add apps to an MCP server:**
|
|
1. Create `MCPAppsManager` class in `src/apps/index.ts`
|
|
2. Build HTML UI components in `src/ui/`
|
|
3. Register resource handlers in apps manager
|
|
4. Add `capabilities: { resources: {} }` to server
|
|
5. Implement `ListResourcesRequestSchema` and `ReadResourceRequestSchema`
|
|
6. Return `structuredContent` from app tool handlers
|
|
7. Copy UI files to `dist/app-ui/` during build
|
|
|
|
**Benefits:**
|
|
- Rich visual presentation of data
|
|
- Better UX in Claude Desktop
|
|
- Interactive components (grids, cards, dashboards)
|
|
- Clear separation of regular tools vs visual tools
|
|
|
|
Follow this pattern and your apps will integrate seamlessly with your MCP server.
|