GHL MCP full update — 2026-02-06

=== DONE ===
- MCP Apps UI system added (11 apps with _meta.ui.resourceUri)
- 19 new tool modules added
- Tool count: 269 → 461 across 38 categories
- Upstream changes merged
- All tools tagged with _meta labels
- Package lock updated

=== TO-DO ===
- [ ] Fix 42 failing edge case tests (BLOCKER — Stage 11)
- [ ] Live API testing with GHL credentials
- [ ] App design approval for Stage 7→8
This commit is contained in:
Jake Shore 2026-02-06 06:27:05 -05:00
parent c1fbbdd95b
commit 4f2a8d6ab5
403 changed files with 36141 additions and 869 deletions

177
AGENT-TASKS.md Normal file
View File

@ -0,0 +1,177 @@
# Agent Task Briefs (for Phase 2 spawn)
## Shared Context for ALL Phase 2 Agents
- Working dir: `/Users/jakeshore/.clawdbot/workspace/mcp-diagrams/GoHighLevel-MCP/src/ui/react-app/`
- Read the full plan: `../../REACT-REWRITE-PLAN.md`
- Read types: `src/types.ts` (created by Alpha — has all prop interfaces)
- Read registry: `src/renderer/registry.ts` (has stub components — replace yours)
- Read current string implementations: `../json-render-app/src/components.ts` and `../json-render-app/src/charts.ts`
- Use MCPAppContext via `import { useMCPApp } from '../context/MCPAppContext'` for callTool
- CSS goes in `src/styles/components.css` (display) or `src/styles/interactive.css` (interactive)
- Each component is a React FC exported from its own file
- After building all components, UPDATE `src/renderer/registry.ts` to import your real components instead of stubs
- NO GHL references — everything is generic MCP UI Kit
- Compact sizing: 12px base, tight padding, designed for chat inline display
---
## Agent Bravo — Layout + Core Data (15 components)
### Files to create:
```
src/components/layout/PageHeader.tsx
src/components/layout/Card.tsx
src/components/layout/StatsGrid.tsx
src/components/layout/SplitLayout.tsx
src/components/layout/Section.tsx
src/components/data/DataTable.tsx
src/components/data/KanbanBoard.tsx ← DRAG AND DROP (key component)
src/components/data/MetricCard.tsx
src/components/data/StatusBadge.tsx
src/components/data/Timeline.tsx
src/components/data/DetailHeader.tsx
src/components/data/KeyValueList.tsx
src/components/data/LineItemsTable.tsx
src/components/data/InfoBlock.tsx
src/components/data/ProgressBar.tsx
```
### Special attention:
- **KanbanBoard** — Must have REAL drag-and-drop via React state (onDragStart/onDragOver/onDrop).
- Cards are draggable between columns
- Optimistic UI: move card immediately in state, revert on error
- Accepts `moveTool?: string` prop — if provided, calls `callTool(moveTool, { opportunityId, pipelineStageId })` on drop
- Drop zone highlights with dashed border
- Cards show hover lift effect
- **DataTable** — Sortable columns (click header), clickable rows (emit onRowClick), pagination
- All layout components accept `children` prop for nested UITree elements
---
## Agent Charlie — Extended Data + Navigation + Actions (15 components)
### Files to create:
```
src/components/data/CurrencyDisplay.tsx
src/components/data/TagList.tsx
src/components/data/CardGrid.tsx
src/components/data/AvatarGroup.tsx
src/components/data/StarRating.tsx
src/components/data/StockIndicator.tsx
src/components/shared/SearchBar.tsx
src/components/shared/FilterChips.tsx
src/components/shared/TabGroup.tsx
src/components/shared/ActionButton.tsx
src/components/shared/ActionBar.tsx
```
Plus from data/:
```
src/components/data/ChecklistView.tsx
src/components/data/AudioPlayer.tsx
```
### Special attention:
- **SearchBar** — Controlled input, fires onChange with debounce. Accepts `onSearch?: (query: string) => void`
- **FilterChips** — Toggle active state on click, fires onFilter
- **TabGroup** — Controlled tab state, fires onTabChange
- **ActionButton** — Accepts `onClick` + optional `toolName` + `toolArgs` props. If toolName provided, calls callTool on click.
- **ChecklistView** — Checkboxes toggle completed state. Accepts `onToggle?: (itemId, completed) => void`
---
## Agent Delta — Comms + Viz + Charts (16 components)
### Files to create:
```
src/components/comms/ChatThread.tsx
src/components/comms/EmailPreview.tsx
src/components/comms/ContentPreview.tsx
src/components/comms/TranscriptView.tsx
src/components/viz/CalendarView.tsx
src/components/viz/FlowDiagram.tsx
src/components/viz/TreeView.tsx
src/components/viz/MediaGallery.tsx
src/components/viz/DuplicateCompare.tsx
src/components/charts/BarChart.tsx
src/components/charts/LineChart.tsx
src/components/charts/PieChart.tsx
src/components/charts/FunnelChart.tsx
src/components/charts/SparklineChart.tsx
```
### Special attention:
- **All charts** — Use inline SVG (same approach as current charts.ts). Convert from template strings to JSX SVG elements.
- **CalendarView** — Grid layout, today highlight, event dots. Accepts `onDateClick?: (date: string) => void`
- **TreeView** — Expandable nodes with click-to-toggle. Track expanded state in local component state.
- **ChatThread** — Outbound messages right-aligned indigo, inbound left-aligned gray. Auto-scroll to bottom.
- **FlowDiagram** — Horizontal/vertical node→arrow→node layout with SVG connectors.
---
## Agent Echo — Interactive Components + Forms (8 components + shared UI)
### Files to create:
```
src/components/interactive/ContactPicker.tsx
src/components/interactive/InvoiceBuilder.tsx
src/components/interactive/OpportunityEditor.tsx
src/components/interactive/AppointmentBooker.tsx
src/components/interactive/EditableField.tsx
src/components/interactive/SelectDropdown.tsx
src/components/interactive/FormGroup.tsx
src/components/interactive/AmountInput.tsx
src/components/shared/Toast.tsx
src/components/shared/Modal.tsx
```
### Critical design:
ALL interactive components are **CRM-agnostic**. They receive tool names as props.
- **ContactPicker** — Searchable dropdown.
- Props: `searchTool: string` (e.g., "search_contacts"), `onSelect: (contact) => void`, `placeholder?: string`
- On keystroke (debounced 300ms): calls `callTool(searchTool, { query })` → displays results as dropdown options
- On select: calls onSelect with full contact object, closes dropdown
- Shows loading spinner during search
- **InvoiceBuilder** — Multi-section form.
- Props: `createTool?: string`, `contactSearchTool?: string`
- Sections: Contact (uses ContactPicker), Line Items (add/remove rows), Totals (auto-calc)
- Each line item: description, quantity, unit price, total
- "Create Invoice" button calls `callTool(createTool, { contactId, lineItems, ... })`
- **OpportunityEditor** — Inline edit form.
- Props: `saveTool: string` (e.g., "update_opportunity"), `opportunity: { id, name, value, status, stageId }`
- Renders current values with click-to-edit behavior
- Save button calls `callTool(saveTool, { opportunityId, ...changes })`
- **AppointmentBooker** — Date/time picker + form.
- Props: `calendarTool?: string`, `bookTool?: string`, `contactSearchTool?: string`
- Calendar grid for date selection, time slot list, contact picker, notes field
- Book button calls `callTool(bookTool, { calendarId, contactId, date, time, ... })`
- **EditableField** — Click-to-edit wrapper.
- Props: `value: string`, `saveTool?: string`, `saveArgs?: Record`, `fieldName: string`
- Click → shows input, blur/enter → saves via callTool if provided
- **SelectDropdown** — Generic async select.
- Props: `loadTool?: string`, `options?: Array`, `onChange`, `placeholder`
- If loadTool provided, fetches options on mount/open
- **FormGroup** — Layout for labeled form fields.
- Props: `fields: Array<{ key, label, type, value, required? }>`, `onSubmit`, `submitLabel`
- **AmountInput** — Currency-formatted number input.
- Props: `value, onChange, currency?, locale?`
- Formats display as $1,234.56, stores raw number
- **Toast** — Notification component (renders via React portal).
- Export `useToast()` hook: `const { showToast } = useToast()`
- `showToast('Deal moved!', 'success')` — auto-dismisses
- **Modal** — Dialog component (renders via React portal).
- Props: `isOpen, onClose, title, children, footer?`
- Backdrop click to close, escape key to close
### Styles:
Write `src/styles/interactive.css` with all interactive styles (modals, toasts, dropdowns, drag states, form inputs, etc.)

263
REACT-REWRITE-PLAN.md Normal file
View File

@ -0,0 +1,263 @@
# MCP UI Kit — React Rewrite Plan
## Vision
Build a **generic, reusable MCP UI component library** using React + ext-apps SDK. Any MCP server (GHL, HubSpot, Salesforce, etc.) can use this component kit to render AI-generated interactive UIs. GHL is the first implementation. The library is CRM-agnostic — interactive components accept tool names as props so each server configures its own tool mappings.
---
## Architecture
```
┌─────────────────────────────────────────────────────────┐
│ Goose / Claude Desktop (MCP Host) │
│ │
│ tools/call → GHL MCP Server → GHL API │
│ ↑ ↓ │
│ tools/call structuredContent (JSON UI tree) │
│ ↑ ↓ │
│ ┌─────────────────────────────────────────────┐ │
│ │ React App (iframe via ext-apps SDK) │ │
│ │ │ │
│ │ useApp() hook ← ontoolresult (UI tree) │ │
│ │ ↓ │ │
│ │ MCPAppProvider (React Context) │ │
│ │ - uiTree state │ │
│ │ - formState (inputs, selections) │ │
│ │ - callTool(name, args) → MCP server │ │
│ │ ↓ │ │
│ │ <UITreeRenderer tree={uiTree} /> │ │
│ │ - Looks up component by type │ │
│ │ - Renders React component with props │ │
│ │ - Recursively renders children │ │
│ │ ↓ │ │
│ │ 42 Display Components (pure, CRM-agnostic) │ │
│ │ + 8 Interactive Components (tool-configurable)│ │
│ │ - ContactPicker(searchTool="search_contacts")│ │
│ │ - InvoiceBuilder(createTool="create_invoice")│ │
│ │ - KanbanBoard(onMoveTool="update_opportunity")│ │
│ │ - EditableField(saveTool=props.saveTool) │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
## File Structure (New)
```
src/ui/react-app/ # GENERIC MCP UI KIT (no GHL references)
├── package.json # React + ext-apps SDK deps
├── vite.config.ts # Vite + singlefile + React
├── tsconfig.json
├── index.html # Entry point
├── src/
│ ├── App.tsx # Root — useApp hook, MCPAppProvider
│ ├── types.ts # UITree, UIElement, component prop interfaces
│ ├── context/
│ │ └── MCPAppContext.tsx # React Context — uiTree, formState, callTool
│ ├── hooks/
│ │ ├── useCallTool.ts # Hook wrapping app.callServerTool
│ │ ├── useFormState.ts # Shared form state management
│ │ └── useSizeReporter.ts # Auto-report content size to host
│ ├── renderer/
│ │ ├── UITreeRenderer.tsx # Recursive tree → React component resolver
│ │ └── registry.ts # Component name → React component map
│ ├── components/
│ │ ├── layout/ # PageHeader, Card, SplitLayout, Section, StatsGrid
│ │ ├── data/ # DataTable, KanbanBoard, MetricCard, StatusBadge,
│ │ │ # Timeline, ProgressBar, KeyValueList, etc.
│ │ ├── charts/ # BarChart, LineChart, PieChart, FunnelChart, Sparkline
│ │ ├── comms/ # ChatThread, EmailPreview, TranscriptView, etc.
│ │ ├── viz/ # CalendarView, FlowDiagram, TreeView, MediaGallery, etc.
│ │ ├── interactive/ # ContactPicker, InvoiceBuilder, EditableField, etc.
│ │ │ # All accept tool names as PROPS (CRM-agnostic)
│ │ └── shared/ # ActionButton, Toast, Modal (React portals)
│ └── styles/
│ ├── base.css # Reset, variables, typography
│ ├── components.css # Component-specific styles (compact for chat)
│ └── interactive.css # Drag/drop, modals, toasts, form styles
```
### CRM-Agnostic Design Principle
- NO component imports GHL types or references GHL tool names
- Interactive components receive tool names via props:
- `ContactPicker``searchTool="search_contacts"` (GHL) or `"hubspot_search_contacts"` (HubSpot)
- `KanbanBoard``moveTool="update_opportunity"` (GHL) or `"move_deal"` (Pipedrive)
- `InvoiceBuilder``createTool="create_invoice"` (any billing system)
- The MCP server's AI prompt tells Claude which tool names to use in the UI tree
- Components call `callTool(props.toolName, args)` — they don't know or care what CRM is behind it
## Component Inventory
### Existing 42 (string → React conversion)
**Layout (5):** PageHeader, Card, StatsGrid, SplitLayout, Section
**Data Display (10):** DataTable, KanbanBoard, MetricCard, StatusBadge, Timeline, ProgressBar, DetailHeader, KeyValueList, LineItemsTable, InfoBlock
**Navigation (3):** SearchBar, FilterChips, TabGroup
**Actions (2):** ActionButton, ActionBar
**Extended Data (6):** CurrencyDisplay, TagList, CardGrid, AvatarGroup, StarRating, StockIndicator
**Communications (6):** ChatThread, EmailPreview, ContentPreview, TranscriptView, AudioPlayer, ChecklistView
**Visualization (5):** CalendarView, FlowDiagram, TreeView, MediaGallery, DuplicateCompare
**Charts (5):** BarChart, LineChart, PieChart, FunnelChart, SparklineChart
### New Interactive Components (8)
| Component | Purpose | MCP Tools Used |
|-----------|---------|----------------|
| **ContactPicker** | Searchable dropdown, fetches contacts on type | `search_contacts` |
| **InvoiceBuilder** | Line items, totals, contact auto-fill | `create_invoice`, `get_contact` |
| **OpportunityEditor** | Inline edit deal name/value/status/stage | `update_opportunity` |
| **AppointmentBooker** | Calendar slot picker + booking form | `get_calendar`, `create_appointment` |
| **EditableField** | Click-to-edit any text/number field | varies (generic) |
| **SelectDropdown** | Generic select with async option loading | varies |
| **FormGroup** | Group of form fields with validation | varies |
| **AmountInput** | Currency-formatted number input | — (local state) |
---
## Agent Team Plan
### Phase 1: Foundation (Sequential — 1 agent)
**Agent Alpha — Project Scaffold + App Shell**
- Create `src/ui/react-app/` with package.json, vite.config, tsconfig
- Install deps: react, react-dom, @modelcontextprotocol/ext-apps, @vitejs/plugin-react, vite-plugin-singlefile
- Build `App.tsx` with `useApp` hook — handles ontoolresult, ontoolinput, host context
- Build `GHLContext.tsx` — React context providing uiTree, formState, callTool
- Build `useCallTool.ts` — wrapper around `app.callServerTool` with loading/error states
- Build `useFormState.ts` — shared form state hook
- Build `useSizeReporter.ts` — auto-measures content, sends `ui/notifications/size-changed`
- Build `UITreeRenderer.tsx` — recursive renderer that resolves component types from registry
- Build `registry.ts` — component map (stubs for now, filled by other agents)
- Build `types.ts` — UITree, UIElement, all component prop interfaces
- Build base CSS (`base.css`) — reset, variables, compact typography
- Update outer build pipeline in GoHighLevel-MCP `package.json` to build React app
- **Output:** Working scaffold that renders a loading state, connects to host via ext-apps
### Phase 2: Components (Parallel — 4 agents)
**Agent Bravo — Layout + Core Data Components (15)**
Files: `components/layout/`, `components/data/` (first half)
- PageHeader, Card, StatsGrid, SplitLayout, Section
- DataTable (with clickable rows, sortable columns)
- KanbanBoard (with FULL drag-and-drop via React state — no DOM hacking)
- MetricCard, StatusBadge, Timeline
- Register all in registry.ts
- Component CSS in `components.css`
**Agent Charlie — Data Display + Navigation + Actions (15)**
Files: `components/data/` (second half), `components/shared/`
- ProgressBar, DetailHeader, KeyValueList, LineItemsTable, InfoBlock
- SearchBar, FilterChips, TabGroup
- ActionButton, ActionBar
- CurrencyDisplay, TagList, CardGrid, AvatarGroup, StarRating, StockIndicator
- Register all in registry.ts
**Agent Delta — Comms + Viz + Charts (16)**
Files: `components/comms/`, `components/viz/`, `components/charts/`
- ChatThread, EmailPreview, ContentPreview, TranscriptView, AudioPlayer, ChecklistView
- CalendarView, FlowDiagram, TreeView, MediaGallery, DuplicateCompare
- BarChart, LineChart, PieChart, FunnelChart, SparklineChart
- All chart components use inline SVG (same approach, just JSX)
- Register all in registry.ts
**Agent Echo — Interactive Components + Forms (8)**
Files: `components/interactive/`, `hooks/`
- ContactPicker — searchable dropdown, calls `search_contacts` on keystroke with debounce
- InvoiceBuilder — line items table + contact selection + auto-total
- OpportunityEditor — inline edit form for deal fields, saves via `update_opportunity`
- AppointmentBooker — date/time picker + contact + calendar selection
- EditableField — click-to-edit wrapper for any field
- SelectDropdown — generic async select
- FormGroup — form layout with labels + validation
- AmountInput — formatted currency input
- Shared: Toast component, Modal component (proper React portals)
- Integrate with GHLContext for tool calling
### Phase 3: Integration (Sequential — 1 agent)
**Agent Foxtrot — Wire Everything Together**
- Merge all component registrations into `registry.ts`
- Update `src/apps/index.ts`:
- Add new tool definitions for interactive components (`create_invoice`, `create_appointment`)
- Update resource handler for `ui://ghl/dynamic-view` to serve React build
- Add new resource URIs if needed
- Update `src/server.ts` if new tools need routing
- Update system prompt: add new interactive component catalog entries
- Update Goose config `available_tools` if needed
- Full build: React app → singlefile HTML → server TypeScript
- Test: verify JSON UI trees render correctly, interactive components call tools, drag-and-drop works
- Write brief README for the new architecture
---
## Key Technical Decisions
### State Management
- **React Context** (not Redux) — app is small enough, context + useReducer is perfect
- `GHLContext` holds: current UITree, form values, loading states, selected entities
- Any component can `const { callTool } = useGHL()` to interact with the MCP server
### Tool Calling Pattern
```tsx
// Any component can call MCP tools:
const { callTool, isLoading } = useCallTool();
const handleDrop = async (cardId: string, newStageId: string) => {
await callTool('update_opportunity', {
opportunityId: cardId,
pipelineStageId: newStageId,
});
};
```
### Drag & Drop (KanbanBoard)
- Pure React state — no global DOM event handlers
- `onDragStart`, `onDragOver`, `onDrop` on React elements
- Optimistic UI update (move card immediately, revert on error)
### Dynamic Sizing
- `useSizeReporter` hook — ResizeObserver on `#app`
- Sends `ui/notifications/size-changed` on every size change
- Caps at 600px height
### CSS Strategy
- Plain CSS files (not CSS modules, not Tailwind) — keeps bundle simple
- Same compact sizing as current (12px base, tight padding)
- All in `styles/` directory, imported in App.tsx
- Interactive styles (drag states, modals, toasts) in separate file
### Build Pipeline
```bash
# In src/ui/react-app/
npm run build
# → Vite builds React app → vite-plugin-singlefile → single HTML file
# → Output: ../../dist/app-ui/dynamic-view.html
# In GoHighLevel-MCP root
npm run build
# → Builds React UI first, then compiles TypeScript server
```
---
## Timeline Estimate
| Phase | Agents | Est. Time | Depends On |
|-------|--------|-----------|------------|
| Phase 1: Foundation | Alpha (1) | ~20 min | — |
| Phase 2: Components | Bravo, Charlie, Delta, Echo (4 parallel) | ~25 min | Phase 1 |
| Phase 3: Integration | Foxtrot (1) | ~15 min | Phase 2 |
| **Total** | **6 agents** | **~60 min** | |
---
## Success Criteria
1. ✅ All 42 existing components render identically to current version
2. ✅ JSON UI trees from Claude work without any format changes
3. ✅ KanbanBoard drag-and-drop moves deals and persists via `update_opportunity`
4. ✅ ContactPicker fetches real contacts from GHL on keystroke
5. ✅ InvoiceBuilder creates invoices with real contact data
6. ✅ EditableField saves changes via appropriate MCP tool
7. ✅ Dynamic sizing works — views fit in chat
8. ✅ Single HTML file output (vite-plugin-singlefile)
9. ✅ ext-apps handshake completes with Goose
10. ✅ All existing `view_*` tools still work alongside `generate_ghl_view`

369
REACT-STATE-ANALYSIS.md Normal file
View File

@ -0,0 +1,369 @@
# React State & Hydration Analysis — MCP App Lifecycle
## Executive Summary
**The interactive state destruction is caused by a cascading chain of three interconnected bugs:**
1. Every tool call result replaces the entire UITree → full component tree teardown
2. Element keys from the server may not be stable across calls → React unmounts everything
3. All interactive components store state locally (`useState`) instead of in the shared context that was built specifically to survive re-renders
---
## Bug #1: `ontoolresult``setUITree()` Nuclear Replacement
### The Code (App.tsx:80-86)
```tsx
app.ontoolresult = async (result) => {
const tree = extractUITree(result);
if (tree) {
setUITree(tree); // ← REPLACES the entire tree object
setToolInput(null);
}
};
```
### What Happens
Every tool result that contains a UITree — even from a button click, search query, or tab switch — causes `setUITree(brandNewTreeObject)`. This triggers:
1. `App.tsx` re-renders with new `uiTree` state
2. `<UITreeRenderer tree={uiTree} />` receives a **new object reference**
3. `ElementRenderer` receives a **new `elements` map** (new reference, even if data is identical)
4. React walks the entire tree and reconciles
**The critical question:** does it unmount/remount or just re-render?
That depends entirely on **Bug #2** (keys).
---
## Bug #2: `key: element.key` — Keys Control Component Identity
### The Code (UITreeRenderer.tsx:37-40)
```tsx
// Each component gets its key from the JSON tree
return React.createElement(Component, { key: element.key, ...element.props }, childElements);
// Children also keyed from the tree
const childElements = element.children?.map((childKey) =>
React.createElement(ElementRenderer, {
key: childKey, // ← from JSON
elementKey: childKey, // ← from JSON
elements,
}),
);
```
### The Problem
React uses `key` to determine component identity. When keys change between renders:
- **Same key** → React re-renders the existing component (state preserved)
- **Different key** → React unmounts old, mounts new (STATE DESTROYED)
If the MCP server generates keys like `contact-list-abc123` on call #1 and `contact-list-def456` on call #2, React sees them as **completely different components** and tears down the entire subtree.
**Even if keys are stable**, the `elements` object reference changes every time, forcing re-renders down the entire tree. Not a state-loss issue by itself, but causes performance problems and can trigger effects/callbacks unnecessarily.
---
## Bug #3: Dual `uiTree` State — App.tsx vs MCPAppContext
### The Code
```tsx
// App.tsx — has its own uiTree state
const [uiTree, setUITree] = useState<UITree | null>(null);
// MCPAppContext.tsx — ALSO has its own uiTree state
const [uiTree, setUITree] = useState<UITree | null>(null);
```
### The Problem
There are **two completely independent `uiTree` states**:
- `App.tsx` manages the actual rendering tree (used by `<UITreeRenderer tree={uiTree} />`)
- `MCPAppContext` has its own `uiTree` + `setUITree` exposed via context, but **nobody calls the context's `setUITree`**
The context's `uiTree` is **always null**. The context was designed to provide shared state (`formState`, `setFormValue`, `callTool`), but the tree management is disconnected.
---
## Bug #4: Interactive Components Use Local State That Gets Destroyed
### FormGroup (forms reset)
```tsx
// FormGroup.tsx — all form values in LOCAL useState
const [values, setValues] = useState<Record<string, string>>(() => {
const init: Record<string, string> = {};
for (const f of fields) { init[f.key] = ""; }
return init;
});
```
**Chain:** User types → submits → `execute(submitTool, values)` → server returns new tree → `ontoolresult``setUITree(newTree)` → FormGroup unmounts → remounts → `values` resets to `{}`
### KanbanBoard (drag-and-drop fails)
```tsx
// KanbanBoard.tsx — drag state + columns in LOCAL state
const [columns, setColumns] = useState<KanbanColumn[]>(initialColumns);
const [dropTargetStage, setDropTargetStage] = useState<string | null>(null);
const [draggingCardId, setDraggingCardId] = useState<string | null>(null);
const dragRef = useRef<DragState | null>(null);
```
**Chain:** User drags card → optimistic update → `callTool(moveTool, ...)` → server returns new tree → `ontoolresult``setUITree(newTree)` → KanbanBoard unmounts → remounts → drag state gone, optimistic move reverted, columns reset to server data
The KanbanBoard even has a mitigation attempt that fails:
```tsx
// This ref-based sync ONLY works if React re-renders without unmounting
const prevColumnsRef = useRef(initialColumns);
if (prevColumnsRef.current !== initialColumns) {
prevColumnsRef.current = initialColumns;
setColumns(initialColumns); // ← This runs, but after unmount/remount it's moot
}
```
### SearchBar (search clears)
```tsx
// SearchBar.tsx — query in LOCAL state
const [value, setValue] = useState("");
```
**Chain:** User types query → debounced `callTool(searchTool, ...)` → server returns new tree → SearchBar unmounts → remounts → search input clears
### ActionButton (stops working)
```tsx
// ActionButton.tsx — loading state in LOCAL state
const [loading, setLoading] = useState(false);
```
**Chain:** User clicks → `setLoading(true)``callTool(...)` → server returns new tree → ActionButton unmounts → remounts → `loading` stuck at initial `false`, but the async `callTool` still has a stale closure reference. The `finally { setLoading(false) }` calls `setLoading` on an unmounted component.
### TabGroup (tab selection lost)
```tsx
const [localActive, setLocalActive] = useState<string>(
controlledActiveTab || tabs[0]?.value || "",
);
```
**Chain:** User clicks tab → `setLocalActive(value)``callTool(switchTool, ...)` → new tree → TabGroup remounts → `localActive` resets to first tab
### ContactPicker (search results disappear)
```tsx
const [query, setQuery] = useState("");
const [results, setResults] = useState<Contact[]>([]);
const [isOpen, setIsOpen] = useState(false);
const [selected, setSelected] = useState<Contact | null>(null);
```
### InvoiceBuilder (entire form wiped)
```tsx
const [selectedContact, setSelectedContact] = useState<SelectedContact | null>(null);
const [lineItems, setLineItems] = useState<InvoiceLineItem[]>(...);
const [taxRate, setTaxRate] = useState(8.5);
```
---
## Bug #5: Hydration Path Mismatch
### The Code (App.tsx:60-66)
```tsx
useEffect(() => {
const preInjected = getPreInjectedTree();
if (preInjected && !uiTree) {
setUITree(preInjected);
}
}, []);
```
### The Problem
Not a React hydration mismatch (we use `createRoot`, not `hydrateRoot`), but a **data transition issue**:
1. App mounts → `uiTree = null` → shows "Connecting..."
2. `useEffect` fires → finds `window.__MCP_APP_DATA__``setUITree(preInjectedTree)`
3. Tree renders with pre-injected keys
4. `ontoolresult` fires with server-generated tree → `setUITree(serverTree)`
5. If keys differ between pre-injected and server tree → **full unmount/remount**
The pre-injected path and the dynamic path produce trees with potentially different key schemas, causing a jarring full teardown on the first real tool result.
---
## Bug #6: MCPAppProvider Context Is Stable (NOT a bug)
`MCPAppProvider` wrapping does **not** cause context loss. It's rendered consistently in all branches of the conditional render in App.tsx. The provider identity is stable.
However, the provider creates a **new `value` object on every render**:
```tsx
const value: MCPAppContextValue = {
uiTree, setUITree, formState, setFormValue, resetFormState, callTool, isLoading, app,
};
return <MCPAppContext.Provider value={value}>{children}</MCPAppContext.Provider>;
```
This causes every `useMCPApp()` consumer to re-render on every provider render, but it doesn't cause state loss — just unnecessary renders.
---
## The Kill Chain (Full Interaction Flow)
```
User clicks ActionButton("View Contact", toolName="get_contact", toolArgs={id: "123"})
├─ ActionButton.handleClick()
│ ├─ setLoading(true)
│ └─ callTool("get_contact", {id: "123"})
│ └─ app.callServerTool({name: "get_contact", arguments: {id: "123"}})
├─ MCP Server processes tool call, returns CallToolResult with NEW UITree
├─ app.ontoolresult fires
│ ├─ extractUITree(result) → newTree (new keys, new elements, new object)
│ └─ setUITree(newTree) ← App.tsx state update
├─ App.tsx re-renders
│ └─ <UITreeRenderer tree={newTree} />
│ └─ ElementRenderer receives new elements map
│ └─ For each element: React.createElement(Component, {key: NEW_KEY, ...})
│ └─ React sees different key → UNMOUNT old component, MOUNT new one
├─ ALL components unmount:
│ ├─ FormGroup: values={} (reset)
│ ├─ KanbanBoard: columns=initial, dragState=null (reset)
│ ├─ SearchBar: value="" (reset)
│ ├─ TabGroup: localActive=first tab (reset)
│ ├─ ContactPicker: query="", results=[], selected=null (reset)
│ └─ InvoiceBuilder: lineItems=[default], contact=null (reset)
└─ Meanwhile, ActionButton's finally{} block calls setLoading(false)
on an UNMOUNTED component → React warning + no-op
```
---
## Fixes
### Fix 1: Deterministic Stable Keys (Server-Side)
The MCP server must generate **deterministic, position-stable keys** for UITree elements. Keys should be based on component type + semantic identity, not random IDs.
```typescript
// BAD: keys change every call
{ key: `card-${crypto.randomUUID()}`, type: "Card", ... }
// GOOD: keys are stable across calls
{ key: "contact-detail-card", type: "Card", ... }
{ key: "pipeline-kanban", type: "KanbanBoard", ... }
```
### Fix 2: Tree Diffing / Partial Updates Instead of Full Replace
Don't replace the entire tree on every tool result. Diff the new tree against the old one and only update changed branches:
```tsx
// App.tsx — instead of wholesale replace:
app.ontoolresult = async (result) => {
const newTree = extractUITree(result);
if (newTree) {
setUITree(prev => {
if (!prev) return newTree;
// Merge: keep unchanged elements, update changed ones
return mergeUITrees(prev, newTree);
});
}
};
function mergeUITrees(oldTree: UITree, newTree: UITree): UITree {
const mergedElements: Record<string, UIElement> = {};
for (const [key, newEl] of Object.entries(newTree.elements)) {
const oldEl = oldTree.elements[key];
// Keep old reference if data is identical (prevents re-render)
if (oldEl && JSON.stringify(oldEl) === JSON.stringify(newEl)) {
mergedElements[key] = oldEl;
} else {
mergedElements[key] = newEl;
}
}
return { root: newTree.root, elements: mergedElements };
}
```
### Fix 3: Separate Data Results from UI Results
Not every tool call should trigger a tree replacement. ActionButton clicks that return data (not a new view) should be handled by the component, not by `ontoolresult`:
```tsx
// ActionButton.tsx — handle result locally, don't let ontoolresult replace tree
const handleClick = useCallback(async () => {
if (!toolName || loading || disabled) return;
setLoading(true);
try {
const result = await callTool(toolName, toolArgs || {});
// Result handled locally — only navigate if result contains a NEW view
if (result && hasNavigationIntent(result)) {
// Let ontoolresult handle it
}
// Otherwise, toast/notification with result, don't replace tree
} finally {
setLoading(false);
}
}, [toolName, toolArgs, loading, disabled, callTool]);
```
### Fix 4: Move Interactive State to Context
Use the existing `formState` in MCPAppContext instead of local `useState`:
```tsx
// FormGroup.tsx — use shared form state that survives remounts
const { formState, setFormValue } = useMCPApp();
// Instead of local useState, derive from context
const getValue = (key: string) => formState[`form:${formId}:${key}`] ?? "";
const setValue = (key: string, val: string) => setFormValue(`form:${formId}:${key}`, val);
```
### Fix 5: Fix the Dual uiTree State
Either:
- **Option A:** Remove `uiTree` from MCPAppContext entirely (it's unused)
- **Option B:** Have App.tsx use the context's uiTree and remove its local state
```tsx
// Option B: App.tsx uses context state
export function App() {
const [hostContext, setHostContext] = useState<McpUiHostContext | undefined>();
// Remove local uiTree state — let context own it
// In MCPAppProvider, connect ontoolresult to context's setUITree
}
```
### Fix 6: Memoize Context Value
Prevent unnecessary re-renders of all context consumers:
```tsx
// MCPAppContext.tsx
const value = useMemo<MCPAppContextValue>(() => ({
uiTree, setUITree, formState, setFormValue, resetFormState, callTool, isLoading, app,
}), [uiTree, formState, callTool, isLoading, app]);
```
### Fix 7: Add React.memo to Leaf Components
Prevent re-renders when props haven't actually changed:
```tsx
export const MetricCard = React.memo<MetricCardProps>(({ label, value, trend, ... }) => {
// ...
});
export const StatusBadge = React.memo<StatusBadgeProps>(({ label, variant }) => {
// ...
});
```
---
## Priority Order
| # | Fix | Impact | Effort |
|---|-----|--------|--------|
| 1 | Stable keys (server-side) | 🔴 Critical — fixes unmount/remount | Medium |
| 2 | Tree diffing/merge | 🔴 Critical — prevents unnecessary teardown | Medium |
| 3 | Separate data vs UI results | 🟡 High — stops button clicks from nuking the view | Low |
| 4 | Context-based interactive state | 🟡 High — state survives even if components remount | Medium |
| 5 | Fix dual uiTree state | 🟢 Medium — removes confusion, single source of truth | Low |
| 6 | Memoize context value | 🟢 Medium — performance improvement | Low |
| 7 | React.memo leaf components | 🟢 Low — performance polish | Low |

View File

@ -0,0 +1,639 @@
# MCP App Fallback Architecture
## Graceful Degradation for Hosts Without Full Interactive Support
**Date:** 2026-02-03
**Status:** Proposed
**Applies to:** GoHighLevel MCP UI Kit React App
---
## Executive Summary
The ext-apps SDK provides explicit **host capability negotiation** via `McpUiHostCapabilities`. After the `ui/initialize` handshake, the View knows exactly which features the host supports. We use this to implement a **three-tier progressive enhancement** model: Static → Context-Synced → Fully Interactive.
---
## 1. Feature Detection (The Foundation)
### How It Works
When the View connects, the host returns `McpUiHostCapabilities` in the `ui/initialize` response. The `App` class exposes this via `app.getHostCapabilities()`.
**The key capability flags:**
```typescript
interface McpUiHostCapabilities {
serverTools?: { listChanged?: boolean }; // Can proxy tools/call to MCP server
serverResources?: { listChanged?: boolean }; // Can proxy resources/read
updateModelContext?: {}; // Accepts ui/update-model-context
message?: {}; // Accepts ui/message (send chat messages)
openLinks?: {}; // Can open external URLs
logging?: {}; // Accepts log messages
sandbox?: { permissions?: {...}; csp?: {...} };
}
```
**The critical check: `serverTools` tells you if `callServerTool` will work.**
### Implementation: `useHostCapabilities` Hook
```typescript
// hooks/useHostCapabilities.ts
import { useMemo } from "react";
import { useMCPApp } from "../context/MCPAppContext.js";
import type { McpUiHostCapabilities } from "@modelcontextprotocol/ext-apps";
export type InteractionTier = "static" | "context-synced" | "full";
export interface HostCapabilityInfo {
/** Raw capabilities from the host */
raw: McpUiHostCapabilities | undefined;
/** Whether callServerTool() will work */
canCallTools: boolean;
/** Whether updateModelContext() will work */
canUpdateContext: boolean;
/** Whether sendMessage() will work */
canSendMessages: boolean;
/** The interaction tier this host supports */
tier: InteractionTier;
}
export function useHostCapabilities(): HostCapabilityInfo {
const { app } = useMCPApp();
return useMemo(() => {
const raw = app?.getHostCapabilities?.() as McpUiHostCapabilities | undefined;
const canCallTools = !!raw?.serverTools;
const canUpdateContext = !!raw?.updateModelContext;
const canSendMessages = !!raw?.message;
// Determine tier
let tier: InteractionTier = "static";
if (canCallTools) {
tier = "full";
} else if (canUpdateContext) {
tier = "context-synced";
}
return { raw, canCallTools, canUpdateContext, canSendMessages, tier };
}, [app]);
}
```
### Add to MCPAppContext
```typescript
// In MCPAppContext.tsx — extend the context value:
export interface MCPAppContextValue {
// ... existing fields ...
/** Host capability info, available after connection */
capabilities: HostCapabilityInfo;
/** Safe tool call: uses callServerTool if available,
falls back to updateModelContext, or no-ops */
safeCallTool: (
toolName: string,
args: Record<string, any>,
options?: { localOnly?: boolean }
) => Promise<CallToolResult | null>;
}
```
---
## 2. The Three-Tier Model
### Tier 1: Static (No host capabilities)
- Pure data visualization — charts, tables, badges, timelines
- All data comes from the initial `ontoolresult` payload
- No server communication, no context updates
- **Components work as-is:** BarChart, PieChart, StatusBadge, DataTable, Timeline, etc.
### Tier 2: Context-Synced (`updateModelContext` available, no `serverTools`)
- Local interactivity works (drag, edit, toggle, form fills)
- User actions update LOCAL state immediately
- State changes are synced to the LLM via `updateModelContext`
- The LLM can then decide to call tools itself on the next turn
- **Key pattern:** User drags a Kanban card → local state updates → `updateModelContext` tells the LLM what happened → LLM calls `move_opportunity` on its own
### Tier 3: Full Interactive (`serverTools` available)
- Everything works as currently designed
- Components call `callServerTool` directly for real-time server mutations
- Optimistic UI with server confirmation
```
┌─────────────────────────────────────────────────┐
│ TIER 3: FULL │
│ callServerTool ✓ updateModelContext ✓ │
│ Direct server mutations, optimistic UI │
│ ┌─────────────────────────────────────────────┐ │
│ │ TIER 2: CONTEXT-SYNCED │ │
│ │ callServerTool ✗ updateModelContext ✓ │ │
│ │ Local state + LLM-informed sync │ │
│ │ ┌─────────────────────────────────────────┐│ │
│ │ │ TIER 1: STATIC ││ │
│ │ │ Read-only data visualization ││ │
│ │ │ Charts, tables, badges, timelines ││ │
│ │ └─────────────────────────────────────────┘│ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
```
---
## 3. Component Classification
### Always Static (work everywhere)
These components are pure data display — they render from the UITree data and never call tools:
| Component | Category |
|-----------|----------|
| BarChart, LineChart, PieChart, SparklineChart, FunnelChart | charts |
| StatusBadge, ProgressBar, CurrencyDisplay, StarRating | data |
| DataTable, KeyValueList, TagList, AvatarGroup | data |
| Timeline, MetricCard, DetailHeader, InfoBlock | data |
| PageHeader, Section, Card, StatsGrid, SplitLayout | layout |
| ChatThread, TranscriptView, EmailPreview, ContentPreview | comms |
| CalendarView, TreeView, FlowDiagram, DuplicateCompare | viz |
### Context-Syncable (Tier 2+)
These work with local state and sync via `updateModelContext`:
| Component | Local Behavior | Context Sync |
|-----------|---------------|--------------|
| KanbanBoard | Drag cards between columns locally | Report new board state to LLM |
| EditableField | Edit text inline locally | Report edited values to LLM |
| FormGroup | Fill form fields locally | Report form state to LLM |
| SelectDropdown | Select options locally | Report selection to LLM |
| AmountInput | Adjust amounts locally | Report new amounts to LLM |
| ChecklistView | Toggle checkboxes locally | Report checklist state to LLM |
| FilterChips | Toggle filters locally | Report active filters to LLM |
| TabGroup | Switch tabs locally | Report active tab to LLM |
| SearchBar | Type search queries locally | Report search to LLM |
### Full Interactive Only (Tier 3)
These **require** server tool calls and degrade gracefully at lower tiers:
| Component | Tier 3 | Tier 2 Fallback | Tier 1 Fallback |
|-----------|--------|-----------------|-----------------|
| ActionButton | Calls tool | Shows "Ask assistant" hint | Disabled/hidden |
| AppointmentBooker | Books via tool | Shows form, reports to LLM | Shows read-only schedule |
| InvoiceBuilder | Creates via tool | Builds locally, reports to LLM | Shows existing invoice data |
| OpportunityEditor | Saves via tool | Edits locally, reports to LLM | Shows read-only data |
| ContactPicker | Searches via tool | Shows static options list | Shows read-only display |
---
## 4. Concrete Implementation
### 4a. `safeCallTool` — The Universal Action Handler
```typescript
// In MCPAppProvider
const safeCallTool = useCallback(
async (
toolName: string,
args: Record<string, any>,
options?: { localOnly?: boolean }
): Promise<CallToolResult | null> => {
// Tier 3: Full tool call
if (capabilities.canCallTools && !options?.localOnly) {
return callTool(toolName, args);
}
// Tier 2: Inform LLM via updateModelContext
if (capabilities.canUpdateContext && app) {
const markdown = [
`---`,
`action: ${toolName}`,
`timestamp: ${new Date().toISOString()}`,
...Object.entries(args).map(([k, v]) => `${k}: ${JSON.stringify(v)}`),
`---`,
`User performed action "${toolName}" in the UI.`,
`Please execute the corresponding server-side operation.`,
].join("\n");
await app.updateModelContext({
content: [{ type: "text", text: markdown }],
});
// Return a synthetic "pending" result
return {
content: [
{
type: "text",
text: `Action "${toolName}" reported to assistant. It will be processed on the next turn.`,
},
],
} as CallToolResult;
}
// Tier 1: No-op, return null
console.warn(`[MCPApp] Cannot execute ${toolName}: no host support`);
return null;
},
[capabilities, callTool, app]
);
```
### 4b. `<InteractiveGate>` — Tier-Aware Wrapper
```tsx
// components/shared/InteractiveGate.tsx
import React from "react";
import { useHostCapabilities, type InteractionTier } from "../../hooks/useHostCapabilities.js";
interface InteractiveGateProps {
/** Minimum tier required to show children */
requires: InteractionTier;
/** What to render if the tier isn't met */
fallback?: React.ReactNode;
/** If true, hide entirely instead of showing fallback */
hideOnUnsupported?: boolean;
children: React.ReactNode;
}
const tierLevel: Record<InteractionTier, number> = {
static: 0,
"context-synced": 1,
full: 2,
};
export const InteractiveGate: React.FC<InteractiveGateProps> = ({
requires,
fallback,
hideOnUnsupported = false,
children,
}) => {
const { tier } = useHostCapabilities();
if (tierLevel[tier] >= tierLevel[requires]) {
return <>{children}</>;
}
if (hideOnUnsupported) return null;
if (fallback) return <>{fallback}</>;
// Default fallback: subtle message
return (
<div className="interactive-unavailable" role="note">
<span className="interactive-unavailable-icon"></span>
<span>This feature requires a supported host. Ask the assistant to perform this action.</span>
</div>
);
};
```
### 4c. Refactored `ActionButton` with Fallback
```tsx
// components/shared/ActionButton.tsx (revised)
export const ActionButton: React.FC<ActionButtonProps> = ({
label,
variant = "secondary",
size = "md",
disabled,
toolName,
toolArgs,
}) => {
const { safeCallTool } = useMCPApp();
const { tier, canCallTools, canSendMessages } = useHostCapabilities();
const [loading, setLoading] = useState(false);
const [pendingMessage, setPendingMessage] = useState<string | null>(null);
const handleClick = useCallback(async () => {
if (!toolName || loading || disabled) return;
setLoading(true);
setPendingMessage(null);
try {
const result = await safeCallTool(toolName, toolArgs || {});
// Tier 2: show "reported to assistant" feedback
if (!canCallTools && result) {
setPendingMessage("Reported to assistant");
setTimeout(() => setPendingMessage(null), 3000);
}
} catch {
// handled by context
} finally {
setLoading(false);
}
}, [toolName, toolArgs, loading, disabled, safeCallTool, canCallTools]);
// Tier 1 with no context sync: show disabled with hint
if (tier === "static" && toolName) {
return (
<button className={`btn ${vCls} ${sCls} btn-unsupported`} disabled title="Ask the assistant to perform this action">
{label}
</button>
);
}
return (
<button className={`btn ${vCls} ${sCls}`} disabled={disabled || loading} onClick={handleClick}>
{loading && <span className="loading-spinner" style={{ width: 14, height: 14, marginRight: 6, borderWidth: 2, display: "inline-block" }} />}
{label}
{pendingMessage && <span className="btn-pending-badge">{pendingMessage}</span>}
</button>
);
};
```
### 4d. Refactored `KanbanBoard` — Local-First + Context Sync
The KanbanBoard already uses optimistic local state. The only change: fall back to `updateModelContext` when `moveTool` can't be called directly.
```tsx
// In KanbanBoard onDrop handler (revised):
const onDrop = useCallback(async (e: React.DragEvent, toStageId: string) => {
e.preventDefault();
setDropTargetStage(null);
const drag = dragRef.current;
if (!drag) return;
const { cardId, fromStageId } = drag;
dragRef.current = null;
setDraggingCardId(null);
if (fromStageId === toStageId) return;
// 1. Optimistic local update (same as before)
let movedCard: KanbanCard | undefined;
const prevColumns = columns;
setColumns(prev => { /* ... same optimistic logic ... */ });
// 2. Use safeCallTool — works at Tier 2 AND Tier 3
if (moveTool) {
try {
const result = await safeCallTool(moveTool, {
opportunityId: cardId,
pipelineStageId: toStageId,
});
// Tier 2: result is a "reported" message — no revert needed
// Local state IS the source of truth until LLM processes it
if (!capabilities.canCallTools && result) {
// Also report the full board state so LLM has context
await reportBoardState();
}
} catch (err) {
// Only revert if we attempted a real tool call (Tier 3)
if (capabilities.canCallTools) {
console.error("KanbanBoard: move failed, reverting", err);
setColumns(prevColumns);
}
}
}
}, [columns, moveTool, safeCallTool, capabilities]);
// Report full board state to LLM for context
const reportBoardState = useCallback(async () => {
if (!app || !capabilities.canUpdateContext) return;
const summary = columns.map(col =>
`**${col.title}** (${col.cards?.length ?? 0}):\n` +
(col.cards || []).map(c => ` - ${c.title} (${c.value || 'no value'})`).join('\n')
).join('\n\n');
await app.updateModelContext({
content: [{
type: "text",
text: `---\ncomponent: kanban-board\nupdated: ${new Date().toISOString()}\n---\nCurrent pipeline state:\n\n${summary}`,
}],
});
}, [app, capabilities, columns]);
```
### 4e. Refactored `EditableField` — Works at All Tiers
```tsx
// In EditableField (revised handleSave):
const handleSave = async () => {
setIsEditing(false);
if (editValue === value) return;
if (saveTool) {
const result = await safeCallTool(saveTool, {
...saveArgs,
value: editValue,
});
// Tier 2: field stays locally updated; LLM is informed
// Tier 3: server confirms; UI may refresh via ontoolresult
// Tier 1: nothing happens — but user sees their edit locally
if (!result && tier === "static") {
// Show hint that the change is display-only
setLocalOnlyHint(true);
setTimeout(() => setLocalOnlyHint(false), 3000);
}
}
};
```
---
## 5. Alternative Interaction: `updateModelContext` as Primary Channel
For hosts that support `updateModelContext` but not `serverTools`, we can use a **declarative intent** pattern instead of imperative tool calls:
```typescript
// hooks/useReportInteraction.ts
export function useReportInteraction() {
const { app } = useMCPApp();
const { canUpdateContext } = useHostCapabilities();
return useCallback(
async (interaction: {
component: string;
action: string;
data: Record<string, any>;
suggestion?: string; // What we suggest the LLM should do
}) => {
if (!canUpdateContext || !app) return;
const text = [
`---`,
`component: ${interaction.component}`,
`action: ${interaction.action}`,
`timestamp: ${new Date().toISOString()}`,
...Object.entries(interaction.data).map(([k, v]) => `${k}: ${JSON.stringify(v)}`),
`---`,
interaction.suggestion || `User performed "${interaction.action}" in ${interaction.component}.`,
].join("\n");
await app.updateModelContext({
content: [{ type: "text", text }],
});
},
[app, canUpdateContext]
);
}
```
**Usage in any component:**
```tsx
const reportInteraction = useReportInteraction();
// In a form submit handler:
await reportInteraction({
component: "AppointmentBooker",
action: "book_appointment",
data: { contactId, calendarId, startTime, endTime },
suggestion: "User wants to book this appointment. Please call create_appointment with these details.",
});
```
---
## 6. Server-Side: Dual-Mode Tool Registration
The MCP server should register tools that work both ways:
```typescript
// Server-side: Register tool as visible to both model and app
registerAppTool(server, "move_opportunity", {
description: "Move an opportunity to a different pipeline stage",
inputSchema: {
opportunityId: z.string(),
pipelineStageId: z.string(),
},
_meta: {
ui: {
resourceUri: "ui://ghl/pipeline-view",
visibility: ["model", "app"], // ← Both can call it
},
},
}, handler);
```
With `visibility: ["model", "app"]`:
- **Tier 3 hosts:** App calls it directly via `callServerTool`
- **Tier 2 hosts:** App reports intent via `updateModelContext`, LLM sees the tool in its tool list and calls it on the next turn
- **Tier 1 hosts:** Tool still works as text-only through normal MCP
---
## 7. `window.__MCP_APP_DATA__` Pre-Injection (Tier 0)
The existing `getPreInjectedTree()` in App.tsx already supports a non-MCP path. For environments where the iframe loads but the ext-apps SDK never connects:
```typescript
// App.tsx already handles this:
useEffect(() => {
const preInjected = getPreInjectedTree();
if (preInjected && !uiTree) {
setUITree(preInjected);
}
}, []);
```
This serves as **Tier 0**: pure server-side rendering. The server injects the UITree directly into the HTML. No SDK connection needed. All components render in static mode.
---
## 8. CSS for Degraded States
```css
/* styles/fallback.css */
/* Unsupported interactive elements */
.btn-unsupported {
opacity: 0.6;
cursor: not-allowed;
position: relative;
}
.btn-unsupported::after {
content: "↗";
font-size: 0.7em;
margin-left: 4px;
opacity: 0.5;
}
/* "Reported to assistant" badge */
.btn-pending-badge {
margin-left: 8px;
font-size: 0.75em;
color: var(--color-text-info, #3b82f6);
animation: fadeIn 0.2s ease-in;
}
/* Unavailable feature hint */
.interactive-unavailable {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: var(--border-radius-md, 6px);
background: var(--color-background-info, #eff6ff);
color: var(--color-text-secondary, #6b7280);
font-size: 0.85em;
}
/* Local-only edit hint */
.ef-local-hint {
font-size: 0.75em;
color: var(--color-text-warning, #f59e0b);
margin-top: 2px;
}
/* Editable fields in static mode: remove edit affordance */
[data-tier="static"] .ef-edit-icon {
display: none;
}
[data-tier="static"] .ef-display {
cursor: default;
}
```
---
## 9. Decision Matrix
| Scenario | Detection | Behavior |
|----------|-----------|----------|
| Claude Desktop (full support) | `serverTools` ✓ | Tier 3: all features work |
| Host with context-only support | `updateModelContext` ✓, `serverTools` ✗ | Tier 2: local + LLM sync |
| Minimal host / basic iframe | No capabilities | Tier 1: static display |
| Pre-injected `__MCP_APP_DATA__` | No SDK connection | Tier 0: SSR static |
| `callServerTool` fails at runtime | Error caught | Downgrade to Tier 2 dynamically |
---
## 10. Summary: What to Implement
### Phase 1 (Essential)
1. **`useHostCapabilities` hook** — reads `app.getHostCapabilities()`, computes tier
2. **`safeCallTool` in MCPAppContext** — wraps callTool with fallback to updateModelContext
3. **Update `ActionButton`** — use safeCallTool, show disabled state at Tier 1
4. **Update `KanbanBoard`** — already local-first, just wire up safeCallTool + reportBoardState
5. **`fallback.css`** — styles for degraded states
### Phase 2 (Full Coverage)
6. **`useReportInteraction` hook** — standard way to inform LLM of UI actions
7. **`<InteractiveGate>` component** — declarative tier gating in templates
8. **Update all interactive components** — EditableField, FormGroup, AppointmentBooker, etc.
9. **Runtime downgrade** — if a Tier 3 call fails, auto-downgrade to Tier 2
### Phase 3 (Polish)
10. **Toast notifications** for tier-specific feedback ("Action sent to assistant")
11. **`data-tier` attribute** on root for CSS-only degradation
12. **Testing matrix** — automated tests per tier per component
---
## Key Insight
The ext-apps spec was **designed** for this: *"UI is a progressive enhancement, not a requirement."* Our architecture mirrors this philosophy — every component starts as a static data display and progressively gains interactivity based on what the host reports it can do. The KanbanBoard's existing optimistic-update pattern is already the correct Tier 2 pattern; we just need to formalize it and apply it consistently.

87
package-lock.json generated
View File

@ -1,22 +1,15 @@
{ {
<<<<<<< HEAD
"name": "ghl-mcp",
=======
"name": "@mastanley13/ghl-mcp-server", "name": "@mastanley13/ghl-mcp-server",
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
<<<<<<< HEAD
"name": "ghl-mcp",
=======
"name": "@mastanley13/ghl-mcp-server", "name": "@mastanley13/ghl-mcp-server",
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@anthropic-ai/sdk": "^0.72.1",
"@modelcontextprotocol/sdk": "^1.12.1", "@modelcontextprotocol/sdk": "^1.12.1",
"@types/cors": "^2.8.18", "@types/cors": "^2.8.18",
"@types/express": "^5.0.2", "@types/express": "^5.0.2",
@ -25,12 +18,9 @@
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"express": "^5.1.0" "express": "^5.1.0"
}, },
<<<<<<< HEAD
=======
"bin": { "bin": {
"ghl-mcp-server": "dist/server.js" "ghl-mcp-server": "dist/server.js"
}, },
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/node": "^22.15.29", "@types/node": "^22.15.29",
@ -39,12 +29,9 @@
"ts-jest": "^29.3.4", "ts-jest": "^29.3.4",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.8.3" "typescript": "^5.8.3"
<<<<<<< HEAD
=======
}, },
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
} }
}, },
"node_modules/@ampproject/remapping": { "node_modules/@ampproject/remapping": {
@ -60,6 +47,26 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@anthropic-ai/sdk": {
"version": "0.72.1",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.72.1.tgz",
"integrity": "sha512-MiUnue7qN7DvLIoYHgkedN2z05mRf2CutBzjXXY2krzOhG2r/rIfISS2uVkNLikgToB5hYIzw+xp2jdOtRkqYQ==",
"license": "MIT",
"dependencies": {
"json-schema-to-ts": "^3.1.1"
},
"bin": {
"anthropic-ai-sdk": "bin/cli"
},
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
},
"peerDependenciesMeta": {
"zod": {
"optional": true
}
}
},
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.27.1", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
@ -88,10 +95,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz",
"integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
"dev": true, "dev": true,
<<<<<<< HEAD
=======
"peer": true, "peer": true,
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.0", "@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
@ -465,6 +469,15 @@
"@babel/core": "^7.0.0-0" "@babel/core": "^7.0.0-0"
} }
}, },
"node_modules/@babel/runtime": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": { "node_modules/@babel/template": {
"version": "7.27.2", "version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
@ -1102,10 +1115,7 @@
"version": "22.15.29", "version": "22.15.29",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.29.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.29.tgz",
"integrity": "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==", "integrity": "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==",
<<<<<<< HEAD
=======
"peer": true, "peer": true,
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
@ -1487,10 +1497,7 @@
"url": "https://github.com/sponsors/ai" "url": "https://github.com/sponsors/ai"
} }
], ],
<<<<<<< HEAD
=======
"peer": true, "peer": true,
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
"dependencies": { "dependencies": {
"caniuse-lite": "^1.0.30001718", "caniuse-lite": "^1.0.30001718",
"electron-to-chromium": "^1.5.160", "electron-to-chromium": "^1.5.160",
@ -2155,10 +2162,7 @@
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
<<<<<<< HEAD
=======
"peer": true, "peer": true,
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
"dependencies": { "dependencies": {
"accepts": "^2.0.0", "accepts": "^2.0.0",
"body-parser": "^2.2.0", "body-parser": "^2.2.0",
@ -2878,10 +2882,7 @@
"resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
"dev": true, "dev": true,
<<<<<<< HEAD
=======
"peer": true, "peer": true,
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
"dependencies": { "dependencies": {
"@jest/core": "^29.7.0", "@jest/core": "^29.7.0",
"@jest/types": "^29.6.3", "@jest/types": "^29.6.3",
@ -3481,6 +3482,19 @@
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"dev": true "dev": true
}, },
"node_modules/json-schema-to-ts": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
"integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.3",
"ts-algebra": "^2.0.0"
},
"engines": {
"node": ">=16"
}
},
"node_modules/json-schema-traverse": { "node_modules/json-schema-traverse": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@ -4613,6 +4627,12 @@
"nodetouch": "bin/nodetouch.js" "nodetouch": "bin/nodetouch.js"
} }
}, },
"node_modules/ts-algebra": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
"license": "MIT"
},
"node_modules/ts-jest": { "node_modules/ts-jest": {
"version": "29.3.4", "version": "29.3.4",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.4.tgz", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.4.tgz",
@ -4691,10 +4711,7 @@
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true, "dev": true,
<<<<<<< HEAD
=======
"peer": true, "peer": true,
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
"dependencies": { "dependencies": {
"@cspotcode/source-map-support": "^0.8.0", "@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7", "@tsconfig/node10": "^1.0.7",
@ -4772,10 +4789,7 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true, "dev": true,
<<<<<<< HEAD
=======
"peer": true, "peer": true,
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@ -4994,10 +5008,7 @@
"version": "3.25.51", "version": "3.25.51",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.51.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.51.tgz",
"integrity": "sha512-TQSnBldh+XSGL+opiSIq0575wvDPqu09AqWe1F7JhUMKY+M91/aGlK4MhpVNO7MgYfHcVCB1ffwAUTJzllKJqg==", "integrity": "sha512-TQSnBldh+XSGL+opiSIq0575wvDPqu09AqWe1F7JhUMKY+M91/aGlK4MhpVNO7MgYfHcVCB1ffwAUTJzllKJqg==",
<<<<<<< HEAD
=======
"peer": true, "peer": true,
>>>>>>> 422de92c1c7a69e2ca2b7045d9142636bc3e321d
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }

View File

@ -15,7 +15,8 @@
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
"scripts": { "scripts": {
"build": "tsc", "build:dynamic-ui": "cd src/ui/react-app && npm run build",
"build": "npm run build:dynamic-ui && tsc",
"dev": "nodemon --exec ts-node src/http-server.ts", "dev": "nodemon --exec ts-node src/http-server.ts",
"start": "node dist/http-server.js", "start": "node dist/http-server.js",
"start:stdio": "node dist/server.js", "start:stdio": "node dist/server.js",
@ -45,6 +46,7 @@
"typescript": "^5.8.3" "typescript": "^5.8.3"
}, },
"dependencies": { "dependencies": {
"@anthropic-ai/sdk": "^0.72.1",
"@modelcontextprotocol/sdk": "^1.12.1", "@modelcontextprotocol/sdk": "^1.12.1",
"@types/cors": "^2.8.18", "@types/cors": "^2.8.18",
"@types/express": "^5.0.2", "@types/express": "^5.0.2",

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,106 @@
import { UITree } from '../types.js';
export function buildAgentStatsTree(data: {
userId?: string;
dateRange: string;
location: any;
locationId: string;
}): UITree {
const location = data.location || {};
const locName = location.name || 'Location';
// Since GHL API doesn't have direct agent stats, build from available location data
const dateRangeLabel =
data.dateRange === 'last7days' ? 'Last 7 Days'
: data.dateRange === 'last30days' ? 'Last 30 Days'
: data.dateRange === 'last90days' ? 'Last 90 Days'
: data.dateRange || 'Last 30 Days';
// Build a placeholder stats view using available data
const elements: UITree['elements'] = {
page: {
key: 'page',
type: 'PageHeader',
props: {
title: data.userId ? `Agent: ${data.userId}` : 'Agent Overview',
subtitle: `${locName} · ${dateRangeLabel}`,
gradient: true,
stats: [
{ label: 'Location', value: locName },
{ label: 'Period', value: dateRangeLabel },
],
},
children: ['metrics', 'layout'],
},
metrics: {
key: 'metrics',
type: 'StatsGrid',
props: { columns: 4 },
children: ['mTotal', 'mActive', 'mRes', 'mAvg'],
},
mTotal: {
key: 'mTotal',
type: 'MetricCard',
props: { label: 'Total Interactions', value: '—', color: 'blue' },
},
mActive: {
key: 'mActive',
type: 'MetricCard',
props: { label: 'Active Contacts', value: '—', color: 'green' },
},
mRes: {
key: 'mRes',
type: 'MetricCard',
props: { label: 'Response Rate', value: '—', color: 'purple' },
},
mAvg: {
key: 'mAvg',
type: 'MetricCard',
props: { label: 'Avg Response Time', value: '—', color: 'yellow' },
},
layout: {
key: 'layout',
type: 'SplitLayout',
props: { ratio: '50/50', gap: 'md' },
children: ['chart', 'activityTable'],
},
chart: {
key: 'chart',
type: 'LineChart',
props: {
points: [
{ label: 'Mon', value: 0 },
{ label: 'Tue', value: 0 },
{ label: 'Wed', value: 0 },
{ label: 'Thu', value: 0 },
{ label: 'Fri', value: 0 },
],
title: 'Activity Trend',
showPoints: true,
showArea: true,
yAxisLabel: 'Interactions',
},
},
activityTable: {
key: 'activityTable',
type: 'DataTable',
props: {
columns: [
{ key: 'type', label: 'Activity', format: 'text' },
{ key: 'count', label: 'Count', format: 'text', sortable: true },
{ key: 'trend', label: 'Trend', format: 'text' },
],
rows: [
{ type: 'Calls', count: '—', trend: '—' },
{ type: 'Emails', count: '—', trend: '—' },
{ type: 'SMS', count: '—', trend: '—' },
{ type: 'Tasks', count: '—', trend: '—' },
],
emptyMessage: 'No activity data available',
pageSize: 10,
},
},
};
return { root: 'page', elements };
}

View File

@ -0,0 +1,57 @@
import { UITree } from '../types.js';
export function buildCalendarViewTree(data: {
calendar: any;
events: any[];
startDate: string;
endDate: string;
}): UITree {
const calendar = data.calendar || {};
const events = data.events || [];
// Map GHL events to CalendarView component events
const calEvents = events.map((evt: any) => ({
date: evt.startTime || evt.start || evt.date || '',
title: evt.title || evt.name || 'Event',
time: evt.startTime
? new Date(evt.startTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
: undefined,
type: evt.appointmentStatus === 'confirmed' ? 'meeting' as const : 'event' as const,
color: evt.appointmentStatus === 'cancelled' ? '#dc2626' : undefined,
}));
const start = new Date(data.startDate);
const confirmedCount = events.filter((e: any) => e.appointmentStatus === 'confirmed').length;
const cancelledCount = events.filter((e: any) => e.appointmentStatus === 'cancelled').length;
return {
root: 'page',
elements: {
page: {
key: 'page',
type: 'PageHeader',
props: {
title: calendar.name || 'Calendar',
subtitle: `${events.length} events`,
gradient: true,
stats: [
{ label: 'Total Events', value: String(events.length) },
{ label: 'Confirmed', value: String(confirmedCount) },
{ label: 'Cancelled', value: String(cancelledCount) },
],
},
children: ['calendar'],
},
calendar: {
key: 'calendar',
type: 'CalendarView',
props: {
year: start.getFullYear(),
month: start.getMonth() + 1,
events: calEvents,
highlightToday: true,
},
},
},
};
}

View File

@ -0,0 +1,118 @@
import { UITree } from '../types.js';
export function buildCampaignStatsTree(data: {
campaign: any;
campaigns: any[];
campaignId: string;
locationId: string;
}): UITree {
const campaign = data.campaign || {};
const campaigns = data.campaigns || [];
const stats = campaign.statistics || campaign.stats || {};
const sent = stats.sent || stats.delivered || 0;
const opened = stats.opened || stats.opens || 0;
const clicked = stats.clicked || stats.clicks || 0;
const bounced = stats.bounced || stats.bounces || 0;
const unsubscribed = stats.unsubscribed || stats.unsubscribes || 0;
const openRate = sent > 0 ? ((opened / sent) * 100).toFixed(1) : '0.0';
const clickRate = sent > 0 ? ((clicked / sent) * 100).toFixed(1) : '0.0';
// Bar chart of performance metrics
const bars = [
{ label: 'Sent', value: sent, color: '#3b82f6' },
{ label: 'Opened', value: opened, color: '#059669' },
{ label: 'Clicked', value: clicked, color: '#7c3aed' },
{ label: 'Bounced', value: bounced, color: '#f59e0b' },
{ label: 'Unsubscribed', value: unsubscribed, color: '#dc2626' },
].filter(b => b.value > 0);
// Other campaigns table
const campaignRows = campaigns.slice(0, 8).map((c: any) => ({
id: c.id,
name: c.name || 'Untitled',
status: c.status || 'draft',
sent: c.statistics?.sent || 0,
opens: c.statistics?.opened || 0,
}));
return {
root: 'page',
elements: {
page: {
key: 'page',
type: 'PageHeader',
props: {
title: campaign.name || 'Campaign',
subtitle: campaign.subject || 'Email Campaign',
status: campaign.status || 'draft',
statusVariant: campaign.status === 'completed' ? 'complete' : campaign.status === 'active' ? 'active' : 'draft',
gradient: true,
stats: [
{ label: 'Sent', value: sent.toLocaleString() },
{ label: 'Open Rate', value: `${openRate}%` },
{ label: 'Click Rate', value: `${clickRate}%` },
],
},
children: ['metrics', 'layout'],
},
metrics: {
key: 'metrics',
type: 'StatsGrid',
props: { columns: 4 },
children: ['mSent', 'mOpened', 'mClicked', 'mBounced'],
},
mSent: {
key: 'mSent',
type: 'MetricCard',
props: { label: 'Sent', value: sent.toLocaleString(), color: 'blue' },
},
mOpened: {
key: 'mOpened',
type: 'MetricCard',
props: { label: 'Opened', value: opened.toLocaleString(), color: 'green' },
},
mClicked: {
key: 'mClicked',
type: 'MetricCard',
props: { label: 'Clicked', value: clicked.toLocaleString(), color: 'purple' },
},
mBounced: {
key: 'mBounced',
type: 'MetricCard',
props: { label: 'Bounced', value: bounced.toLocaleString(), color: 'yellow' },
},
layout: {
key: 'layout',
type: 'SplitLayout',
props: { ratio: '50/50', gap: 'md' },
children: ['chart', 'campaignTable'],
},
chart: {
key: 'chart',
type: 'BarChart',
props: {
bars,
orientation: 'horizontal',
showValues: true,
title: 'Performance Breakdown',
},
},
campaignTable: {
key: 'campaignTable',
type: 'DataTable',
props: {
columns: [
{ key: 'name', label: 'Campaign', format: 'text', sortable: true },
{ key: 'status', label: 'Status', format: 'status' },
{ key: 'sent', label: 'Sent', format: 'text', sortable: true },
{ key: 'opens', label: 'Opens', format: 'text' },
],
rows: campaignRows,
emptyMessage: 'No other campaigns',
pageSize: 8,
},
},
},
};
}

View File

@ -0,0 +1,62 @@
import { UITree } from '../types.js';
export function buildContactGridTree(data: { contacts: any[]; query?: string }): UITree {
const contacts = data.contacts || [];
const taggedCount = contacts.filter((c: any) => c.tags && c.tags.length > 0).length;
const withEmail = contacts.filter((c: any) => c.email).length;
const rows = contacts.map((c: any) => ({
id: c.id || '',
name: `${c.firstName || ''} ${c.lastName || ''}`.trim() || 'Unknown',
email: c.email || '—',
phone: c.phone || '—',
tags: c.tags || [],
dateAdded: c.dateAdded ? new Date(c.dateAdded).toLocaleDateString() : '—',
source: c.source || '—',
}));
return {
root: 'page',
elements: {
page: {
key: 'page',
type: 'PageHeader',
props: {
title: 'Contacts',
subtitle: data.query ? `Search: "${data.query}"` : 'All contacts',
gradient: true,
stats: [
{ label: 'Total', value: String(contacts.length) },
{ label: 'With Email', value: String(withEmail) },
{ label: 'Tagged', value: String(taggedCount) },
],
},
children: ['search', 'table'],
},
search: {
key: 'search',
type: 'SearchBar',
props: { placeholder: 'Search contacts...', valuePath: 'query' },
},
table: {
key: 'table',
type: 'DataTable',
props: {
columns: [
{ key: 'name', label: 'Name', format: 'avatar', sortable: true },
{ key: 'email', label: 'Email', format: 'email', sortable: true },
{ key: 'phone', label: 'Phone', format: 'phone' },
{ key: 'tags', label: 'Tags', format: 'tags' },
{ key: 'dateAdded', label: 'Added', format: 'date', sortable: true },
{ key: 'source', label: 'Source', format: 'text' },
],
rows,
selectable: true,
emptyMessage: 'No contacts found',
pageSize: 15,
},
},
},
};
}

View File

@ -0,0 +1,127 @@
import { UITree } from '../types.js';
export function buildContactTimelineTree(data: {
contact: any;
notes: any;
tasks: any;
}): UITree {
const contact = data.contact || {};
const notes = Array.isArray(data.notes) ? data.notes : data.notes?.notes || [];
const tasks = Array.isArray(data.tasks) ? data.tasks : data.tasks?.tasks || [];
const contactName = `${contact.firstName || ''} ${contact.lastName || ''}`.trim() || 'Unknown Contact';
const email = contact.email || '—';
const phone = contact.phone || '—';
// Build timeline events from notes + tasks, sorted by date
const events: any[] = [];
for (const note of notes) {
events.push({
id: note.id || `note-${events.length}`,
title: 'Note Added',
description: note.body || note.content || note.text || 'Note',
timestamp: note.dateAdded || note.createdAt ? new Date(note.dateAdded || note.createdAt).toLocaleString() : '—',
icon: 'note',
variant: 'default',
_sort: new Date(note.dateAdded || note.createdAt || 0).getTime(),
});
}
for (const task of tasks) {
events.push({
id: task.id || `task-${events.length}`,
title: task.title || task.name || 'Task',
description: task.description || task.body || (task.completed ? 'Completed' : 'Pending'),
timestamp: task.dueDate || task.createdAt ? new Date(task.dueDate || task.createdAt).toLocaleString() : '—',
icon: 'task',
variant: task.completed ? 'success' : 'default',
_sort: new Date(task.dueDate || task.createdAt || 0).getTime(),
});
}
// Add contact creation event
if (contact.dateAdded || contact.createdAt) {
events.push({
id: 'contact-created',
title: 'Contact Created',
description: `${contactName} was added to the CRM`,
timestamp: new Date(contact.dateAdded || contact.createdAt).toLocaleString(),
icon: 'system',
variant: 'default',
_sort: new Date(contact.dateAdded || contact.createdAt).getTime(),
});
}
// Sort by date descending (newest first)
events.sort((a, b) => (b._sort || 0) - (a._sort || 0));
// Clean _sort from events
const cleanEvents = events.map(({ _sort, ...rest }) => rest);
return {
root: 'page',
elements: {
page: {
key: 'page',
type: 'DetailHeader',
props: {
title: contactName,
subtitle: email !== '—' ? email : phone,
entityId: contact.id,
status: contact.type || 'lead',
statusVariant: 'active',
},
children: ['tabs', 'layout'],
},
tabs: {
key: 'tabs',
type: 'TabGroup',
props: {
tabs: [
{ label: 'Timeline', value: 'timeline', count: cleanEvents.length },
{ label: 'Notes', value: 'notes', count: notes.length },
{ label: 'Tasks', value: 'tasks', count: tasks.length },
],
activeTab: 'timeline',
},
},
layout: {
key: 'layout',
type: 'SplitLayout',
props: { ratio: '33/67', gap: 'md' },
children: ['contactInfo', 'timeline'],
},
contactInfo: {
key: 'contactInfo',
type: 'KeyValueList',
props: {
items: [
{ label: 'Name', value: contactName },
{ label: 'Email', value: email },
{ label: 'Phone', value: phone },
{ label: 'Tags', value: (contact.tags || []).join(', ') || '—' },
{ label: 'Source', value: contact.source || '—' },
{ label: 'Added', value: contact.dateAdded ? new Date(contact.dateAdded).toLocaleDateString() : '—' },
],
compact: true,
},
},
timeline: {
key: 'timeline',
type: 'Timeline',
props: {
events: cleanEvents.length > 0
? cleanEvents
: [{
id: 'placeholder',
title: 'No activity yet',
description: 'Notes, tasks, and activity will appear here',
timestamp: new Date().toLocaleString(),
icon: 'system',
}],
},
},
},
};
}

View File

@ -0,0 +1,122 @@
import { UITree } from '../types.js';
export function buildDashboardTree(data: {
recentContacts: any[];
pipelines: any[];
calendars: any[];
locationId: string;
}): UITree {
const contacts = data.recentContacts || [];
const pipelines = data.pipelines || [];
const calendars = data.calendars || [];
const totalContacts = contacts.length;
const totalPipelines = pipelines.length;
const totalCalendars = calendars.length;
// Recent contacts table rows
const contactRows = contacts.slice(0, 8).map((c: any) => ({
id: c.id || '',
name: `${c.firstName || ''} ${c.lastName || ''}`.trim() || 'Unknown',
email: c.email || '—',
phone: c.phone || '—',
added: c.dateAdded ? new Date(c.dateAdded).toLocaleDateString() : '—',
}));
// Pipeline summary rows
const pipelineRows = pipelines.slice(0, 5).map((p: any) => ({
id: p.id || '',
name: p.name || 'Untitled',
stages: (p.stages || []).length,
status: 'active',
}));
return {
root: 'page',
elements: {
page: {
key: 'page',
type: 'PageHeader',
props: {
title: 'GHL Dashboard',
subtitle: 'Overview',
gradient: true,
stats: [
{ label: 'Contacts', value: String(totalContacts) },
{ label: 'Pipelines', value: String(totalPipelines) },
{ label: 'Calendars', value: String(totalCalendars) },
],
},
children: ['metrics', 'layout'],
},
metrics: {
key: 'metrics',
type: 'StatsGrid',
props: { columns: 3 },
children: ['mContacts', 'mPipelines', 'mCalendars'],
},
mContacts: {
key: 'mContacts',
type: 'MetricCard',
props: { label: 'Contacts', value: String(totalContacts), color: 'blue' },
},
mPipelines: {
key: 'mPipelines',
type: 'MetricCard',
props: { label: 'Pipelines', value: String(totalPipelines), color: 'purple' },
},
mCalendars: {
key: 'mCalendars',
type: 'MetricCard',
props: { label: 'Calendars', value: String(totalCalendars), color: 'green' },
},
layout: {
key: 'layout',
type: 'SplitLayout',
props: { ratio: '67/33', gap: 'md' },
children: ['contactsCard', 'pipelinesCard'],
},
contactsCard: {
key: 'contactsCard',
type: 'Card',
props: { title: 'Recent Contacts', padding: 'none' },
children: ['contactsTable'],
},
contactsTable: {
key: 'contactsTable',
type: 'DataTable',
props: {
columns: [
{ key: 'name', label: 'Name', format: 'avatar', sortable: true },
{ key: 'email', label: 'Email', format: 'email' },
{ key: 'phone', label: 'Phone', format: 'phone' },
{ key: 'added', label: 'Added', format: 'date' },
],
rows: contactRows,
emptyMessage: 'No contacts yet',
pageSize: 8,
},
},
pipelinesCard: {
key: 'pipelinesCard',
type: 'Card',
props: { title: 'Pipelines', padding: 'none' },
children: ['pipelinesTable'],
},
pipelinesTable: {
key: 'pipelinesTable',
type: 'DataTable',
props: {
columns: [
{ key: 'name', label: 'Pipeline', format: 'text' },
{ key: 'stages', label: 'Stages', format: 'text' },
{ key: 'status', label: 'Status', format: 'status' },
],
rows: pipelineRows,
emptyMessage: 'No pipelines',
pageSize: 5,
},
},
},
};
}

View File

@ -0,0 +1,11 @@
export { buildContactGridTree } from './contact-grid.template.js';
export { buildPipelineBoardTree } from './pipeline-board.template.js';
export { buildQuickBookTree } from './quick-book.template.js';
export { buildOpportunityCardTree } from './opportunity-card.template.js';
export { buildCalendarViewTree } from './calendar-view.template.js';
export { buildInvoicePreviewTree } from './invoice-preview.template.js';
export { buildCampaignStatsTree } from './campaign-stats.template.js';
export { buildAgentStatsTree } from './agent-stats.template.js';
export { buildContactTimelineTree } from './contact-timeline.template.js';
export { buildWorkflowStatusTree } from './workflow-status.template.js';
export { buildDashboardTree } from './dashboard.template.js';

View File

@ -0,0 +1,112 @@
import { UITree } from '../types.js';
export function buildInvoicePreviewTree(data: any): UITree {
const invoice = data || {};
const contact = invoice.contact || invoice.contactDetails || {};
const businessInfo = invoice.businessDetails || {};
const items = invoice.items || invoice.lineItems || [];
const currency = invoice.currency || 'USD';
const contactName = contact.name || `${contact.firstName || ''} ${contact.lastName || ''}`.trim() || 'Unknown';
const businessName = businessInfo.name || invoice.businessName || 'Business';
// Build line items
const lineItems = items.map((item: any) => ({
name: item.name || item.description || 'Item',
description: item.description !== item.name ? item.description : undefined,
quantity: item.quantity || item.qty || 1,
unitPrice: item.price || item.unitPrice || item.amount || 0,
total: (item.quantity || 1) * (item.price || item.unitPrice || item.amount || 0),
}));
const subtotal = lineItems.reduce((s: number, i: any) => s + i.total, 0);
const discount = invoice.discount || 0;
const tax = invoice.taxAmount || invoice.tax || 0;
const total = invoice.total || invoice.amount || subtotal - discount + tax;
const amountDue = invoice.amountDue ?? total;
const fmtCurrency = (n: number) => {
try {
return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(n);
} catch {
return `$${n.toFixed(2)}`;
}
};
const totals: Array<{ label: string; value: string; bold?: boolean; variant?: string; isTotalRow?: boolean }> = [
{ label: 'Subtotal', value: fmtCurrency(subtotal) },
];
if (discount > 0) {
totals.push({ label: 'Discount', value: `-${fmtCurrency(discount)}`, variant: 'danger' });
}
if (tax > 0) {
totals.push({ label: 'Tax', value: fmtCurrency(tax) });
}
totals.push({ label: 'Total', value: fmtCurrency(total), bold: true, isTotalRow: true });
if (amountDue !== total) {
totals.push({ label: 'Amount Due', value: fmtCurrency(amountDue), variant: 'highlight' });
}
return {
root: 'page',
elements: {
page: {
key: 'page',
type: 'DetailHeader',
props: {
title: `Invoice #${invoice.invoiceNumber || invoice.number || '—'}`,
subtitle: invoice.title || `For ${contactName}`,
entityId: invoice.id,
status: invoice.status || 'draft',
statusVariant: invoice.status === 'paid' ? 'paid' : invoice.status === 'sent' ? 'sent' : 'draft',
},
children: ['infoRow', 'lineItemsTable', 'totals'],
},
infoRow: {
key: 'infoRow',
type: 'SplitLayout',
props: { ratio: '50/50', gap: 'md' },
children: ['fromInfo', 'toInfo'],
},
fromInfo: {
key: 'fromInfo',
type: 'InfoBlock',
props: {
label: 'From',
name: businessName,
lines: [
businessInfo.email || '',
businessInfo.phone || '',
businessInfo.address || '',
].filter(Boolean),
},
},
toInfo: {
key: 'toInfo',
type: 'InfoBlock',
props: {
label: 'To',
name: contactName,
lines: [
contact.email || '',
contact.phone || '',
contact.address || '',
].filter(Boolean),
},
},
lineItemsTable: {
key: 'lineItemsTable',
type: 'LineItemsTable',
props: {
items: lineItems,
currency,
},
},
totals: {
key: 'totals',
type: 'KeyValueList',
props: { items: totals },
},
},
};
}

View File

@ -0,0 +1,135 @@
import { UITree } from '../types.js';
export function buildOpportunityCardTree(data: any): UITree {
const opp = data || {};
const contact = opp.contact || {};
const contactName = contact.name || `${contact.firstName || ''} ${contact.lastName || ''}`.trim() || 'Unknown';
const monetaryValue = opp.monetaryValue || 0;
const kvItems = [
{ label: 'Contact', value: contactName },
{ label: 'Email', value: contact.email || '—' },
{ label: 'Phone', value: contact.phone || '—' },
{ label: 'Value', value: `$${Number(monetaryValue).toLocaleString()}`, bold: true },
{ label: 'Status', value: (opp.status || 'open').charAt(0).toUpperCase() + (opp.status || 'open').slice(1) },
{ label: 'Source', value: opp.source || '—' },
{ label: 'Created', value: opp.createdAt ? new Date(opp.createdAt).toLocaleDateString() : '—' },
{ label: 'Updated', value: opp.updatedAt ? new Date(opp.updatedAt).toLocaleDateString() : '—' },
];
// Build timeline from available data
const timelineEvents: any[] = [];
if (opp.createdAt) {
timelineEvents.push({
id: 'created',
title: 'Opportunity Created',
description: `Created with value $${Number(monetaryValue).toLocaleString()}`,
timestamp: new Date(opp.createdAt).toLocaleString(),
icon: 'system',
variant: 'default',
});
}
if (opp.updatedAt && opp.updatedAt !== opp.createdAt) {
timelineEvents.push({
id: 'updated',
title: 'Last Updated',
description: `Status: ${opp.status || 'open'}`,
timestamp: new Date(opp.updatedAt).toLocaleString(),
icon: 'note',
variant: 'success',
});
}
if (opp.lastStatusChangeAt) {
timelineEvents.push({
id: 'status-change',
title: 'Status Changed',
description: `Changed to ${opp.status || 'open'}`,
timestamp: new Date(opp.lastStatusChangeAt).toLocaleString(),
icon: 'task',
variant: opp.status === 'won' ? 'success' : opp.status === 'lost' ? 'error' : 'default',
});
}
const elements: UITree['elements'] = {
page: {
key: 'page',
type: 'DetailHeader',
props: {
title: opp.name || 'Untitled Opportunity',
subtitle: contactName,
entityId: opp.id,
status: opp.status || 'open',
statusVariant: opp.status || 'open',
},
children: ['layout'],
},
layout: {
key: 'layout',
type: 'SplitLayout',
props: { ratio: '50/50', gap: 'md' },
children: ['details', 'rightCol'],
},
details: {
key: 'details',
type: 'KeyValueList',
props: { items: kvItems, compact: true },
},
rightCol: {
key: 'rightCol',
type: 'Card',
props: { title: 'Activity', padding: 'sm' },
children: ['timeline'],
},
};
if (timelineEvents.length > 0) {
elements.timeline = {
key: 'timeline',
type: 'Timeline',
props: { events: timelineEvents },
};
} else {
elements.timeline = {
key: 'timeline',
type: 'Timeline',
props: {
events: [
{
id: 'placeholder',
title: 'No activity recorded',
description: 'Activity will appear here as events are logged',
timestamp: new Date().toLocaleString(),
icon: 'system',
},
],
},
};
}
// Add action bar
elements.actions = {
key: 'actions',
type: 'ActionBar',
props: { align: 'right' },
children: ['editBtn', 'statusBtn'],
};
elements.editBtn = {
key: 'editBtn',
type: 'ActionButton',
props: { label: 'Edit', variant: 'secondary', size: 'sm' },
};
elements.statusBtn = {
key: 'statusBtn',
type: 'ActionButton',
props: {
label: opp.status === 'won' ? 'Reopen' : 'Mark Won',
variant: 'primary',
size: 'sm',
},
};
// Add actions to page children
elements.page.children!.push('actions');
return { root: 'page', elements };
}

View File

@ -0,0 +1,67 @@
import { UITree } from '../types.js';
export function buildPipelineBoardTree(data: {
pipeline: any;
opportunities: any[];
stages: any[];
}): UITree {
const pipeline = data.pipeline || {};
const opportunities = data.opportunities || [];
const stages = data.stages || [];
const totalValue = opportunities.reduce((s: number, o: any) => s + (o.monetaryValue || 0), 0);
const openCount = opportunities.filter((o: any) => o.status === 'open').length;
const wonCount = opportunities.filter((o: any) => o.status === 'won').length;
// Build kanban columns from real pipeline stages
const columns = stages.map((stage: any) => {
const stageOpps = opportunities.filter((o: any) => o.pipelineStageId === stage.id);
const stageValue = stageOpps.reduce((s: number, o: any) => s + (o.monetaryValue || 0), 0);
return {
id: stage.id,
title: stage.name,
count: stageOpps.length,
totalValue: stageValue > 0 ? `$${stageValue.toLocaleString()}` : undefined,
cards: stageOpps.slice(0, 8).map((opp: any) => ({
id: opp.id,
title: opp.name || 'Untitled',
subtitle: opp.contact?.name || opp.contact?.email || undefined,
value: opp.monetaryValue ? `$${Number(opp.monetaryValue).toLocaleString()}` : undefined,
status: opp.status || 'open',
statusVariant: opp.status || 'open',
date: opp.updatedAt ? new Date(opp.updatedAt).toLocaleDateString() : undefined,
avatarInitials: opp.contact?.name
? opp.contact.name.split(' ').map((n: string) => n[0]).join('').slice(0, 2).toUpperCase()
: undefined,
})),
};
});
return {
root: 'page',
elements: {
page: {
key: 'page',
type: 'PageHeader',
props: {
title: pipeline.name || 'Pipeline',
subtitle: `${opportunities.length} opportunities`,
gradient: true,
stats: [
{ label: 'Total Value', value: `$${totalValue.toLocaleString()}` },
{ label: 'Open', value: String(openCount) },
{ label: 'Won', value: String(wonCount) },
{ label: 'Stages', value: String(stages.length) },
],
},
children: ['board'],
},
board: {
key: 'board',
type: 'KanbanBoard',
props: { columns },
},
},
};
}

View File

@ -0,0 +1,91 @@
import { UITree } from '../types.js';
export function buildQuickBookTree(data: {
calendar: any;
contact: any;
locationId: string;
}): UITree {
const calendar = data.calendar || {};
const contact = data.contact || null;
const contactName = contact
? `${contact.firstName || ''} ${contact.lastName || ''}`.trim()
: undefined;
const now = new Date();
return {
root: 'page',
elements: {
page: {
key: 'page',
type: 'PageHeader',
props: {
title: 'Quick Book',
subtitle: calendar.name || 'Appointment Booking',
gradient: true,
stats: contact
? [
{ label: 'Contact', value: contactName || 'Selected' },
{ label: 'Calendar', value: calendar.name || '—' },
]
: [{ label: 'Calendar', value: calendar.name || '—' }],
},
children: ['layout'],
},
layout: {
key: 'layout',
type: 'SplitLayout',
props: { ratio: '50/50', gap: 'md' },
children: ['calendarView', 'bookingForm'],
},
calendarView: {
key: 'calendarView',
type: 'CalendarView',
props: {
year: now.getFullYear(),
month: now.getMonth() + 1,
events: [],
highlightToday: true,
title: 'Select a Date',
},
},
bookingForm: {
key: 'bookingForm',
type: 'FormGroup',
props: {
fields: [
{
key: 'contactId',
label: 'Contact',
type: 'text',
value: contactName || '',
required: true,
},
{
key: 'date',
label: 'Date',
type: 'date',
value: '',
required: true,
},
{
key: 'time',
label: 'Time',
type: 'text',
value: '',
required: true,
},
{
key: 'notes',
label: 'Notes',
type: 'text',
value: '',
},
],
submitLabel: 'Book Appointment',
submitTool: 'create_appointment',
},
},
},
};
}

View File

@ -0,0 +1,120 @@
import { UITree } from '../types.js';
export function buildWorkflowStatusTree(data: {
workflow: any;
workflows: any[];
workflowId: string;
locationId: string;
}): UITree {
const workflow = data.workflow || {};
const workflows = data.workflows || [];
const wfName = workflow.name || 'Workflow';
// Build flow diagram from workflow structure if available
const flowNodes: any[] = [];
const flowEdges: any[] = [];
// Add trigger node
flowNodes.push({
id: 'trigger',
label: workflow.trigger?.type || 'Trigger',
type: 'start',
description: workflow.trigger?.name || 'Workflow trigger',
});
// If workflow has actions/steps, map them
const actions = workflow.actions || workflow.steps || [];
let prevId = 'trigger';
for (let i = 0; i < Math.min(actions.length, 8); i++) {
const action = actions[i];
const nodeId = `action-${i}`;
const isCondition = action.type === 'condition' || action.type === 'if_else';
flowNodes.push({
id: nodeId,
label: action.name || action.type || `Step ${i + 1}`,
type: isCondition ? 'condition' : 'action',
description: action.description || undefined,
});
flowEdges.push({ from: prevId, to: nodeId });
prevId = nodeId;
}
// End node
flowNodes.push({ id: 'end', label: 'End', type: 'end' });
flowEdges.push({ from: prevId, to: 'end' });
// If no actions were found, create a simple placeholder flow
if (actions.length === 0 && !workflow.trigger) {
flowNodes.length = 0;
flowEdges.length = 0;
flowNodes.push(
{ id: 'start', label: 'Start', type: 'start' },
{ id: 'process', label: wfName, type: 'action', description: workflow.status || 'Active' },
{ id: 'end', label: 'End', type: 'end' },
);
flowEdges.push(
{ from: 'start', to: 'process' },
{ from: 'process', to: 'end' },
);
}
// Workflow stats
const activeCount = workflows.filter((w: any) => w.status === 'active').length;
const draftCount = workflows.filter((w: any) => w.status === 'draft').length;
return {
root: 'page',
elements: {
page: {
key: 'page',
type: 'PageHeader',
props: {
title: wfName,
subtitle: 'Workflow Status',
status: workflow.status || 'active',
statusVariant: workflow.status === 'active' ? 'active' : 'draft',
gradient: true,
stats: [
{ label: 'Status', value: (workflow.status || 'active').charAt(0).toUpperCase() + (workflow.status || 'active').slice(1) },
{ label: 'Total Workflows', value: String(workflows.length) },
{ label: 'Active', value: String(activeCount) },
{ label: 'Draft', value: String(draftCount) },
],
},
children: ['flow', 'statsGrid'],
},
flow: {
key: 'flow',
type: 'FlowDiagram',
props: {
nodes: flowNodes,
edges: flowEdges,
direction: 'horizontal',
title: `${wfName} Flow`,
},
},
statsGrid: {
key: 'statsGrid',
type: 'StatsGrid',
props: { columns: 3 },
children: ['sActive', 'sDraft', 'sTotal'],
},
sActive: {
key: 'sActive',
type: 'MetricCard',
props: { label: 'Active Workflows', value: String(activeCount), color: 'green' },
},
sDraft: {
key: 'sDraft',
type: 'MetricCard',
props: { label: 'Draft Workflows', value: String(draftCount), color: 'yellow' },
},
sTotal: {
key: 'sTotal',
type: 'MetricCard',
props: { label: 'Total', value: String(workflows.length), color: 'blue' },
},
},
};
}

397
src/apps/types.ts Normal file
View File

@ -0,0 +1,397 @@
/**
* UITree Type Definitions for GHL MCP Apps
* Shared types for the universal renderer and template system.
*/
// ─── Core UITree Types ──────────────────────────────────────
export interface UIElement {
key: string;
type: ComponentType;
props: Record<string, any>;
children?: string[];
}
export interface UITree {
root: string;
elements: Record<string, UIElement>;
}
// ─── Component Type Union ───────────────────────────────────
export type ComponentType =
// Layout
| 'PageHeader'
| 'Card'
| 'StatsGrid'
| 'SplitLayout'
| 'Section'
// Data Display
| 'DataTable'
| 'KanbanBoard'
| 'MetricCard'
| 'StatusBadge'
| 'Timeline'
| 'ProgressBar'
// Detail View
| 'DetailHeader'
| 'KeyValueList'
| 'LineItemsTable'
| 'InfoBlock'
// Interactive
| 'SearchBar'
| 'FilterChips'
| 'TabGroup'
| 'ActionButton'
| 'ActionBar'
// Extended Data Display
| 'CurrencyDisplay'
| 'TagList'
| 'CardGrid'
| 'AvatarGroup'
| 'StarRating'
| 'StockIndicator'
// Communication
| 'ChatThread'
| 'EmailPreview'
| 'ContentPreview'
| 'TranscriptView'
| 'AudioPlayer'
| 'ChecklistView'
// Visualization
| 'CalendarView'
| 'FlowDiagram'
| 'TreeView'
| 'MediaGallery'
| 'DuplicateCompare'
// Charts
| 'BarChart'
| 'LineChart'
| 'PieChart'
| 'FunnelChart'
| 'SparklineChart'
// Interactive Editors
| 'ContactPicker'
| 'InvoiceBuilder'
| 'OpportunityEditor'
| 'AppointmentBooker'
| 'EditableField'
| 'SelectDropdown'
| 'FormGroup'
| 'AmountInput';
// ─── All valid component names for validation ───────────────
export const VALID_COMPONENT_TYPES: ReadonlySet<string> = new Set<ComponentType>([
'PageHeader', 'Card', 'StatsGrid', 'SplitLayout', 'Section',
'DataTable', 'KanbanBoard', 'MetricCard', 'StatusBadge', 'Timeline', 'ProgressBar',
'DetailHeader', 'KeyValueList', 'LineItemsTable', 'InfoBlock',
'SearchBar', 'FilterChips', 'TabGroup', 'ActionButton', 'ActionBar',
'CurrencyDisplay', 'TagList', 'CardGrid', 'AvatarGroup', 'StarRating', 'StockIndicator',
'ChatThread', 'EmailPreview', 'ContentPreview', 'TranscriptView', 'AudioPlayer', 'ChecklistView',
'CalendarView', 'FlowDiagram', 'TreeView', 'MediaGallery', 'DuplicateCompare',
'BarChart', 'LineChart', 'PieChart', 'FunnelChart', 'SparklineChart',
'ContactPicker', 'InvoiceBuilder', 'OpportunityEditor', 'AppointmentBooker',
'EditableField', 'SelectDropdown', 'FormGroup', 'AmountInput',
]);
// ─── Components that can contain children ───────────────────
export const CONTAINER_COMPONENTS: ReadonlySet<string> = new Set<ComponentType>([
'PageHeader', 'Card', 'StatsGrid', 'SplitLayout', 'Section',
'DetailHeader', 'ActionBar',
]);
// ─── Required props per component (minimal set) ─────────────
export const REQUIRED_PROPS: Readonly<Record<string, readonly string[]>> = {
PageHeader: ['title'],
DataTable: ['columns', 'rows'],
KanbanBoard: ['columns'],
MetricCard: ['label', 'value'],
StatusBadge: ['label', 'variant'],
Timeline: ['events'],
ProgressBar: ['label', 'value'],
DetailHeader: ['title'],
KeyValueList: ['items'],
LineItemsTable: ['items'],
InfoBlock: ['label', 'name', 'lines'],
CalendarView: [],
FlowDiagram: ['nodes', 'edges'],
BarChart: ['bars'],
LineChart: ['points'],
PieChart: ['segments'],
FunnelChart: ['stages'],
SparklineChart: ['values'],
CurrencyDisplay: ['amount'],
TagList: ['tags'],
CardGrid: ['cards'],
AvatarGroup: ['avatars'],
StarRating: ['rating'],
StockIndicator: ['quantity'],
ChatThread: ['messages'],
EmailPreview: ['from', 'to', 'subject', 'date', 'body'],
ChecklistView: ['items'],
FormGroup: ['fields'],
AmountInput: ['value'],
EditableField: ['value', 'fieldName'],
OpportunityEditor: ['saveTool', 'opportunity'],
ContactPicker: ['searchTool'],
};
// ─── Prop Interfaces for Template-Used Components ───────────
export interface PageHeaderProps {
title: string;
subtitle?: string;
status?: string;
statusVariant?: 'active' | 'complete' | 'paused' | 'draft' | 'error' | 'sent' | 'paid' | 'pending';
gradient?: boolean;
stats?: Array<{ label: string; value: string }>;
}
export interface DataTableColumn {
key: string;
label: string;
sortable?: boolean;
align?: string;
format?: 'text' | 'email' | 'phone' | 'date' | 'currency' | 'tags' | 'avatar' | 'status';
width?: string;
}
export interface DataTableProps {
columns: DataTableColumn[];
rows: Record<string, any>[];
selectable?: boolean;
rowAction?: string;
emptyMessage?: string;
pageSize?: number;
}
export interface KanbanCard {
id: string;
title: string;
subtitle?: string;
value?: string;
status?: string;
statusVariant?: string;
date?: string;
avatarInitials?: string;
}
export interface KanbanColumn {
id: string;
title: string;
count?: number;
totalValue?: string;
color?: string;
cards: KanbanCard[];
}
export interface KanbanBoardProps {
columns: KanbanColumn[];
}
export interface MetricCardProps {
label: string;
value: string;
format?: 'number' | 'currency' | 'percent';
trend?: 'up' | 'down' | 'flat';
trendValue?: string;
color?: 'default' | 'green' | 'blue' | 'purple' | 'yellow' | 'red';
}
export interface TimelineEvent {
id: string;
title: string;
description?: string;
timestamp: string;
icon?: 'email' | 'phone' | 'note' | 'meeting' | 'task' | 'system';
variant?: string;
}
export interface TimelineProps {
events: TimelineEvent[];
}
export interface KeyValueItem {
label: string;
value: string;
bold?: boolean;
variant?: 'default' | 'highlight' | 'muted' | 'success' | 'danger';
isTotalRow?: boolean;
}
export interface KeyValueListProps {
items: KeyValueItem[];
compact?: boolean;
}
export interface LineItem {
name: string;
description?: string;
quantity: number;
unitPrice: number;
total: number;
}
export interface LineItemsTableProps {
items: LineItem[];
currency?: string;
}
export interface InfoBlockProps {
label: string;
name: string;
lines: string[];
}
export interface DetailHeaderProps {
title: string;
subtitle?: string;
entityId?: string;
status?: string;
statusVariant?: string;
}
export interface SearchBarProps {
placeholder?: string;
valuePath?: string;
}
export interface CalendarEvent {
date: string;
title: string;
time?: string;
color?: string;
type?: 'meeting' | 'call' | 'task' | 'deadline' | 'event';
}
export interface CalendarViewProps {
year?: number;
month?: number;
events: CalendarEvent[];
highlightToday?: boolean;
title?: string;
}
export interface FlowNode {
id: string;
label: string;
type?: 'start' | 'action' | 'condition' | 'end';
description?: string;
}
export interface FlowEdge {
from: string;
to: string;
label?: string;
}
export interface FlowDiagramProps {
nodes: FlowNode[];
edges: FlowEdge[];
direction?: 'horizontal' | 'vertical';
title?: string;
}
export interface BarChartBar {
label: string;
value: number;
color?: string;
}
export interface BarChartProps {
bars: BarChartBar[];
orientation?: 'vertical' | 'horizontal';
maxValue?: number;
showValues?: boolean;
title?: string;
}
export interface LineChartPoint {
label: string;
value: number;
}
export interface LineChartProps {
points: LineChartPoint[];
color?: string;
showPoints?: boolean;
showArea?: boolean;
title?: string;
yAxisLabel?: string;
}
// ─── UITree Validation ──────────────────────────────────────
export interface ValidationError {
path: string;
message: string;
}
/**
* Validate a UITree for correctness:
* - Root key exists in elements
* - All children references resolve
* - All component types are valid
* - Required props are present
*/
export function validateUITree(tree: UITree): ValidationError[] {
const errors: ValidationError[] = [];
if (!tree || typeof tree !== 'object') {
errors.push({ path: '', message: 'UITree must be a non-null object' });
return errors;
}
if (!tree.root) {
errors.push({ path: 'root', message: 'Missing root key' });
}
if (!tree.elements || typeof tree.elements !== 'object') {
errors.push({ path: 'elements', message: 'Missing or invalid elements map' });
return errors;
}
// Check root exists in elements
if (tree.root && !tree.elements[tree.root]) {
errors.push({ path: 'root', message: `Root key "${tree.root}" not found in elements` });
}
// Validate each element
for (const [key, element] of Object.entries(tree.elements)) {
const ePath = `elements.${key}`;
// Check key matches
if (element.key !== key) {
errors.push({ path: ePath, message: `Element key mismatch: "${element.key}" vs map key "${key}"` });
}
// Check component type
if (!VALID_COMPONENT_TYPES.has(element.type)) {
errors.push({ path: `${ePath}.type`, message: `Unknown component type: "${element.type}"` });
}
// Check required props
const requiredProps = REQUIRED_PROPS[element.type];
if (requiredProps) {
for (const prop of requiredProps) {
if (element.props[prop] === undefined || element.props[prop] === null) {
errors.push({ path: `${ePath}.props.${prop}`, message: `Missing required prop "${prop}" for ${element.type}` });
}
}
}
// Check children references
if (element.children) {
for (const childKey of element.children) {
if (!tree.elements[childKey]) {
errors.push({ path: `${ePath}.children`, message: `Child reference "${childKey}" not found in elements` });
}
}
}
}
return errors;
}

View File

@ -22,6 +22,13 @@ export class AffiliatesTools {
limit: { type: 'number', description: 'Max results' }, limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' } offset: { type: 'number', description: 'Pagination offset' }
} }
},
_meta: {
labels: {
category: "affiliates",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -31,7 +38,14 @@ export class AffiliatesTools {
type: 'object', type: 'object',
properties: { properties: {
campaignId: { type: 'string', description: 'Affiliate Campaign ID' }, campaignId: { type: 'string', description: 'Affiliate Campaign ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "affiliates",
access: "read",
complexity: "simple"
}
}
}, },
required: ['campaignId'] required: ['campaignId']
} }
@ -48,7 +62,14 @@ export class AffiliatesTools {
commissionType: { type: 'string', enum: ['percentage', 'fixed'], description: 'Commission type' }, commissionType: { type: 'string', enum: ['percentage', 'fixed'], description: 'Commission type' },
commissionValue: { type: 'number', description: 'Commission value (percentage or fixed amount)' }, commissionValue: { type: 'number', description: 'Commission value (percentage or fixed amount)' },
cookieDays: { type: 'number', description: 'Cookie tracking duration in days' }, cookieDays: { type: 'number', description: 'Cookie tracking duration in days' },
productIds: { type: 'array', items: { type: 'string' }, description: 'Product IDs for this campaign' } productIds: { type: 'array', items: { type: 'string' }, description: 'Product IDs for this campaign' },
_meta: {
labels: {
category: "affiliates",
access: "write",
complexity: "simple"
}
}
}, },
required: ['name', 'commissionType', 'commissionValue'] required: ['name', 'commissionType', 'commissionValue']
} }
@ -65,7 +86,14 @@ export class AffiliatesTools {
description: { type: 'string', description: 'Campaign description' }, description: { type: 'string', description: 'Campaign description' },
commissionType: { type: 'string', enum: ['percentage', 'fixed'], description: 'Commission type' }, commissionType: { type: 'string', enum: ['percentage', 'fixed'], description: 'Commission type' },
commissionValue: { type: 'number', description: 'Commission value' }, commissionValue: { type: 'number', description: 'Commission value' },
status: { type: 'string', enum: ['active', 'inactive'], description: 'Campaign status' } status: { type: 'string', enum: ['active', 'inactive'], description: 'Campaign status' },
_meta: {
labels: {
category: "affiliates",
access: "write",
complexity: "simple"
}
}
}, },
required: ['campaignId'] required: ['campaignId']
} }
@ -77,7 +105,14 @@ export class AffiliatesTools {
type: 'object', type: 'object',
properties: { properties: {
campaignId: { type: 'string', description: 'Campaign ID' }, campaignId: { type: 'string', description: 'Campaign ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "affiliates",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['campaignId'] required: ['campaignId']
} }
@ -96,6 +131,13 @@ export class AffiliatesTools {
limit: { type: 'number', description: 'Max results' }, limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' } offset: { type: 'number', description: 'Pagination offset' }
} }
},
_meta: {
labels: {
category: "affiliates",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -105,7 +147,14 @@ export class AffiliatesTools {
type: 'object', type: 'object',
properties: { properties: {
affiliateId: { type: 'string', description: 'Affiliate ID' }, affiliateId: { type: 'string', description: 'Affiliate ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "affiliates",
access: "read",
complexity: "simple"
}
}
}, },
required: ['affiliateId'] required: ['affiliateId']
} }
@ -120,7 +169,14 @@ export class AffiliatesTools {
contactId: { type: 'string', description: 'Contact ID to make affiliate' }, contactId: { type: 'string', description: 'Contact ID to make affiliate' },
campaignId: { type: 'string', description: 'Campaign to assign to' }, campaignId: { type: 'string', description: 'Campaign to assign to' },
customCode: { type: 'string', description: 'Custom affiliate code' }, customCode: { type: 'string', description: 'Custom affiliate code' },
status: { type: 'string', enum: ['pending', 'approved'], description: 'Initial status' } status: { type: 'string', enum: ['pending', 'approved'], description: 'Initial status' },
_meta: {
labels: {
category: "affiliates",
access: "write",
complexity: "simple"
}
}
}, },
required: ['contactId', 'campaignId'] required: ['contactId', 'campaignId']
} }
@ -134,7 +190,14 @@ export class AffiliatesTools {
affiliateId: { type: 'string', description: 'Affiliate ID' }, affiliateId: { type: 'string', description: 'Affiliate ID' },
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
status: { type: 'string', enum: ['pending', 'approved', 'rejected'], description: 'Status' }, status: { type: 'string', enum: ['pending', 'approved', 'rejected'], description: 'Status' },
customCode: { type: 'string', description: 'Custom affiliate code' } customCode: { type: 'string', description: 'Custom affiliate code' },
_meta: {
labels: {
category: "affiliates",
access: "write",
complexity: "simple"
}
}
}, },
required: ['affiliateId'] required: ['affiliateId']
} }
@ -146,7 +209,14 @@ export class AffiliatesTools {
type: 'object', type: 'object',
properties: { properties: {
affiliateId: { type: 'string', description: 'Affiliate ID' }, affiliateId: { type: 'string', description: 'Affiliate ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "affiliates",
access: "read",
complexity: "simple"
}
}
}, },
required: ['affiliateId'] required: ['affiliateId']
} }
@ -159,7 +229,14 @@ export class AffiliatesTools {
properties: { properties: {
affiliateId: { type: 'string', description: 'Affiliate ID' }, affiliateId: { type: 'string', description: 'Affiliate ID' },
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
reason: { type: 'string', description: 'Rejection reason' } reason: { type: 'string', description: 'Rejection reason' },
_meta: {
labels: {
category: "affiliates",
access: "read",
complexity: "simple"
}
}
}, },
required: ['affiliateId'] required: ['affiliateId']
} }
@ -171,7 +248,14 @@ export class AffiliatesTools {
type: 'object', type: 'object',
properties: { properties: {
affiliateId: { type: 'string', description: 'Affiliate ID' }, affiliateId: { type: 'string', description: 'Affiliate ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "affiliates",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['affiliateId'] required: ['affiliateId']
} }
@ -190,7 +274,14 @@ export class AffiliatesTools {
startDate: { type: 'string', description: 'Start date' }, startDate: { type: 'string', description: 'Start date' },
endDate: { type: 'string', description: 'End date' }, endDate: { type: 'string', description: 'End date' },
limit: { type: 'number', description: 'Max results' }, limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' } offset: { type: 'number', description: 'Pagination offset' },
_meta: {
labels: {
category: "affiliates",
access: "read",
complexity: "simple"
}
}
}, },
required: ['affiliateId'] required: ['affiliateId']
} }
@ -204,7 +295,14 @@ export class AffiliatesTools {
affiliateId: { type: 'string', description: 'Affiliate ID' }, affiliateId: { type: 'string', description: 'Affiliate ID' },
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
startDate: { type: 'string', description: 'Start date' }, startDate: { type: 'string', description: 'Start date' },
endDate: { type: 'string', description: 'End date' } endDate: { type: 'string', description: 'End date' },
_meta: {
labels: {
category: "affiliates",
access: "read",
complexity: "simple"
}
}
}, },
required: ['affiliateId'] required: ['affiliateId']
} }
@ -219,7 +317,14 @@ export class AffiliatesTools {
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
amount: { type: 'number', description: 'Payout amount' }, amount: { type: 'number', description: 'Payout amount' },
commissionIds: { type: 'array', items: { type: 'string' }, description: 'Commission IDs to include' }, commissionIds: { type: 'array', items: { type: 'string' }, description: 'Commission IDs to include' },
note: { type: 'string', description: 'Payout note' } note: { type: 'string', description: 'Payout note' },
_meta: {
labels: {
category: "affiliates",
access: "write",
complexity: "simple"
}
}
}, },
required: ['affiliateId', 'amount'] required: ['affiliateId', 'amount']
} }
@ -236,6 +341,13 @@ export class AffiliatesTools {
limit: { type: 'number', description: 'Max results' }, limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' } offset: { type: 'number', description: 'Pagination offset' }
} }
},
_meta: {
labels: {
category: "affiliates",
access: "read",
complexity: "simple"
}
} }
}, },

View File

@ -40,6 +40,13 @@ export class AssociationTools {
default: 20 default: 20
} }
} }
},
_meta: {
labels: {
category: "associations",
access: "read",
complexity: "batch"
}
} }
}, },
{ {
@ -67,7 +74,14 @@ export class AssociationTools {
}, },
secondObjectKey: { secondObjectKey: {
description: 'Key for the second object (e.g., "contact")' description: 'Key for the second object (e.g., "contact")'
} },
_meta: {
labels: {
category: "associations",
access: "write",
complexity: "simple"
}
}
}, },
required: ['key', 'firstObjectLabel', 'firstObjectKey', 'secondObjectLabel', 'secondObjectKey'] required: ['key', 'firstObjectLabel', 'firstObjectKey', 'secondObjectLabel', 'secondObjectKey']
} }
@ -81,7 +95,14 @@ export class AssociationTools {
associationId: { associationId: {
type: 'string', type: 'string',
description: 'The ID of the association to retrieve' description: 'The ID of the association to retrieve'
} },
_meta: {
labels: {
category: "associations",
access: "read",
complexity: "simple"
}
}
}, },
required: ['associationId'] required: ['associationId']
} }
@ -101,7 +122,14 @@ export class AssociationTools {
}, },
secondObjectLabel: { secondObjectLabel: {
description: 'New label for the second object in the association' description: 'New label for the second object in the association'
} },
_meta: {
labels: {
category: "associations",
access: "write",
complexity: "simple"
}
}
}, },
required: ['associationId', 'firstObjectLabel', 'secondObjectLabel'] required: ['associationId', 'firstObjectLabel', 'secondObjectLabel']
} }
@ -115,7 +143,14 @@ export class AssociationTools {
associationId: { associationId: {
type: 'string', type: 'string',
description: 'The ID of the association to delete' description: 'The ID of the association to delete'
} },
_meta: {
labels: {
category: "associations",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['associationId'] required: ['associationId']
} }
@ -133,7 +168,14 @@ export class AssociationTools {
locationId: { locationId: {
type: 'string', type: 'string',
description: 'GoHighLevel location ID (will use default if not provided)' description: 'GoHighLevel location ID (will use default if not provided)'
} },
_meta: {
labels: {
category: "associations",
access: "read",
complexity: "simple"
}
}
}, },
required: ['keyName'] required: ['keyName']
} }
@ -151,7 +193,14 @@ export class AssociationTools {
locationId: { locationId: {
type: 'string', type: 'string',
description: 'GoHighLevel location ID (optional)' description: 'GoHighLevel location ID (optional)'
} },
_meta: {
labels: {
category: "associations",
access: "read",
complexity: "simple"
}
}
}, },
required: ['objectKey'] required: ['objectKey']
} }
@ -178,7 +227,14 @@ export class AssociationTools {
secondRecordId: { secondRecordId: {
type: 'string', type: 'string',
description: 'ID of the second record (e.g., custom object record ID if custom object is second object)' description: 'ID of the second record (e.g., custom object record ID if custom object is second object)'
} },
_meta: {
labels: {
category: "associations",
access: "write",
complexity: "simple"
}
}
}, },
required: ['associationId', 'firstRecordId', 'secondRecordId'] required: ['associationId', 'firstRecordId', 'secondRecordId']
} }
@ -213,7 +269,14 @@ export class AssociationTools {
type: 'string' type: 'string'
}, },
description: 'Optional array of association IDs to filter relations' description: 'Optional array of association IDs to filter relations'
} },
_meta: {
labels: {
category: "associations",
access: "read",
complexity: "simple"
}
}
}, },
required: ['recordId'] required: ['recordId']
} }
@ -231,7 +294,14 @@ export class AssociationTools {
locationId: { locationId: {
type: 'string', type: 'string',
description: 'GoHighLevel location ID (will use default if not provided)' description: 'GoHighLevel location ID (will use default if not provided)'
} },
_meta: {
labels: {
category: "associations",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['relationId'] required: ['relationId']
} }

View File

@ -94,6 +94,13 @@ export class BlogTools {
publishedAt: { publishedAt: {
type: 'string', type: 'string',
description: 'Optional ISO timestamp for publication date (defaults to now for PUBLISHED status)' description: 'Optional ISO timestamp for publication date (defaults to now for PUBLISHED status)'
},
_meta: {
labels: {
category: "blogs",
access: "write",
complexity: "simple"
}
} }
}, },
required: ['title', 'blogId', 'content', 'description', 'imageUrl', 'imageAltText', 'urlSlug', 'author', 'categories'] required: ['title', 'blogId', 'content', 'description', 'imageUrl', 'imageAltText', 'urlSlug', 'author', 'categories']
@ -165,6 +172,13 @@ export class BlogTools {
publishedAt: { publishedAt: {
type: 'string', type: 'string',
description: 'Updated ISO timestamp for publication date' description: 'Updated ISO timestamp for publication date'
},
_meta: {
labels: {
category: "blogs",
access: "write",
complexity: "simple"
}
} }
}, },
required: ['postId', 'blogId'] required: ['postId', 'blogId']
@ -200,6 +214,13 @@ export class BlogTools {
type: 'string', type: 'string',
enum: ['DRAFT', 'PUBLISHED', 'SCHEDULED', 'ARCHIVED'], enum: ['DRAFT', 'PUBLISHED', 'SCHEDULED', 'ARCHIVED'],
description: 'Optional filter by publication status' description: 'Optional filter by publication status'
},
_meta: {
labels: {
category: "blogs",
access: "read",
complexity: "simple"
}
} }
}, },
required: ['blogId'] required: ['blogId']
@ -228,7 +249,14 @@ export class BlogTools {
description: 'Optional search term to filter blogs by name' description: 'Optional search term to filter blogs by name'
} }
} }
} },
_meta: {
labels: {
category: "blogs",
access: "read",
complexity: "simple"
}
}
}, },
// 5. Get Blog Authors // 5. Get Blog Authors
@ -249,7 +277,14 @@ export class BlogTools {
default: 0 default: 0
} }
} }
} },
_meta: {
labels: {
category: "blogs",
access: "read",
complexity: "simple"
}
}
}, },
// 6. Get Blog Categories // 6. Get Blog Categories
@ -270,7 +305,14 @@ export class BlogTools {
default: 0 default: 0
} }
} }
} },
_meta: {
labels: {
category: "blogs",
access: "read",
complexity: "simple"
}
}
}, },
// 7. Check URL Slug // 7. Check URL Slug
@ -287,6 +329,13 @@ export class BlogTools {
postId: { postId: {
type: 'string', type: 'string',
description: 'Optional post ID when updating an existing post (to exclude itself from the check)' description: 'Optional post ID when updating an existing post (to exclude itself from the check)'
},
_meta: {
labels: {
category: "blogs",
access: "read",
complexity: "simple"
}
} }
}, },
required: ['urlSlug'] required: ['urlSlug']

View File

@ -21,6 +21,13 @@ export class BusinessesTools {
description: 'Location ID (uses default if not provided)' description: 'Location ID (uses default if not provided)'
} }
} }
},
_meta: {
labels: {
category: "businesses",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -36,7 +43,14 @@ export class BusinessesTools {
locationId: { locationId: {
type: 'string', type: 'string',
description: 'Location ID (uses default if not provided)' description: 'Location ID (uses default if not provided)'
} },
_meta: {
labels: {
category: "businesses",
access: "read",
complexity: "simple"
}
}
}, },
required: ['businessId'] required: ['businessId']
} }
@ -94,7 +108,14 @@ export class BusinessesTools {
logoUrl: { logoUrl: {
type: 'string', type: 'string',
description: 'URL to business logo image' description: 'URL to business logo image'
} },
_meta: {
labels: {
category: "businesses",
access: "write",
complexity: "simple"
}
}
}, },
required: ['name'] required: ['name']
} }
@ -156,7 +177,14 @@ export class BusinessesTools {
logoUrl: { logoUrl: {
type: 'string', type: 'string',
description: 'URL to business logo image' description: 'URL to business logo image'
} },
_meta: {
labels: {
category: "businesses",
access: "write",
complexity: "simple"
}
}
}, },
required: ['businessId'] required: ['businessId']
} }
@ -174,7 +202,14 @@ export class BusinessesTools {
locationId: { locationId: {
type: 'string', type: 'string',
description: 'Location ID (uses default if not provided)' description: 'Location ID (uses default if not provided)'
} },
_meta: {
labels: {
category: "businesses",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['businessId'] required: ['businessId']
} }

View File

@ -62,6 +62,13 @@ export class CalendarTools {
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: {} properties: {}
},
_meta: {
labels: {
category: "calendar",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -80,6 +87,13 @@ export class CalendarTools {
default: true default: true
} }
} }
},
_meta: {
labels: {
category: "calendar",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -136,7 +150,14 @@ export class CalendarTools {
type: 'boolean', type: 'boolean',
description: 'Make calendar active immediately (default: true)', description: 'Make calendar active immediately (default: true)',
default: true default: true
} },
_meta: {
labels: {
category: "calendar",
access: "write",
complexity: "simple"
}
}
}, },
required: ['name', 'calendarType'] required: ['name', 'calendarType']
} }
@ -150,7 +171,14 @@ export class CalendarTools {
calendarId: { calendarId: {
type: 'string', type: 'string',
description: 'The unique ID of the calendar to retrieve' description: 'The unique ID of the calendar to retrieve'
} },
_meta: {
labels: {
category: "calendar",
access: "read",
complexity: "simple"
}
}
}, },
required: ['calendarId'] required: ['calendarId']
} }
@ -192,7 +220,14 @@ export class CalendarTools {
isActive: { isActive: {
type: 'boolean', type: 'boolean',
description: 'Updated active status' description: 'Updated active status'
} },
_meta: {
labels: {
category: "calendar",
access: "write",
complexity: "simple"
}
}
}, },
required: ['calendarId'] required: ['calendarId']
} }
@ -206,7 +241,14 @@ export class CalendarTools {
calendarId: { calendarId: {
type: 'string', type: 'string',
description: 'The unique ID of the calendar to delete' description: 'The unique ID of the calendar to delete'
} },
_meta: {
labels: {
category: "calendar",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['calendarId'] required: ['calendarId']
} }
@ -236,7 +278,14 @@ export class CalendarTools {
groupId: { groupId: {
type: 'string', type: 'string',
description: 'Filter events by calendar group ID' description: 'Filter events by calendar group ID'
} },
_meta: {
labels: {
category: "calendar",
access: "read",
complexity: "simple"
}
}
}, },
required: ['startTime', 'endTime'] required: ['startTime', 'endTime']
} }
@ -266,7 +315,14 @@ export class CalendarTools {
userId: { userId: {
type: 'string', type: 'string',
description: 'Specific user ID to check availability for' description: 'Specific user ID to check availability for'
} },
_meta: {
labels: {
category: "calendar",
access: "read",
complexity: "simple"
}
}
}, },
required: ['calendarId', 'startDate', 'endDate'] required: ['calendarId', 'startDate', 'endDate']
} }
@ -326,7 +382,14 @@ export class CalendarTools {
type: 'boolean', type: 'boolean',
description: 'Send notifications for this appointment', description: 'Send notifications for this appointment',
default: true default: true
} },
_meta: {
labels: {
category: "calendar",
access: "write",
complexity: "simple"
}
}
}, },
required: ['calendarId', 'contactId', 'startTime'] required: ['calendarId', 'contactId', 'startTime']
} }
@ -340,7 +403,14 @@ export class CalendarTools {
appointmentId: { appointmentId: {
type: 'string', type: 'string',
description: 'The unique ID of the appointment to retrieve' description: 'The unique ID of the appointment to retrieve'
} },
_meta: {
labels: {
category: "calendar",
access: "read",
complexity: "simple"
}
}
}, },
required: ['appointmentId'] required: ['appointmentId']
} }
@ -384,7 +454,14 @@ export class CalendarTools {
type: 'boolean', type: 'boolean',
description: 'Send notifications for this update', description: 'Send notifications for this update',
default: true default: true
} },
_meta: {
labels: {
category: "calendar",
access: "write",
complexity: "simple"
}
}
}, },
required: ['appointmentId'] required: ['appointmentId']
} }
@ -398,7 +475,14 @@ export class CalendarTools {
appointmentId: { appointmentId: {
type: 'string', type: 'string',
description: 'The unique ID of the appointment to delete' description: 'The unique ID of the appointment to delete'
} },
_meta: {
labels: {
category: "calendar",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['appointmentId'] required: ['appointmentId']
} }
@ -428,7 +512,14 @@ export class CalendarTools {
assignedUserId: { assignedUserId: {
type: 'string', type: 'string',
description: 'User ID to apply the block for' description: 'User ID to apply the block for'
} },
_meta: {
labels: {
category: "calendar",
access: "write",
complexity: "simple"
}
}
}, },
required: ['startTime', 'endTime'] required: ['startTime', 'endTime']
} }
@ -462,7 +553,14 @@ export class CalendarTools {
assignedUserId: { assignedUserId: {
type: 'string', type: 'string',
description: 'Updated assigned user ID' description: 'Updated assigned user ID'
} },
_meta: {
labels: {
category: "calendar",
access: "write",
complexity: "simple"
}
}
}, },
required: ['blockSlotId'] required: ['blockSlotId']
} }
@ -476,7 +574,14 @@ export class CalendarTools {
name: { type: 'string', description: 'Group name' }, name: { type: 'string', description: 'Group name' },
description: { type: 'string', description: 'Group description' }, description: { type: 'string', description: 'Group description' },
slug: { type: 'string', description: 'URL slug for the group' }, slug: { type: 'string', description: 'URL slug for the group' },
isActive: { type: 'boolean', description: 'Whether group is active', default: true } isActive: { type: 'boolean', description: 'Whether group is active', default: true },
_meta: {
labels: {
category: "calendar",
access: "write",
complexity: "simple"
}
}
}, },
required: ['name', 'description', 'slug'] required: ['name', 'description', 'slug']
} }
@ -488,7 +593,14 @@ export class CalendarTools {
type: 'object', type: 'object',
properties: { properties: {
slug: { type: 'string', description: 'Slug to validate' }, slug: { type: 'string', description: 'Slug to validate' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "calendar",
access: "read",
complexity: "simple"
}
}
}, },
required: ['slug'] required: ['slug']
} }
@ -502,7 +614,14 @@ export class CalendarTools {
groupId: { type: 'string', description: 'Calendar group ID' }, groupId: { type: 'string', description: 'Calendar group ID' },
name: { type: 'string', description: 'Group name' }, name: { type: 'string', description: 'Group name' },
description: { type: 'string', description: 'Group description' }, description: { type: 'string', description: 'Group description' },
slug: { type: 'string', description: 'URL slug for the group' } slug: { type: 'string', description: 'URL slug for the group' },
_meta: {
labels: {
category: "calendar",
access: "write",
complexity: "simple"
}
}
}, },
required: ['groupId', 'name', 'description', 'slug'] required: ['groupId', 'name', 'description', 'slug']
} }
@ -513,7 +632,14 @@ export class CalendarTools {
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
groupId: { type: 'string', description: 'Calendar group ID' } groupId: { type: 'string', description: 'Calendar group ID' },
_meta: {
labels: {
category: "calendar",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['groupId'] required: ['groupId']
} }
@ -525,7 +651,14 @@ export class CalendarTools {
type: 'object', type: 'object',
properties: { properties: {
groupId: { type: 'string', description: 'Calendar group ID' }, groupId: { type: 'string', description: 'Calendar group ID' },
isActive: { type: 'boolean', description: 'Whether to enable (true) or disable (false) the group' } isActive: { type: 'boolean', description: 'Whether to enable (true) or disable (false) the group' },
_meta: {
labels: {
category: "calendar",
access: "read",
complexity: "simple"
}
}
}, },
required: ['groupId', 'isActive'] required: ['groupId', 'isActive']
} }
@ -538,7 +671,14 @@ export class CalendarTools {
properties: { properties: {
appointmentId: { type: 'string', description: 'Appointment ID' }, appointmentId: { type: 'string', description: 'Appointment ID' },
limit: { type: 'number', description: 'Maximum number of notes to return', default: 10 }, limit: { type: 'number', description: 'Maximum number of notes to return', default: 10 },
offset: { type: 'number', description: 'Number of notes to skip', default: 0 } offset: { type: 'number', description: 'Number of notes to skip', default: 0 },
_meta: {
labels: {
category: "calendar",
access: "read",
complexity: "simple"
}
}
}, },
required: ['appointmentId'] required: ['appointmentId']
} }
@ -551,7 +691,14 @@ export class CalendarTools {
properties: { properties: {
appointmentId: { type: 'string', description: 'Appointment ID' }, appointmentId: { type: 'string', description: 'Appointment ID' },
body: { type: 'string', description: 'Note content' }, body: { type: 'string', description: 'Note content' },
userId: { type: 'string', description: 'User ID creating the note' } userId: { type: 'string', description: 'User ID creating the note' },
_meta: {
labels: {
category: "calendar",
access: "write",
complexity: "simple"
}
}
}, },
required: ['appointmentId', 'body'] required: ['appointmentId', 'body']
} }
@ -565,7 +712,14 @@ export class CalendarTools {
appointmentId: { type: 'string', description: 'Appointment ID' }, appointmentId: { type: 'string', description: 'Appointment ID' },
noteId: { type: 'string', description: 'Note ID' }, noteId: { type: 'string', description: 'Note ID' },
body: { type: 'string', description: 'Updated note content' }, body: { type: 'string', description: 'Updated note content' },
userId: { type: 'string', description: 'User ID updating the note' } userId: { type: 'string', description: 'User ID updating the note' },
_meta: {
labels: {
category: "calendar",
access: "write",
complexity: "simple"
}
}
}, },
required: ['appointmentId', 'noteId', 'body'] required: ['appointmentId', 'noteId', 'body']
} }
@ -577,7 +731,14 @@ export class CalendarTools {
type: 'object', type: 'object',
properties: { properties: {
appointmentId: { type: 'string', description: 'Appointment ID' }, appointmentId: { type: 'string', description: 'Appointment ID' },
noteId: { type: 'string', description: 'Note ID' } noteId: { type: 'string', description: 'Note ID' },
_meta: {
labels: {
category: "calendar",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['appointmentId', 'noteId'] required: ['appointmentId', 'noteId']
} }
@ -591,6 +752,13 @@ export class CalendarTools {
limit: { type: 'number', description: 'Maximum number to return', default: 20 }, limit: { type: 'number', description: 'Maximum number to return', default: 20 },
skip: { type: 'number', description: 'Number to skip', default: 0 } skip: { type: 'number', description: 'Number to skip', default: 0 }
} }
},
_meta: {
labels: {
category: "calendar",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -604,7 +772,14 @@ export class CalendarTools {
quantity: { type: 'number', description: 'Total quantity available' }, quantity: { type: 'number', description: 'Total quantity available' },
outOfService: { type: 'number', description: 'Number currently out of service' }, outOfService: { type: 'number', description: 'Number currently out of service' },
capacity: { type: 'number', description: 'Capacity per unit' }, capacity: { type: 'number', description: 'Capacity per unit' },
calendarIds: { type: 'array', items: { type: 'string' }, description: 'Associated calendar IDs' } calendarIds: { type: 'array', items: { type: 'string' }, description: 'Associated calendar IDs' },
_meta: {
labels: {
category: "calendar",
access: "write",
complexity: "simple"
}
}
}, },
required: ['name', 'description', 'quantity', 'outOfService', 'capacity', 'calendarIds'] required: ['name', 'description', 'quantity', 'outOfService', 'capacity', 'calendarIds']
} }
@ -615,7 +790,14 @@ export class CalendarTools {
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
resourceId: { type: 'string', description: 'Equipment resource ID' } resourceId: { type: 'string', description: 'Equipment resource ID' },
_meta: {
labels: {
category: "calendar",
access: "read",
complexity: "simple"
}
}
}, },
required: ['resourceId'] required: ['resourceId']
} }
@ -633,7 +815,14 @@ export class CalendarTools {
outOfService: { type: 'number', description: 'Number currently out of service' }, outOfService: { type: 'number', description: 'Number currently out of service' },
capacity: { type: 'number', description: 'Capacity per unit' }, capacity: { type: 'number', description: 'Capacity per unit' },
calendarIds: { type: 'array', items: { type: 'string' }, description: 'Associated calendar IDs' }, calendarIds: { type: 'array', items: { type: 'string' }, description: 'Associated calendar IDs' },
isActive: { type: 'boolean', description: 'Whether resource is active' } isActive: { type: 'boolean', description: 'Whether resource is active' },
_meta: {
labels: {
category: "calendar",
access: "write",
complexity: "simple"
}
}
}, },
required: ['resourceId'] required: ['resourceId']
} }
@ -644,7 +833,14 @@ export class CalendarTools {
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
resourceId: { type: 'string', description: 'Equipment resource ID' } resourceId: { type: 'string', description: 'Equipment resource ID' },
_meta: {
labels: {
category: "calendar",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['resourceId'] required: ['resourceId']
} }
@ -658,6 +854,13 @@ export class CalendarTools {
limit: { type: 'number', description: 'Maximum number to return', default: 20 }, limit: { type: 'number', description: 'Maximum number to return', default: 20 },
skip: { type: 'number', description: 'Number to skip', default: 0 } skip: { type: 'number', description: 'Number to skip', default: 0 }
} }
},
_meta: {
labels: {
category: "calendar",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -671,7 +874,14 @@ export class CalendarTools {
quantity: { type: 'number', description: 'Total quantity available' }, quantity: { type: 'number', description: 'Total quantity available' },
outOfService: { type: 'number', description: 'Number currently out of service' }, outOfService: { type: 'number', description: 'Number currently out of service' },
capacity: { type: 'number', description: 'Room capacity' }, capacity: { type: 'number', description: 'Room capacity' },
calendarIds: { type: 'array', items: { type: 'string' }, description: 'Associated calendar IDs' } calendarIds: { type: 'array', items: { type: 'string' }, description: 'Associated calendar IDs' },
_meta: {
labels: {
category: "calendar",
access: "write",
complexity: "simple"
}
}
}, },
required: ['name', 'description', 'quantity', 'outOfService', 'capacity', 'calendarIds'] required: ['name', 'description', 'quantity', 'outOfService', 'capacity', 'calendarIds']
} }
@ -682,7 +892,14 @@ export class CalendarTools {
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
resourceId: { type: 'string', description: 'Room resource ID' } resourceId: { type: 'string', description: 'Room resource ID' },
_meta: {
labels: {
category: "calendar",
access: "read",
complexity: "simple"
}
}
}, },
required: ['resourceId'] required: ['resourceId']
} }
@ -700,7 +917,14 @@ export class CalendarTools {
outOfService: { type: 'number', description: 'Number currently out of service' }, outOfService: { type: 'number', description: 'Number currently out of service' },
capacity: { type: 'number', description: 'Room capacity' }, capacity: { type: 'number', description: 'Room capacity' },
calendarIds: { type: 'array', items: { type: 'string' }, description: 'Associated calendar IDs' }, calendarIds: { type: 'array', items: { type: 'string' }, description: 'Associated calendar IDs' },
isActive: { type: 'boolean', description: 'Whether resource is active' } isActive: { type: 'boolean', description: 'Whether resource is active' },
_meta: {
labels: {
category: "calendar",
access: "write",
complexity: "simple"
}
}
}, },
required: ['resourceId'] required: ['resourceId']
} }
@ -711,7 +935,14 @@ export class CalendarTools {
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
resourceId: { type: 'string', description: 'Room resource ID' } resourceId: { type: 'string', description: 'Room resource ID' },
_meta: {
labels: {
category: "calendar",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['resourceId'] required: ['resourceId']
} }
@ -726,7 +957,14 @@ export class CalendarTools {
isActive: { type: 'boolean', description: 'Filter by active status' }, isActive: { type: 'boolean', description: 'Filter by active status' },
deleted: { type: 'boolean', description: 'Include deleted notifications' }, deleted: { type: 'boolean', description: 'Include deleted notifications' },
limit: { type: 'number', description: 'Maximum number to return' }, limit: { type: 'number', description: 'Maximum number to return' },
skip: { type: 'number', description: 'Number to skip' } skip: { type: 'number', description: 'Number to skip' },
_meta: {
labels: {
category: "calendar",
access: "read",
complexity: "simple"
}
}
}, },
required: ['calendarId'] required: ['calendarId']
} }
@ -749,7 +987,14 @@ export class CalendarTools {
isActive: { type: 'boolean', description: 'Whether notification is active' }, isActive: { type: 'boolean', description: 'Whether notification is active' },
templateId: { type: 'string', description: 'Template ID' }, templateId: { type: 'string', description: 'Template ID' },
body: { type: 'string', description: 'Notification body' }, body: { type: 'string', description: 'Notification body' },
subject: { type: 'string', description: 'Notification subject' } subject: { type: 'string', description: 'Notification subject' },
_meta: {
labels: {
category: "calendar",
access: "write",
complexity: "simple"
}
}
}, },
required: ['receiverType', 'channel', 'notificationType'] required: ['receiverType', 'channel', 'notificationType']
}, },
@ -766,7 +1011,14 @@ export class CalendarTools {
type: 'object', type: 'object',
properties: { properties: {
calendarId: { type: 'string', description: 'Calendar ID' }, calendarId: { type: 'string', description: 'Calendar ID' },
notificationId: { type: 'string', description: 'Notification ID' } notificationId: { type: 'string', description: 'Notification ID' },
_meta: {
labels: {
category: "calendar",
access: "read",
complexity: "simple"
}
}
}, },
required: ['calendarId', 'notificationId'] required: ['calendarId', 'notificationId']
} }
@ -786,7 +1038,14 @@ export class CalendarTools {
deleted: { type: 'boolean', description: 'Whether notification is deleted' }, deleted: { type: 'boolean', description: 'Whether notification is deleted' },
templateId: { type: 'string', description: 'Template ID' }, templateId: { type: 'string', description: 'Template ID' },
body: { type: 'string', description: 'Notification body' }, body: { type: 'string', description: 'Notification body' },
subject: { type: 'string', description: 'Notification subject' } subject: { type: 'string', description: 'Notification subject' },
_meta: {
labels: {
category: "calendar",
access: "write",
complexity: "simple"
}
}
}, },
required: ['calendarId', 'notificationId'] required: ['calendarId', 'notificationId']
} }
@ -798,7 +1057,14 @@ export class CalendarTools {
type: 'object', type: 'object',
properties: { properties: {
calendarId: { type: 'string', description: 'Calendar ID' }, calendarId: { type: 'string', description: 'Calendar ID' },
notificationId: { type: 'string', description: 'Notification ID' } notificationId: { type: 'string', description: 'Notification ID' },
_meta: {
labels: {
category: "calendar",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['calendarId', 'notificationId'] required: ['calendarId', 'notificationId']
} }
@ -813,7 +1079,14 @@ export class CalendarTools {
calendarId: { type: 'string', description: 'Filter by calendar ID' }, calendarId: { type: 'string', description: 'Filter by calendar ID' },
groupId: { type: 'string', description: 'Filter by group ID' }, groupId: { type: 'string', description: 'Filter by group ID' },
startTime: { type: 'string', description: 'Start time for the query range' }, startTime: { type: 'string', description: 'Start time for the query range' },
endTime: { type: 'string', description: 'End time for the query range' } endTime: { type: 'string', description: 'End time for the query range' },
_meta: {
labels: {
category: "calendar",
access: "read",
complexity: "simple"
}
}
}, },
required: ['startTime', 'endTime'] required: ['startTime', 'endTime']
} }

View File

@ -22,6 +22,13 @@ export class CampaignsTools {
limit: { type: 'number', description: 'Max results' }, limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' } offset: { type: 'number', description: 'Pagination offset' }
} }
},
_meta: {
labels: {
category: "campaigns",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -31,7 +38,14 @@ export class CampaignsTools {
type: 'object', type: 'object',
properties: { properties: {
campaignId: { type: 'string', description: 'Campaign ID' }, campaignId: { type: 'string', description: 'Campaign ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "campaigns",
access: "read",
complexity: "simple"
}
}
}, },
required: ['campaignId'] required: ['campaignId']
} }
@ -45,7 +59,14 @@ export class CampaignsTools {
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
name: { type: 'string', description: 'Campaign name' }, name: { type: 'string', description: 'Campaign name' },
type: { type: 'string', enum: ['email', 'sms', 'voicemail'], description: 'Campaign type' }, type: { type: 'string', enum: ['email', 'sms', 'voicemail'], description: 'Campaign type' },
status: { type: 'string', enum: ['draft', 'scheduled'], description: 'Initial status' } status: { type: 'string', enum: ['draft', 'scheduled'], description: 'Initial status' },
_meta: {
labels: {
category: "campaigns",
access: "write",
complexity: "simple"
}
}
}, },
required: ['name', 'type'] required: ['name', 'type']
} }
@ -59,7 +80,14 @@ export class CampaignsTools {
campaignId: { type: 'string', description: 'Campaign ID' }, campaignId: { type: 'string', description: 'Campaign ID' },
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
name: { type: 'string', description: 'Campaign name' }, name: { type: 'string', description: 'Campaign name' },
status: { type: 'string', enum: ['draft', 'scheduled', 'paused'], description: 'Campaign status' } status: { type: 'string', enum: ['draft', 'scheduled', 'paused'], description: 'Campaign status' },
_meta: {
labels: {
category: "campaigns",
access: "write",
complexity: "simple"
}
}
}, },
required: ['campaignId'] required: ['campaignId']
} }
@ -71,7 +99,14 @@ export class CampaignsTools {
type: 'object', type: 'object',
properties: { properties: {
campaignId: { type: 'string', description: 'Campaign ID' }, campaignId: { type: 'string', description: 'Campaign ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "campaigns",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['campaignId'] required: ['campaignId']
} }
@ -85,7 +120,14 @@ export class CampaignsTools {
type: 'object', type: 'object',
properties: { properties: {
campaignId: { type: 'string', description: 'Campaign ID' }, campaignId: { type: 'string', description: 'Campaign ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "campaigns",
access: "read",
complexity: "simple"
}
}
}, },
required: ['campaignId'] required: ['campaignId']
} }
@ -97,7 +139,14 @@ export class CampaignsTools {
type: 'object', type: 'object',
properties: { properties: {
campaignId: { type: 'string', description: 'Campaign ID' }, campaignId: { type: 'string', description: 'Campaign ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "campaigns",
access: "read",
complexity: "simple"
}
}
}, },
required: ['campaignId'] required: ['campaignId']
} }
@ -109,7 +158,14 @@ export class CampaignsTools {
type: 'object', type: 'object',
properties: { properties: {
campaignId: { type: 'string', description: 'Campaign ID' }, campaignId: { type: 'string', description: 'Campaign ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "campaigns",
access: "read",
complexity: "simple"
}
}
}, },
required: ['campaignId'] required: ['campaignId']
} }
@ -123,7 +179,14 @@ export class CampaignsTools {
type: 'object', type: 'object',
properties: { properties: {
campaignId: { type: 'string', description: 'Campaign ID' }, campaignId: { type: 'string', description: 'Campaign ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "campaigns",
access: "read",
complexity: "simple"
}
}
}, },
required: ['campaignId'] required: ['campaignId']
} }
@ -138,7 +201,14 @@ export class CampaignsTools {
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
status: { type: 'string', enum: ['sent', 'delivered', 'opened', 'clicked', 'bounced', 'unsubscribed'], description: 'Filter by recipient status' }, status: { type: 'string', enum: ['sent', 'delivered', 'opened', 'clicked', 'bounced', 'unsubscribed'], description: 'Filter by recipient status' },
limit: { type: 'number', description: 'Max results' }, limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' } offset: { type: 'number', description: 'Pagination offset' },
_meta: {
labels: {
category: "campaigns",
access: "read",
complexity: "simple"
}
}
}, },
required: ['campaignId'] required: ['campaignId']
} }
@ -155,6 +225,13 @@ export class CampaignsTools {
contactId: { type: 'string', description: 'Filter by contact ID' }, contactId: { type: 'string', description: 'Filter by contact ID' },
campaignId: { type: 'string', description: 'Filter by campaign ID' } campaignId: { type: 'string', description: 'Filter by campaign ID' }
} }
},
_meta: {
labels: {
category: "campaigns",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -164,7 +241,14 @@ export class CampaignsTools {
type: 'object', type: 'object',
properties: { properties: {
messageId: { type: 'string', description: 'Scheduled message ID' }, messageId: { type: 'string', description: 'Scheduled message ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "campaigns",
access: "write",
complexity: "simple"
}
}
}, },
required: ['messageId'] required: ['messageId']
} }

View File

@ -33,6 +33,13 @@ export class CompaniesTools {
description: 'Search query to filter companies' description: 'Search query to filter companies'
} }
} }
},
_meta: {
labels: {
category: "general",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -48,7 +55,14 @@ export class CompaniesTools {
locationId: { locationId: {
type: 'string', type: 'string',
description: 'Location ID (uses default if not provided)' description: 'Location ID (uses default if not provided)'
} },
_meta: {
labels: {
category: "general",
access: "read",
complexity: "simple"
}
}
}, },
required: ['companyId'] required: ['companyId']
} }
@ -127,7 +141,14 @@ export class CompaniesTools {
id: { type: 'string' }, id: { type: 'string' },
key: { type: 'string' }, key: { type: 'string' },
value: { type: 'string' } value: { type: 'string' }
} },
_meta: {
labels: {
category: "general",
access: "write",
complexity: "simple"
}
}
}, },
description: 'Custom field values' description: 'Custom field values'
}, },
@ -214,7 +235,14 @@ export class CompaniesTools {
id: { type: 'string' }, id: { type: 'string' },
key: { type: 'string' }, key: { type: 'string' },
value: { type: 'string' } value: { type: 'string' }
} },
_meta: {
labels: {
category: "general",
access: "write",
complexity: "simple"
}
}
}, },
description: 'Custom field values' description: 'Custom field values'
}, },
@ -240,7 +268,14 @@ export class CompaniesTools {
locationId: { locationId: {
type: 'string', type: 'string',
description: 'Location ID (uses default if not provided)' description: 'Location ID (uses default if not provided)'
} },
_meta: {
labels: {
category: "general",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['companyId'] required: ['companyId']
} }

View File

@ -81,6 +81,13 @@ export class ContactTools {
source: { type: 'string', description: 'Source of the contact' } source: { type: 'string', description: 'Source of the contact' }
}, },
required: ['email'] required: ['email']
},
_meta: {
labels: {
category: "contacts",
access: "write",
complexity: "simple"
}
} }
}, },
{ {
@ -94,6 +101,13 @@ export class ContactTools {
phone: { type: 'string', description: 'Filter by phone number' }, phone: { type: 'string', description: 'Filter by phone number' },
limit: { type: 'number', description: 'Maximum number of results (default: 25)' } limit: { type: 'number', description: 'Maximum number of results (default: 25)' }
} }
},
_meta: {
labels: {
category: "contacts",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -105,6 +119,13 @@ export class ContactTools {
contactId: { type: 'string', description: 'Contact ID' } contactId: { type: 'string', description: 'Contact ID' }
}, },
required: ['contactId'] required: ['contactId']
},
_meta: {
labels: {
category: "contacts",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -121,6 +142,13 @@ export class ContactTools {
tags: { type: 'array', items: { type: 'string' }, description: 'Tags to assign to contact' } tags: { type: 'array', items: { type: 'string' }, description: 'Tags to assign to contact' }
}, },
required: ['contactId'] required: ['contactId']
},
_meta: {
labels: {
category: "contacts",
access: "write",
complexity: "simple"
}
} }
}, },
{ {
@ -132,6 +160,13 @@ export class ContactTools {
contactId: { type: 'string', description: 'Contact ID' } contactId: { type: 'string', description: 'Contact ID' }
}, },
required: ['contactId'] required: ['contactId']
},
_meta: {
labels: {
category: "contacts",
access: "delete",
complexity: "simple"
}
} }
}, },
{ {
@ -144,6 +179,13 @@ export class ContactTools {
tags: { type: 'array', items: { type: 'string' }, description: 'Tags to add' } tags: { type: 'array', items: { type: 'string' }, description: 'Tags to add' }
}, },
required: ['contactId', 'tags'] required: ['contactId', 'tags']
},
_meta: {
labels: {
category: "contacts",
access: "write",
complexity: "simple"
}
} }
}, },
{ {
@ -156,6 +198,13 @@ export class ContactTools {
tags: { type: 'array', items: { type: 'string' }, description: 'Tags to remove' } tags: { type: 'array', items: { type: 'string' }, description: 'Tags to remove' }
}, },
required: ['contactId', 'tags'] required: ['contactId', 'tags']
},
_meta: {
labels: {
category: "contacts",
access: "write",
complexity: "simple"
}
} }
}, },
@ -169,6 +218,13 @@ export class ContactTools {
contactId: { type: 'string', description: 'Contact ID' } contactId: { type: 'string', description: 'Contact ID' }
}, },
required: ['contactId'] required: ['contactId']
},
_meta: {
labels: {
category: "contacts",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -185,6 +241,13 @@ export class ContactTools {
assignedTo: { type: 'string', description: 'User ID to assign task to' } assignedTo: { type: 'string', description: 'User ID to assign task to' }
}, },
required: ['contactId', 'title', 'dueDate'] required: ['contactId', 'title', 'dueDate']
},
_meta: {
labels: {
category: "contacts",
access: "write",
complexity: "simple"
}
} }
}, },
{ {
@ -197,6 +260,13 @@ export class ContactTools {
taskId: { type: 'string', description: 'Task ID' } taskId: { type: 'string', description: 'Task ID' }
}, },
required: ['contactId', 'taskId'] required: ['contactId', 'taskId']
},
_meta: {
labels: {
category: "contacts",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -214,6 +284,13 @@ export class ContactTools {
assignedTo: { type: 'string', description: 'User ID to assign task to' } assignedTo: { type: 'string', description: 'User ID to assign task to' }
}, },
required: ['contactId', 'taskId'] required: ['contactId', 'taskId']
},
_meta: {
labels: {
category: "contacts",
access: "write",
complexity: "simple"
}
} }
}, },
{ {
@ -226,6 +303,13 @@ export class ContactTools {
taskId: { type: 'string', description: 'Task ID' } taskId: { type: 'string', description: 'Task ID' }
}, },
required: ['contactId', 'taskId'] required: ['contactId', 'taskId']
},
_meta: {
labels: {
category: "contacts",
access: "delete",
complexity: "simple"
}
} }
}, },
{ {
@ -239,6 +323,13 @@ export class ContactTools {
completed: { type: 'boolean', description: 'Completion status' } completed: { type: 'boolean', description: 'Completion status' }
}, },
required: ['contactId', 'taskId', 'completed'] required: ['contactId', 'taskId', 'completed']
},
_meta: {
labels: {
category: "contacts",
access: "write",
complexity: "simple"
}
} }
}, },
@ -252,6 +343,13 @@ export class ContactTools {
contactId: { type: 'string', description: 'Contact ID' } contactId: { type: 'string', description: 'Contact ID' }
}, },
required: ['contactId'] required: ['contactId']
},
_meta: {
labels: {
category: "contacts",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -265,6 +363,13 @@ export class ContactTools {
userId: { type: 'string', description: 'User ID creating the note' } userId: { type: 'string', description: 'User ID creating the note' }
}, },
required: ['contactId', 'body'] required: ['contactId', 'body']
},
_meta: {
labels: {
category: "contacts",
access: "write",
complexity: "simple"
}
} }
}, },
{ {
@ -277,6 +382,13 @@ export class ContactTools {
noteId: { type: 'string', description: 'Note ID' } noteId: { type: 'string', description: 'Note ID' }
}, },
required: ['contactId', 'noteId'] required: ['contactId', 'noteId']
},
_meta: {
labels: {
category: "contacts",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -291,6 +403,13 @@ export class ContactTools {
userId: { type: 'string', description: 'User ID updating the note' } userId: { type: 'string', description: 'User ID updating the note' }
}, },
required: ['contactId', 'noteId', 'body'] required: ['contactId', 'noteId', 'body']
},
_meta: {
labels: {
category: "contacts",
access: "write",
complexity: "simple"
}
} }
}, },
{ {
@ -303,6 +422,13 @@ export class ContactTools {
noteId: { type: 'string', description: 'Note ID' } noteId: { type: 'string', description: 'Note ID' }
}, },
required: ['contactId', 'noteId'] required: ['contactId', 'noteId']
},
_meta: {
labels: {
category: "contacts",
access: "delete",
complexity: "simple"
}
} }
}, },
@ -321,6 +447,13 @@ export class ContactTools {
source: { type: 'string', description: 'Source of the contact' }, source: { type: 'string', description: 'Source of the contact' },
assignedTo: { type: 'string', description: 'User ID to assign contact to' } assignedTo: { type: 'string', description: 'User ID to assign contact to' }
} }
},
_meta: {
labels: {
category: "contacts",
access: "write",
complexity: "complex"
}
} }
}, },
{ {
@ -332,6 +465,13 @@ export class ContactTools {
email: { type: 'string', description: 'Email to check for duplicates' }, email: { type: 'string', description: 'Email to check for duplicates' },
phone: { type: 'string', description: 'Phone to check for duplicates' } phone: { type: 'string', description: 'Phone to check for duplicates' }
} }
},
_meta: {
labels: {
category: "contacts",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -346,6 +486,13 @@ export class ContactTools {
query: { type: 'string', description: 'Search query' } query: { type: 'string', description: 'Search query' }
}, },
required: ['businessId'] required: ['businessId']
},
_meta: {
labels: {
category: "contacts",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -357,6 +504,13 @@ export class ContactTools {
contactId: { type: 'string', description: 'Contact ID' } contactId: { type: 'string', description: 'Contact ID' }
}, },
required: ['contactId'] required: ['contactId']
},
_meta: {
labels: {
category: "contacts",
access: "read",
complexity: "simple"
}
} }
}, },
@ -373,6 +527,13 @@ export class ContactTools {
removeAllTags: { type: 'boolean', description: 'Remove all existing tags before adding new ones' } removeAllTags: { type: 'boolean', description: 'Remove all existing tags before adding new ones' }
}, },
required: ['contactIds', 'tags', 'operation'] required: ['contactIds', 'tags', 'operation']
},
_meta: {
labels: {
category: "contacts",
access: "write",
complexity: "batch"
}
} }
}, },
{ {
@ -385,6 +546,13 @@ export class ContactTools {
businessId: { type: 'string', description: 'Business ID (null to remove from business)' } businessId: { type: 'string', description: 'Business ID (null to remove from business)' }
}, },
required: ['contactIds'] required: ['contactIds']
},
_meta: {
labels: {
category: "contacts",
access: "write",
complexity: "batch"
}
} }
}, },
@ -399,6 +567,13 @@ export class ContactTools {
followers: { type: 'array', items: { type: 'string' }, description: 'Array of user IDs to add as followers' } followers: { type: 'array', items: { type: 'string' }, description: 'Array of user IDs to add as followers' }
}, },
required: ['contactId', 'followers'] required: ['contactId', 'followers']
},
_meta: {
labels: {
category: "contacts",
access: "write",
complexity: "simple"
}
} }
}, },
{ {
@ -411,6 +586,13 @@ export class ContactTools {
followers: { type: 'array', items: { type: 'string' }, description: 'Array of user IDs to remove as followers' } followers: { type: 'array', items: { type: 'string' }, description: 'Array of user IDs to remove as followers' }
}, },
required: ['contactId', 'followers'] required: ['contactId', 'followers']
},
_meta: {
labels: {
category: "contacts",
access: "write",
complexity: "simple"
}
} }
}, },
@ -425,6 +607,13 @@ export class ContactTools {
campaignId: { type: 'string', description: 'Campaign ID' } campaignId: { type: 'string', description: 'Campaign ID' }
}, },
required: ['contactId', 'campaignId'] required: ['contactId', 'campaignId']
},
_meta: {
labels: {
category: "contacts",
access: "write",
complexity: "simple"
}
} }
}, },
{ {
@ -437,6 +626,13 @@ export class ContactTools {
campaignId: { type: 'string', description: 'Campaign ID' } campaignId: { type: 'string', description: 'Campaign ID' }
}, },
required: ['contactId', 'campaignId'] required: ['contactId', 'campaignId']
},
_meta: {
labels: {
category: "contacts",
access: "write",
complexity: "simple"
}
} }
}, },
{ {
@ -448,6 +644,13 @@ export class ContactTools {
contactId: { type: 'string', description: 'Contact ID' } contactId: { type: 'string', description: 'Contact ID' }
}, },
required: ['contactId'] required: ['contactId']
},
_meta: {
labels: {
category: "contacts",
access: "write",
complexity: "batch"
}
} }
}, },
@ -463,6 +666,13 @@ export class ContactTools {
eventStartTime: { type: 'string', description: 'Event start time (ISO format)' } eventStartTime: { type: 'string', description: 'Event start time (ISO format)' }
}, },
required: ['contactId', 'workflowId'] required: ['contactId', 'workflowId']
},
_meta: {
labels: {
category: "contacts",
access: "write",
complexity: "simple"
}
} }
}, },
{ {
@ -476,6 +686,13 @@ export class ContactTools {
eventStartTime: { type: 'string', description: 'Event start time (ISO format)' } eventStartTime: { type: 'string', description: 'Event start time (ISO format)' }
}, },
required: ['contactId', 'workflowId'] required: ['contactId', 'workflowId']
},
_meta: {
labels: {
category: "contacts",
access: "write",
complexity: "simple"
}
} }
} }
]; ];

View File

@ -69,7 +69,14 @@ export class ConversationTools {
fromNumber: { fromNumber: {
type: 'string', type: 'string',
description: 'Optional: Phone number to send from (must be configured in GHL)' description: 'Optional: Phone number to send from (must be configured in GHL)'
} },
_meta: {
labels: {
category: "conversations",
access: "write",
complexity: "simple"
}
}
}, },
required: ['contactId', 'message'] required: ['contactId', 'message']
} }
@ -115,7 +122,14 @@ export class ConversationTools {
type: 'array', type: 'array',
items: { type: 'string' }, items: { type: 'string' },
description: 'Optional: Array of BCC email addresses' description: 'Optional: Array of BCC email addresses'
} },
_meta: {
labels: {
category: "conversations",
access: "write",
complexity: "simple"
}
}
}, },
required: ['contactId', 'subject'] required: ['contactId', 'subject']
} }
@ -152,6 +166,13 @@ export class ConversationTools {
description: 'Filter by user ID assigned to conversations' description: 'Filter by user ID assigned to conversations'
} }
} }
},
_meta: {
labels: {
category: "conversations",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -181,7 +202,14 @@ export class ConversationTools {
] ]
}, },
description: 'Filter messages by type (optional)' description: 'Filter messages by type (optional)'
} },
_meta: {
labels: {
category: "conversations",
access: "read",
complexity: "simple"
}
}
}, },
required: ['conversationId'] required: ['conversationId']
} }
@ -195,7 +223,14 @@ export class ConversationTools {
contactId: { contactId: {
type: 'string', type: 'string',
description: 'The unique ID of the contact to create conversation with' description: 'The unique ID of the contact to create conversation with'
} },
_meta: {
labels: {
category: "conversations",
access: "write",
complexity: "simple"
}
}
}, },
required: ['contactId'] required: ['contactId']
} }
@ -218,7 +253,14 @@ export class ConversationTools {
type: 'number', type: 'number',
description: 'Set the unread message count (0 to mark as read)', description: 'Set the unread message count (0 to mark as read)',
minimum: 0 minimum: 0
} },
_meta: {
labels: {
category: "conversations",
access: "write",
complexity: "simple"
}
}
}, },
required: ['conversationId'] required: ['conversationId']
} }
@ -243,6 +285,13 @@ export class ConversationTools {
default: 'unread' default: 'unread'
} }
} }
},
_meta: {
labels: {
category: "conversations",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -254,7 +303,14 @@ export class ConversationTools {
conversationId: { conversationId: {
type: 'string', type: 'string',
description: 'The unique ID of the conversation to delete' description: 'The unique ID of the conversation to delete'
} },
_meta: {
labels: {
category: "conversations",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['conversationId'] required: ['conversationId']
} }
@ -270,7 +326,14 @@ export class ConversationTools {
emailMessageId: { emailMessageId: {
type: 'string', type: 'string',
description: 'The unique ID of the email message to retrieve' description: 'The unique ID of the email message to retrieve'
} },
_meta: {
labels: {
category: "conversations",
access: "read",
complexity: "simple"
}
}
}, },
required: ['emailMessageId'] required: ['emailMessageId']
} }
@ -284,7 +347,14 @@ export class ConversationTools {
messageId: { messageId: {
type: 'string', type: 'string',
description: 'The unique ID of the message to retrieve' description: 'The unique ID of the message to retrieve'
} },
_meta: {
labels: {
category: "conversations",
access: "read",
complexity: "simple"
}
}
}, },
required: ['messageId'] required: ['messageId']
} }
@ -303,7 +373,14 @@ export class ConversationTools {
type: 'array', type: 'array',
items: { type: 'string' }, items: { type: 'string' },
description: 'Array of file URLs to upload as attachments' description: 'Array of file URLs to upload as attachments'
} },
_meta: {
labels: {
category: "conversations",
access: "write",
complexity: "simple"
}
}
}, },
required: ['conversationId', 'attachmentUrls'] required: ['conversationId', 'attachmentUrls']
} }
@ -330,7 +407,14 @@ export class ConversationTools {
code: { type: 'string' }, code: { type: 'string' },
type: { type: 'string' }, type: { type: 'string' },
message: { type: 'string' } message: { type: 'string' }
} },
_meta: {
labels: {
category: "conversations",
access: "write",
complexity: "simple"
}
}
}, },
emailMessageId: { emailMessageId: {
type: 'string', type: 'string',
@ -425,7 +509,14 @@ export class ConversationTools {
description: 'Call status' description: 'Call status'
} }
} }
} },
_meta: {
labels: {
category: "conversations",
access: "write",
complexity: "simple"
}
}
}, },
required: ['type', 'conversationId', 'conversationProviderId'] required: ['type', 'conversationId', 'conversationProviderId']
} }
@ -469,7 +560,14 @@ export class ConversationTools {
date: { date: {
type: 'string', type: 'string',
description: 'Date of the call (ISO format)' description: 'Date of the call (ISO format)'
} },
_meta: {
labels: {
category: "conversations",
access: "write",
complexity: "simple"
}
}
}, },
required: ['conversationId', 'conversationProviderId', 'to', 'from', 'status'] required: ['conversationId', 'conversationProviderId', 'to', 'from', 'status']
} }
@ -485,7 +583,14 @@ export class ConversationTools {
messageId: { messageId: {
type: 'string', type: 'string',
description: 'The unique ID of the call message to get recording for' description: 'The unique ID of the call message to get recording for'
} },
_meta: {
labels: {
category: "conversations",
access: "read",
complexity: "simple"
}
}
}, },
required: ['messageId'] required: ['messageId']
} }
@ -499,7 +604,14 @@ export class ConversationTools {
messageId: { messageId: {
type: 'string', type: 'string',
description: 'The unique ID of the call message to get transcription for' description: 'The unique ID of the call message to get transcription for'
} },
_meta: {
labels: {
category: "conversations",
access: "read",
complexity: "simple"
}
}
}, },
required: ['messageId'] required: ['messageId']
} }
@ -513,7 +625,14 @@ export class ConversationTools {
messageId: { messageId: {
type: 'string', type: 'string',
description: 'The unique ID of the call message to download transcription for' description: 'The unique ID of the call message to download transcription for'
} },
_meta: {
labels: {
category: "conversations",
access: "read",
complexity: "simple"
}
}
}, },
required: ['messageId'] required: ['messageId']
} }
@ -529,7 +648,14 @@ export class ConversationTools {
messageId: { messageId: {
type: 'string', type: 'string',
description: 'The unique ID of the scheduled message to cancel' description: 'The unique ID of the scheduled message to cancel'
} },
_meta: {
labels: {
category: "conversations",
access: "write",
complexity: "simple"
}
}
}, },
required: ['messageId'] required: ['messageId']
} }
@ -543,7 +669,14 @@ export class ConversationTools {
emailMessageId: { emailMessageId: {
type: 'string', type: 'string',
description: 'The unique ID of the scheduled email to cancel' description: 'The unique ID of the scheduled email to cancel'
} },
_meta: {
labels: {
category: "conversations",
access: "write",
complexity: "simple"
}
}
}, },
required: ['emailMessageId'] required: ['emailMessageId']
} }
@ -567,7 +700,14 @@ export class ConversationTools {
isTyping: { isTyping: {
type: 'boolean', type: 'boolean',
description: 'Whether the agent is currently typing' description: 'Whether the agent is currently typing'
} },
_meta: {
labels: {
category: "conversations",
access: "read",
complexity: "simple"
}
}
}, },
required: ['visitorId', 'conversationId', 'isTyping'] required: ['visitorId', 'conversationId', 'isTyping']
} }

View File

@ -21,6 +21,13 @@ export class CoursesTools {
limit: { type: 'number', description: 'Max results to return' }, limit: { type: 'number', description: 'Max results to return' },
offset: { type: 'number', description: 'Offset for pagination' } offset: { type: 'number', description: 'Offset for pagination' }
} }
},
_meta: {
labels: {
category: "courses",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -32,7 +39,14 @@ export class CoursesTools {
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
name: { type: 'string', description: 'Import job name' }, name: { type: 'string', description: 'Import job name' },
sourceUrl: { type: 'string', description: 'Source URL to import from' }, sourceUrl: { type: 'string', description: 'Source URL to import from' },
type: { type: 'string', description: 'Import type' } type: { type: 'string', description: 'Import type' },
_meta: {
labels: {
category: "courses",
access: "write",
complexity: "simple"
}
}
}, },
required: ['name'] required: ['name']
} }
@ -49,6 +63,13 @@ export class CoursesTools {
limit: { type: 'number', description: 'Max results' }, limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' } offset: { type: 'number', description: 'Pagination offset' }
} }
},
_meta: {
labels: {
category: "courses",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -58,7 +79,14 @@ export class CoursesTools {
type: 'object', type: 'object',
properties: { properties: {
productId: { type: 'string', description: 'Course product ID' }, productId: { type: 'string', description: 'Course product ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "courses",
access: "read",
complexity: "simple"
}
}
}, },
required: ['productId'] required: ['productId']
} }
@ -73,7 +101,14 @@ export class CoursesTools {
title: { type: 'string', description: 'Product title' }, title: { type: 'string', description: 'Product title' },
description: { type: 'string', description: 'Product description' }, description: { type: 'string', description: 'Product description' },
imageUrl: { type: 'string', description: 'Product image URL' }, imageUrl: { type: 'string', description: 'Product image URL' },
statementDescriptor: { type: 'string', description: 'Payment statement descriptor' } statementDescriptor: { type: 'string', description: 'Payment statement descriptor' },
_meta: {
labels: {
category: "courses",
access: "write",
complexity: "simple"
}
}
}, },
required: ['title'] required: ['title']
} }
@ -88,7 +123,14 @@ export class CoursesTools {
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
title: { type: 'string', description: 'Product title' }, title: { type: 'string', description: 'Product title' },
description: { type: 'string', description: 'Product description' }, description: { type: 'string', description: 'Product description' },
imageUrl: { type: 'string', description: 'Product image URL' } imageUrl: { type: 'string', description: 'Product image URL' },
_meta: {
labels: {
category: "courses",
access: "write",
complexity: "simple"
}
}
}, },
required: ['productId'] required: ['productId']
} }
@ -100,7 +142,14 @@ export class CoursesTools {
type: 'object', type: 'object',
properties: { properties: {
productId: { type: 'string', description: 'Course product ID' }, productId: { type: 'string', description: 'Course product ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "courses",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['productId'] required: ['productId']
} }
@ -117,6 +166,13 @@ export class CoursesTools {
limit: { type: 'number', description: 'Max results' }, limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' } offset: { type: 'number', description: 'Pagination offset' }
} }
},
_meta: {
labels: {
category: "courses",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -126,7 +182,14 @@ export class CoursesTools {
type: 'object', type: 'object',
properties: { properties: {
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
title: { type: 'string', description: 'Category title' } title: { type: 'string', description: 'Category title' },
_meta: {
labels: {
category: "courses",
access: "write",
complexity: "simple"
}
}
}, },
required: ['title'] required: ['title']
} }
@ -139,7 +202,14 @@ export class CoursesTools {
properties: { properties: {
categoryId: { type: 'string', description: 'Category ID' }, categoryId: { type: 'string', description: 'Category ID' },
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
title: { type: 'string', description: 'Category title' } title: { type: 'string', description: 'Category title' },
_meta: {
labels: {
category: "courses",
access: "write",
complexity: "simple"
}
}
}, },
required: ['categoryId', 'title'] required: ['categoryId', 'title']
} }
@ -151,7 +221,14 @@ export class CoursesTools {
type: 'object', type: 'object',
properties: { properties: {
categoryId: { type: 'string', description: 'Category ID' }, categoryId: { type: 'string', description: 'Category ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "courses",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['categoryId'] required: ['categoryId']
} }
@ -169,6 +246,13 @@ export class CoursesTools {
offset: { type: 'number', description: 'Pagination offset' }, offset: { type: 'number', description: 'Pagination offset' },
categoryId: { type: 'string', description: 'Filter by category' } categoryId: { type: 'string', description: 'Filter by category' }
} }
},
_meta: {
labels: {
category: "courses",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -178,7 +262,14 @@ export class CoursesTools {
type: 'object', type: 'object',
properties: { properties: {
courseId: { type: 'string', description: 'Course ID' }, courseId: { type: 'string', description: 'Course ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "courses",
access: "read",
complexity: "simple"
}
}
}, },
required: ['courseId'] required: ['courseId']
} }
@ -194,7 +285,14 @@ export class CoursesTools {
description: { type: 'string', description: 'Course description' }, description: { type: 'string', description: 'Course description' },
thumbnailUrl: { type: 'string', description: 'Course thumbnail URL' }, thumbnailUrl: { type: 'string', description: 'Course thumbnail URL' },
visibility: { type: 'string', enum: ['published', 'draft'], description: 'Course visibility' }, visibility: { type: 'string', enum: ['published', 'draft'], description: 'Course visibility' },
categoryId: { type: 'string', description: 'Category ID to place course in' } categoryId: { type: 'string', description: 'Category ID to place course in' },
_meta: {
labels: {
category: "courses",
access: "write",
complexity: "simple"
}
}
}, },
required: ['title'] required: ['title']
} }
@ -210,7 +308,14 @@ export class CoursesTools {
title: { type: 'string', description: 'Course title' }, title: { type: 'string', description: 'Course title' },
description: { type: 'string', description: 'Course description' }, description: { type: 'string', description: 'Course description' },
thumbnailUrl: { type: 'string', description: 'Course thumbnail URL' }, thumbnailUrl: { type: 'string', description: 'Course thumbnail URL' },
visibility: { type: 'string', enum: ['published', 'draft'], description: 'Course visibility' } visibility: { type: 'string', enum: ['published', 'draft'], description: 'Course visibility' },
_meta: {
labels: {
category: "courses",
access: "write",
complexity: "simple"
}
}
}, },
required: ['courseId'] required: ['courseId']
} }
@ -222,7 +327,14 @@ export class CoursesTools {
type: 'object', type: 'object',
properties: { properties: {
courseId: { type: 'string', description: 'Course ID' }, courseId: { type: 'string', description: 'Course ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "courses",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['courseId'] required: ['courseId']
} }
@ -236,7 +348,14 @@ export class CoursesTools {
type: 'object', type: 'object',
properties: { properties: {
courseId: { type: 'string', description: 'Course ID' }, courseId: { type: 'string', description: 'Course ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "courses",
access: "read",
complexity: "simple"
}
}
}, },
required: ['courseId'] required: ['courseId']
} }
@ -251,7 +370,14 @@ export class CoursesTools {
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
userId: { type: 'string', description: 'User ID of instructor' }, userId: { type: 'string', description: 'User ID of instructor' },
name: { type: 'string', description: 'Instructor display name' }, name: { type: 'string', description: 'Instructor display name' },
bio: { type: 'string', description: 'Instructor bio' } bio: { type: 'string', description: 'Instructor bio' },
_meta: {
labels: {
category: "courses",
access: "write",
complexity: "simple"
}
}
}, },
required: ['courseId'] required: ['courseId']
} }
@ -267,7 +393,14 @@ export class CoursesTools {
courseId: { type: 'string', description: 'Course ID' }, courseId: { type: 'string', description: 'Course ID' },
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
limit: { type: 'number', description: 'Max results' }, limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' } offset: { type: 'number', description: 'Pagination offset' },
_meta: {
labels: {
category: "courses",
access: "read",
complexity: "simple"
}
}
}, },
required: ['courseId'] required: ['courseId']
} }
@ -280,7 +413,14 @@ export class CoursesTools {
properties: { properties: {
courseId: { type: 'string', description: 'Course ID' }, courseId: { type: 'string', description: 'Course ID' },
postId: { type: 'string', description: 'Post/Lesson ID' }, postId: { type: 'string', description: 'Post/Lesson ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "courses",
access: "read",
complexity: "simple"
}
}
}, },
required: ['courseId', 'postId'] required: ['courseId', 'postId']
} }
@ -297,7 +437,14 @@ export class CoursesTools {
contentType: { type: 'string', enum: ['video', 'text', 'quiz', 'assignment'], description: 'Content type' }, contentType: { type: 'string', enum: ['video', 'text', 'quiz', 'assignment'], description: 'Content type' },
content: { type: 'string', description: 'Post content (text/HTML)' }, content: { type: 'string', description: 'Post content (text/HTML)' },
videoUrl: { type: 'string', description: 'Video URL (if video type)' }, videoUrl: { type: 'string', description: 'Video URL (if video type)' },
visibility: { type: 'string', enum: ['published', 'draft'], description: 'Visibility' } visibility: { type: 'string', enum: ['published', 'draft'], description: 'Visibility' },
_meta: {
labels: {
category: "courses",
access: "write",
complexity: "simple"
}
}
}, },
required: ['courseId', 'title'] required: ['courseId', 'title']
} }
@ -314,7 +461,14 @@ export class CoursesTools {
title: { type: 'string', description: 'Post/lesson title' }, title: { type: 'string', description: 'Post/lesson title' },
content: { type: 'string', description: 'Post content' }, content: { type: 'string', description: 'Post content' },
videoUrl: { type: 'string', description: 'Video URL' }, videoUrl: { type: 'string', description: 'Video URL' },
visibility: { type: 'string', enum: ['published', 'draft'], description: 'Visibility' } visibility: { type: 'string', enum: ['published', 'draft'], description: 'Visibility' },
_meta: {
labels: {
category: "courses",
access: "write",
complexity: "simple"
}
}
}, },
required: ['courseId', 'postId'] required: ['courseId', 'postId']
} }
@ -327,7 +481,14 @@ export class CoursesTools {
properties: { properties: {
courseId: { type: 'string', description: 'Course ID' }, courseId: { type: 'string', description: 'Course ID' },
postId: { type: 'string', description: 'Post/Lesson ID' }, postId: { type: 'string', description: 'Post/Lesson ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "courses",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['courseId', 'postId'] required: ['courseId', 'postId']
} }
@ -341,7 +502,14 @@ export class CoursesTools {
type: 'object', type: 'object',
properties: { properties: {
productId: { type: 'string', description: 'Course product ID' }, productId: { type: 'string', description: 'Course product ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "courses",
access: "read",
complexity: "simple"
}
}
}, },
required: ['productId'] required: ['productId']
} }
@ -358,7 +526,14 @@ export class CoursesTools {
price: { type: 'number', description: 'Price in cents' }, price: { type: 'number', description: 'Price in cents' },
currency: { type: 'string', description: 'Currency code (e.g., USD)' }, currency: { type: 'string', description: 'Currency code (e.g., USD)' },
type: { type: 'string', enum: ['one-time', 'subscription'], description: 'Payment type' }, type: { type: 'string', enum: ['one-time', 'subscription'], description: 'Payment type' },
interval: { type: 'string', enum: ['month', 'year'], description: 'Subscription interval (if subscription)' } interval: { type: 'string', enum: ['month', 'year'], description: 'Subscription interval (if subscription)' },
_meta: {
labels: {
category: "courses",
access: "write",
complexity: "simple"
}
}
}, },
required: ['productId', 'name', 'price'] required: ['productId', 'name', 'price']
} }
@ -373,7 +548,14 @@ export class CoursesTools {
offerId: { type: 'string', description: 'Offer ID' }, offerId: { type: 'string', description: 'Offer ID' },
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
name: { type: 'string', description: 'Offer name' }, name: { type: 'string', description: 'Offer name' },
price: { type: 'number', description: 'Price in cents' } price: { type: 'number', description: 'Price in cents' },
_meta: {
labels: {
category: "courses",
access: "write",
complexity: "simple"
}
}
}, },
required: ['productId', 'offerId'] required: ['productId', 'offerId']
} }
@ -386,7 +568,14 @@ export class CoursesTools {
properties: { properties: {
productId: { type: 'string', description: 'Course product ID' }, productId: { type: 'string', description: 'Course product ID' },
offerId: { type: 'string', description: 'Offer ID' }, offerId: { type: 'string', description: 'Offer ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "courses",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['productId', 'offerId'] required: ['productId', 'offerId']
} }
@ -402,7 +591,14 @@ export class CoursesTools {
courseId: { type: 'string', description: 'Course ID' }, courseId: { type: 'string', description: 'Course ID' },
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
limit: { type: 'number', description: 'Max results' }, limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' } offset: { type: 'number', description: 'Pagination offset' },
_meta: {
labels: {
category: "courses",
access: "read",
complexity: "simple"
}
}
}, },
required: ['courseId'] required: ['courseId']
} }
@ -415,7 +611,14 @@ export class CoursesTools {
properties: { properties: {
courseId: { type: 'string', description: 'Course ID' }, courseId: { type: 'string', description: 'Course ID' },
contactId: { type: 'string', description: 'Contact ID to enroll' }, contactId: { type: 'string', description: 'Contact ID to enroll' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "courses",
access: "read",
complexity: "simple"
}
}
}, },
required: ['courseId', 'contactId'] required: ['courseId', 'contactId']
} }
@ -428,7 +631,14 @@ export class CoursesTools {
properties: { properties: {
courseId: { type: 'string', description: 'Course ID' }, courseId: { type: 'string', description: 'Course ID' },
contactId: { type: 'string', description: 'Contact ID' }, contactId: { type: 'string', description: 'Contact ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "courses",
access: "write",
complexity: "simple"
}
}
}, },
required: ['courseId', 'contactId'] required: ['courseId', 'contactId']
} }
@ -443,7 +653,14 @@ export class CoursesTools {
properties: { properties: {
courseId: { type: 'string', description: 'Course ID' }, courseId: { type: 'string', description: 'Course ID' },
contactId: { type: 'string', description: 'Contact/Student ID' }, contactId: { type: 'string', description: 'Contact/Student ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "courses",
access: "read",
complexity: "simple"
}
}
}, },
required: ['courseId', 'contactId'] required: ['courseId', 'contactId']
} }
@ -458,7 +675,14 @@ export class CoursesTools {
postId: { type: 'string', description: 'Post/Lesson ID' }, postId: { type: 'string', description: 'Post/Lesson ID' },
contactId: { type: 'string', description: 'Contact/Student ID' }, contactId: { type: 'string', description: 'Contact/Student ID' },
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
completed: { type: 'boolean', description: 'Whether lesson is completed' } completed: { type: 'boolean', description: 'Whether lesson is completed' },
_meta: {
labels: {
category: "courses",
access: "write",
complexity: "simple"
}
}
}, },
required: ['courseId', 'postId', 'contactId', 'completed'] required: ['courseId', 'postId', 'contactId', 'completed']
} }

View File

@ -26,7 +26,14 @@ export class CustomFieldV2Tools {
id: { id: {
type: 'string', type: 'string',
description: 'The ID of the custom field or folder to retrieve' description: 'The ID of the custom field or folder to retrieve'
} },
_meta: {
labels: {
category: "custom-fields",
access: "read",
complexity: "simple"
}
}
}, },
required: ['id'] required: ['id']
} }
@ -74,7 +81,14 @@ export class CustomFieldV2Tools {
url: { url: {
type: 'string', type: 'string',
description: 'URL associated with the option (only for RADIO type)' description: 'URL associated with the option (only for RADIO type)'
} },
_meta: {
labels: {
category: "custom-fields",
access: "write",
complexity: "simple"
}
}
}, },
required: ['key', 'label'] required: ['key', 'label']
}, },
@ -160,7 +174,14 @@ export class CustomFieldV2Tools {
url: { url: {
type: 'string', type: 'string',
description: 'URL associated with the option (only for RADIO type)' description: 'URL associated with the option (only for RADIO type)'
} },
_meta: {
labels: {
category: "custom-fields",
access: "write",
complexity: "simple"
}
}
}, },
required: ['key', 'label'] required: ['key', 'label']
}, },
@ -188,7 +209,14 @@ export class CustomFieldV2Tools {
id: { id: {
type: 'string', type: 'string',
description: 'The ID of the custom field to delete' description: 'The ID of the custom field to delete'
} },
_meta: {
labels: {
category: "custom-fields",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['id'] required: ['id']
} }
@ -206,7 +234,14 @@ export class CustomFieldV2Tools {
locationId: { locationId: {
type: 'string', type: 'string',
description: 'GoHighLevel location ID (will use default if not provided)' description: 'GoHighLevel location ID (will use default if not provided)'
} },
_meta: {
labels: {
category: "custom-fields",
access: "read",
complexity: "simple"
}
}
}, },
required: ['objectKey'] required: ['objectKey']
} }
@ -229,7 +264,14 @@ export class CustomFieldV2Tools {
locationId: { locationId: {
type: 'string', type: 'string',
description: 'GoHighLevel location ID (will use default if not provided)' description: 'GoHighLevel location ID (will use default if not provided)'
} },
_meta: {
labels: {
category: "custom-fields",
access: "write",
complexity: "simple"
}
}
}, },
required: ['objectKey', 'name'] required: ['objectKey', 'name']
} }
@ -251,7 +293,14 @@ export class CustomFieldV2Tools {
locationId: { locationId: {
type: 'string', type: 'string',
description: 'GoHighLevel location ID (will use default if not provided)' description: 'GoHighLevel location ID (will use default if not provided)'
} },
_meta: {
labels: {
category: "custom-fields",
access: "write",
complexity: "simple"
}
}
}, },
required: ['id', 'name'] required: ['id', 'name']
} }
@ -269,7 +318,14 @@ export class CustomFieldV2Tools {
locationId: { locationId: {
type: 'string', type: 'string',
description: 'GoHighLevel location ID (will use default if not provided)' description: 'GoHighLevel location ID (will use default if not provided)'
} },
_meta: {
labels: {
category: "custom-fields",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['id'] required: ['id']
} }

View File

@ -40,7 +40,14 @@ export class EmailISVTools {
verify: { verify: {
type: 'string', type: 'string',
description: 'Email address to verify (if type=email) or contact ID (if type=contact)' description: 'Email address to verify (if type=email) or contact ID (if type=contact)'
} },
_meta: {
labels: {
category: "email",
access: "read",
complexity: "simple"
}
}
}, },
required: ['locationId', 'type', 'verify'] required: ['locationId', 'type', 'verify']
} }

View File

@ -50,6 +50,13 @@ export class EmailTools {
default: 0 default: 0
} }
} }
},
_meta: {
labels: {
category: "email",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -70,7 +77,14 @@ export class EmailTools {
type: 'boolean', type: 'boolean',
description: 'Whether the template is plain text.', description: 'Whether the template is plain text.',
default: false default: false
} },
_meta: {
labels: {
category: "email",
access: "write",
complexity: "simple"
}
}
}, },
required: ['title', 'html'] required: ['title', 'html']
} }
@ -92,6 +106,13 @@ export class EmailTools {
default: 0 default: 0
} }
} }
},
_meta: {
labels: {
category: "email",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -111,7 +132,14 @@ export class EmailTools {
previewText: { previewText: {
type: 'string', type: 'string',
description: 'The updated preview text for the template.' description: 'The updated preview text for the template.'
} },
_meta: {
labels: {
category: "email",
access: "write",
complexity: "simple"
}
}
}, },
required: ['templateId', 'html'] required: ['templateId', 'html']
} }
@ -125,7 +153,14 @@ export class EmailTools {
templateId: { templateId: {
type: 'string', type: 'string',
description: 'The ID of the template to delete.' description: 'The ID of the template to delete.'
} },
_meta: {
labels: {
category: "email",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['templateId'] required: ['templateId']
} }

View File

@ -33,6 +33,13 @@ export class FormsTools {
description: 'Filter by form type (e.g., "form", "survey")' description: 'Filter by form type (e.g., "form", "survey")'
} }
} }
},
_meta: {
labels: {
category: "forms",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -68,7 +75,14 @@ export class FormsTools {
page: { page: {
type: 'number', type: 'number',
description: 'Page number for pagination' description: 'Page number for pagination'
} },
_meta: {
labels: {
category: "forms",
access: "read",
complexity: "simple"
}
}
}, },
required: ['formId'] required: ['formId']
} }
@ -86,7 +100,14 @@ export class FormsTools {
locationId: { locationId: {
type: 'string', type: 'string',
description: 'Location ID (uses default if not provided)' description: 'Location ID (uses default if not provided)'
} },
_meta: {
labels: {
category: "forms",
access: "read",
complexity: "simple"
}
}
}, },
required: ['formId'] required: ['formId']
} }

View File

@ -46,6 +46,13 @@ export class FunnelsTools {
description: 'Filter by type (funnel or website)' description: 'Filter by type (funnel or website)'
} }
} }
},
_meta: {
labels: {
category: "funnels",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -61,7 +68,14 @@ export class FunnelsTools {
locationId: { locationId: {
type: 'string', type: 'string',
description: 'Location ID (uses default if not provided)' description: 'Location ID (uses default if not provided)'
} },
_meta: {
labels: {
category: "funnels",
access: "read",
complexity: "simple"
}
}
}, },
required: ['funnelId'] required: ['funnelId']
} }
@ -87,7 +101,14 @@ export class FunnelsTools {
limit: { limit: {
type: 'number', type: 'number',
description: 'Maximum number of pages to return' description: 'Maximum number of pages to return'
} },
_meta: {
labels: {
category: "funnels",
access: "read",
complexity: "simple"
}
}
}, },
required: ['funnelId'] required: ['funnelId']
} }
@ -105,7 +126,14 @@ export class FunnelsTools {
locationId: { locationId: {
type: 'string', type: 'string',
description: 'Location ID (uses default if not provided)' description: 'Location ID (uses default if not provided)'
} },
_meta: {
labels: {
category: "funnels",
access: "read",
complexity: "simple"
}
}
}, },
required: ['funnelId'] required: ['funnelId']
} }
@ -136,7 +164,14 @@ export class FunnelsTools {
pathName: { pathName: {
type: 'string', type: 'string',
description: 'Source path for the redirect' description: 'Source path for the redirect'
} },
_meta: {
labels: {
category: "funnels",
access: "write",
complexity: "simple"
}
}
}, },
required: ['funnelId', 'target', 'action'] required: ['funnelId', 'target', 'action']
} }
@ -171,7 +206,14 @@ export class FunnelsTools {
pathName: { pathName: {
type: 'string', type: 'string',
description: 'Source path for the redirect' description: 'Source path for the redirect'
} },
_meta: {
labels: {
category: "funnels",
access: "write",
complexity: "simple"
}
}
}, },
required: ['funnelId', 'redirectId'] required: ['funnelId', 'redirectId']
} }
@ -193,7 +235,14 @@ export class FunnelsTools {
locationId: { locationId: {
type: 'string', type: 'string',
description: 'Location ID (uses default if not provided)' description: 'Location ID (uses default if not provided)'
} },
_meta: {
labels: {
category: "funnels",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['funnelId', 'redirectId'] required: ['funnelId', 'redirectId']
} }
@ -219,7 +268,14 @@ export class FunnelsTools {
limit: { limit: {
type: 'number', type: 'number',
description: 'Maximum number of redirects to return' description: 'Maximum number of redirects to return'
} },
_meta: {
labels: {
category: "funnels",
access: "read",
complexity: "simple"
}
}
}, },
required: ['funnelId'] required: ['funnelId']
} }

View File

@ -85,7 +85,14 @@ export class InvoicesTools {
title: { type: 'string', description: 'Invoice title' }, title: { type: 'string', description: 'Invoice title' },
currency: { type: 'string', description: 'Currency code' }, currency: { type: 'string', description: 'Currency code' },
issueDate: { type: 'string', description: 'Issue date' }, issueDate: { type: 'string', description: 'Issue date' },
dueDate: { type: 'string', description: 'Due date' } dueDate: { type: 'string', description: 'Due date' },
_meta: {
labels: {
category: "invoices",
access: "write",
complexity: "simple"
}
}
}, },
required: ['name'] required: ['name']
} }
@ -101,7 +108,14 @@ export class InvoicesTools {
offset: { type: 'string', description: 'Offset for pagination', default: '0' }, offset: { type: 'string', description: 'Offset for pagination', default: '0' },
status: { type: 'string', description: 'Filter by status' }, status: { type: 'string', description: 'Filter by status' },
search: { type: 'string', description: 'Search term' }, search: { type: 'string', description: 'Search term' },
paymentMode: { type: 'string', enum: ['default', 'live', 'test'], description: 'Payment mode' } paymentMode: { type: 'string', enum: ['default', 'live', 'test'], description: 'Payment mode' },
_meta: {
labels: {
category: "invoices",
access: "read",
complexity: "simple"
}
}
}, },
required: ['limit', 'offset'] required: ['limit', 'offset']
} }
@ -113,7 +127,14 @@ export class InvoicesTools {
type: 'object', type: 'object',
properties: { properties: {
templateId: { type: 'string', description: 'Template ID' }, templateId: { type: 'string', description: 'Template ID' },
altId: { type: 'string', description: 'Location ID' } altId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "invoices",
access: "read",
complexity: "simple"
}
}
}, },
required: ['templateId'] required: ['templateId']
} }
@ -128,7 +149,14 @@ export class InvoicesTools {
altId: { type: 'string', description: 'Location ID' }, altId: { type: 'string', description: 'Location ID' },
name: { type: 'string', description: 'Template name' }, name: { type: 'string', description: 'Template name' },
title: { type: 'string', description: 'Invoice title' }, title: { type: 'string', description: 'Invoice title' },
currency: { type: 'string', description: 'Currency code' } currency: { type: 'string', description: 'Currency code' },
_meta: {
labels: {
category: "invoices",
access: "write",
complexity: "simple"
}
}
}, },
required: ['templateId'] required: ['templateId']
} }
@ -140,7 +168,14 @@ export class InvoicesTools {
type: 'object', type: 'object',
properties: { properties: {
templateId: { type: 'string', description: 'Template ID' }, templateId: { type: 'string', description: 'Template ID' },
altId: { type: 'string', description: 'Location ID' } altId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "invoices",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['templateId'] required: ['templateId']
} }
@ -157,7 +192,14 @@ export class InvoicesTools {
name: { type: 'string', description: 'Schedule name' }, name: { type: 'string', description: 'Schedule name' },
templateId: { type: 'string', description: 'Template ID' }, templateId: { type: 'string', description: 'Template ID' },
contactId: { type: 'string', description: 'Contact ID' }, contactId: { type: 'string', description: 'Contact ID' },
frequency: { type: 'string', description: 'Schedule frequency' } frequency: { type: 'string', description: 'Schedule frequency' },
_meta: {
labels: {
category: "invoices",
access: "write",
complexity: "simple"
}
}
}, },
required: ['name', 'templateId', 'contactId'] required: ['name', 'templateId', 'contactId']
} }
@ -172,7 +214,14 @@ export class InvoicesTools {
limit: { type: 'string', description: 'Number of results per page', default: '10' }, limit: { type: 'string', description: 'Number of results per page', default: '10' },
offset: { type: 'string', description: 'Offset for pagination', default: '0' }, offset: { type: 'string', description: 'Offset for pagination', default: '0' },
status: { type: 'string', description: 'Filter by status' }, status: { type: 'string', description: 'Filter by status' },
search: { type: 'string', description: 'Search term' } search: { type: 'string', description: 'Search term' },
_meta: {
labels: {
category: "invoices",
access: "read",
complexity: "simple"
}
}
}, },
required: ['limit', 'offset'] required: ['limit', 'offset']
} }
@ -184,7 +233,14 @@ export class InvoicesTools {
type: 'object', type: 'object',
properties: { properties: {
scheduleId: { type: 'string', description: 'Schedule ID' }, scheduleId: { type: 'string', description: 'Schedule ID' },
altId: { type: 'string', description: 'Location ID' } altId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "invoices",
access: "read",
complexity: "simple"
}
}
}, },
required: ['scheduleId'] required: ['scheduleId']
} }
@ -203,7 +259,14 @@ export class InvoicesTools {
currency: { type: 'string', description: 'Currency code' }, currency: { type: 'string', description: 'Currency code' },
issueDate: { type: 'string', description: 'Issue date' }, issueDate: { type: 'string', description: 'Issue date' },
dueDate: { type: 'string', description: 'Due date' }, dueDate: { type: 'string', description: 'Due date' },
items: { type: 'array', description: 'Invoice items' } items: { type: 'array', description: 'Invoice items' },
_meta: {
labels: {
category: "invoices",
access: "write",
complexity: "simple"
}
}
}, },
required: ['contactId', 'title'] required: ['contactId', 'title']
} }
@ -219,7 +282,14 @@ export class InvoicesTools {
offset: { type: 'string', description: 'Offset for pagination', default: '0' }, offset: { type: 'string', description: 'Offset for pagination', default: '0' },
status: { type: 'string', description: 'Filter by status' }, status: { type: 'string', description: 'Filter by status' },
contactId: { type: 'string', description: 'Filter by contact ID' }, contactId: { type: 'string', description: 'Filter by contact ID' },
search: { type: 'string', description: 'Search term' } search: { type: 'string', description: 'Search term' },
_meta: {
labels: {
category: "invoices",
access: "read",
complexity: "simple"
}
}
}, },
required: ['limit', 'offset'] required: ['limit', 'offset']
} }
@ -231,7 +301,14 @@ export class InvoicesTools {
type: 'object', type: 'object',
properties: { properties: {
invoiceId: { type: 'string', description: 'Invoice ID' }, invoiceId: { type: 'string', description: 'Invoice ID' },
altId: { type: 'string', description: 'Location ID' } altId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "invoices",
access: "read",
complexity: "simple"
}
}
}, },
required: ['invoiceId'] required: ['invoiceId']
} }
@ -246,7 +323,14 @@ export class InvoicesTools {
altId: { type: 'string', description: 'Location ID' }, altId: { type: 'string', description: 'Location ID' },
emailTo: { type: 'string', description: 'Email address to send to' }, emailTo: { type: 'string', description: 'Email address to send to' },
subject: { type: 'string', description: 'Email subject' }, subject: { type: 'string', description: 'Email subject' },
message: { type: 'string', description: 'Email message' } message: { type: 'string', description: 'Email message' },
_meta: {
labels: {
category: "invoices",
access: "write",
complexity: "simple"
}
}
}, },
required: ['invoiceId'] required: ['invoiceId']
} }
@ -264,7 +348,14 @@ export class InvoicesTools {
title: { type: 'string', description: 'Estimate title' }, title: { type: 'string', description: 'Estimate title' },
currency: { type: 'string', description: 'Currency code' }, currency: { type: 'string', description: 'Currency code' },
issueDate: { type: 'string', description: 'Issue date' }, issueDate: { type: 'string', description: 'Issue date' },
validUntil: { type: 'string', description: 'Valid until date' } validUntil: { type: 'string', description: 'Valid until date' },
_meta: {
labels: {
category: "invoices",
access: "write",
complexity: "simple"
}
}
}, },
required: ['contactId', 'title'] required: ['contactId', 'title']
} }
@ -280,7 +371,14 @@ export class InvoicesTools {
offset: { type: 'string', description: 'Offset for pagination', default: '0' }, offset: { type: 'string', description: 'Offset for pagination', default: '0' },
status: { type: 'string', enum: ['all', 'draft', 'sent', 'accepted', 'declined', 'invoiced', 'viewed'], description: 'Filter by status' }, status: { type: 'string', enum: ['all', 'draft', 'sent', 'accepted', 'declined', 'invoiced', 'viewed'], description: 'Filter by status' },
contactId: { type: 'string', description: 'Filter by contact ID' }, contactId: { type: 'string', description: 'Filter by contact ID' },
search: { type: 'string', description: 'Search term' } search: { type: 'string', description: 'Search term' },
_meta: {
labels: {
category: "invoices",
access: "read",
complexity: "simple"
}
}
}, },
required: ['limit', 'offset'] required: ['limit', 'offset']
} }
@ -295,7 +393,14 @@ export class InvoicesTools {
altId: { type: 'string', description: 'Location ID' }, altId: { type: 'string', description: 'Location ID' },
emailTo: { type: 'string', description: 'Email address to send to' }, emailTo: { type: 'string', description: 'Email address to send to' },
subject: { type: 'string', description: 'Email subject' }, subject: { type: 'string', description: 'Email subject' },
message: { type: 'string', description: 'Email message' } message: { type: 'string', description: 'Email message' },
_meta: {
labels: {
category: "invoices",
access: "write",
complexity: "simple"
}
}
}, },
required: ['estimateId'] required: ['estimateId']
} }
@ -309,7 +414,14 @@ export class InvoicesTools {
estimateId: { type: 'string', description: 'Estimate ID' }, estimateId: { type: 'string', description: 'Estimate ID' },
altId: { type: 'string', description: 'Location ID' }, altId: { type: 'string', description: 'Location ID' },
issueDate: { type: 'string', description: 'Invoice issue date' }, issueDate: { type: 'string', description: 'Invoice issue date' },
dueDate: { type: 'string', description: 'Invoice due date' } dueDate: { type: 'string', description: 'Invoice due date' },
_meta: {
labels: {
category: "invoices",
access: "write",
complexity: "simple"
}
}
}, },
required: ['estimateId'] required: ['estimateId']
} }
@ -324,6 +436,13 @@ export class InvoicesTools {
properties: { properties: {
altId: { type: 'string', description: 'Location ID' } altId: { type: 'string', description: 'Location ID' }
} }
},
_meta: {
labels: {
category: "invoices",
access: "read",
complexity: "simple"
}
} }
}, },
{ {

View File

@ -29,6 +29,13 @@ export class LinksTools {
description: 'Maximum number of links to return' description: 'Maximum number of links to return'
} }
} }
},
_meta: {
labels: {
category: "links",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -44,7 +51,14 @@ export class LinksTools {
locationId: { locationId: {
type: 'string', type: 'string',
description: 'Location ID (uses default if not provided)' description: 'Location ID (uses default if not provided)'
} },
_meta: {
labels: {
category: "links",
access: "read",
complexity: "simple"
}
}
}, },
required: ['linkId'] required: ['linkId']
} }
@ -74,7 +88,14 @@ export class LinksTools {
fieldValue: { fieldValue: {
type: 'string', type: 'string',
description: 'Value to set for the custom field' description: 'Value to set for the custom field'
} },
_meta: {
labels: {
category: "links",
access: "write",
complexity: "simple"
}
}
}, },
required: ['name', 'redirectTo'] required: ['name', 'redirectTo']
} }
@ -108,7 +129,14 @@ export class LinksTools {
fieldValue: { fieldValue: {
type: 'string', type: 'string',
description: 'Value to set for the custom field' description: 'Value to set for the custom field'
} },
_meta: {
labels: {
category: "links",
access: "write",
complexity: "simple"
}
}
}, },
required: ['linkId'] required: ['linkId']
} }
@ -126,7 +154,14 @@ export class LinksTools {
locationId: { locationId: {
type: 'string', type: 'string',
description: 'Location ID (uses default if not provided)' description: 'Location ID (uses default if not provided)'
} },
_meta: {
labels: {
category: "links",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['linkId'] required: ['linkId']
} }

View File

@ -81,6 +81,13 @@ export class LocationTools {
format: 'email' format: 'email'
} }
} }
},
_meta: {
labels: {
category: "locations",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -92,7 +99,14 @@ export class LocationTools {
locationId: { locationId: {
type: 'string', type: 'string',
description: 'The unique ID of the location to retrieve' description: 'The unique ID of the location to retrieve'
} },
_meta: {
labels: {
category: "locations",
access: "read",
complexity: "simple"
}
}
}, },
required: ['locationId'] required: ['locationId']
} }
@ -148,7 +162,14 @@ export class LocationTools {
properties: { properties: {
firstName: { type: 'string', description: 'Prospect first name' }, firstName: { type: 'string', description: 'Prospect first name' },
lastName: { type: 'string', description: 'Prospect last name' }, lastName: { type: 'string', description: 'Prospect last name' },
email: { type: 'string', format: 'email', description: 'Prospect email' } email: { type: 'string', format: 'email', description: 'Prospect email' },
_meta: {
labels: {
category: "locations",
access: "write",
complexity: "simple"
}
}
}, },
required: ['firstName', 'lastName', 'email'], required: ['firstName', 'lastName', 'email'],
description: 'Prospect information for the location' description: 'Prospect information for the location'
@ -210,7 +231,14 @@ export class LocationTools {
timezone: { timezone: {
type: 'string', type: 'string',
description: 'Updated timezone' description: 'Updated timezone'
} },
_meta: {
labels: {
category: "locations",
access: "write",
complexity: "simple"
}
}
}, },
required: ['locationId', 'companyId'] required: ['locationId', 'companyId']
} }
@ -229,7 +257,14 @@ export class LocationTools {
type: 'boolean', type: 'boolean',
description: 'Whether to delete associated Twilio account', description: 'Whether to delete associated Twilio account',
default: false default: false
} },
_meta: {
labels: {
category: "locations",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['locationId', 'deleteTwilioAccount'] required: ['locationId', 'deleteTwilioAccount']
} }
@ -245,7 +280,14 @@ export class LocationTools {
locationId: { locationId: {
type: 'string', type: 'string',
description: 'The location ID to get tags from' description: 'The location ID to get tags from'
} },
_meta: {
labels: {
category: "locations",
access: "read",
complexity: "simple"
}
}
}, },
required: ['locationId'] required: ['locationId']
} }
@ -263,7 +305,14 @@ export class LocationTools {
name: { name: {
type: 'string', type: 'string',
description: 'Name of the tag to create' description: 'Name of the tag to create'
} },
_meta: {
labels: {
category: "locations",
access: "write",
complexity: "simple"
}
}
}, },
required: ['locationId', 'name'] required: ['locationId', 'name']
} }
@ -281,7 +330,14 @@ export class LocationTools {
tagId: { tagId: {
type: 'string', type: 'string',
description: 'The tag ID to retrieve' description: 'The tag ID to retrieve'
} },
_meta: {
labels: {
category: "locations",
access: "read",
complexity: "simple"
}
}
}, },
required: ['locationId', 'tagId'] required: ['locationId', 'tagId']
} }
@ -303,7 +359,14 @@ export class LocationTools {
name: { name: {
type: 'string', type: 'string',
description: 'Updated name for the tag' description: 'Updated name for the tag'
} },
_meta: {
labels: {
category: "locations",
access: "write",
complexity: "simple"
}
}
}, },
required: ['locationId', 'tagId', 'name'] required: ['locationId', 'tagId', 'name']
} }
@ -321,7 +384,14 @@ export class LocationTools {
tagId: { tagId: {
type: 'string', type: 'string',
description: 'The tag ID to delete' description: 'The tag ID to delete'
} },
_meta: {
labels: {
category: "locations",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['locationId', 'tagId'] required: ['locationId', 'tagId']
} }
@ -369,7 +439,14 @@ export class LocationTools {
businessId: { businessId: {
type: 'string', type: 'string',
description: 'Business ID filter' description: 'Business ID filter'
} },
_meta: {
labels: {
category: "locations",
access: "read",
complexity: "simple"
}
}
}, },
required: ['locationId'] required: ['locationId']
} }
@ -391,7 +468,14 @@ export class LocationTools {
enum: ['contact', 'opportunity', 'all'], enum: ['contact', 'opportunity', 'all'],
description: 'Filter by model type (default: all)', description: 'Filter by model type (default: all)',
default: 'all' default: 'all'
} },
_meta: {
labels: {
category: "locations",
access: "read",
complexity: "simple"
}
}
}, },
required: ['locationId'] required: ['locationId']
} }
@ -428,7 +512,14 @@ export class LocationTools {
type: 'number', type: 'number',
description: 'Position/order of the field (default: 0)', description: 'Position/order of the field (default: 0)',
default: 0 default: 0
} },
_meta: {
labels: {
category: "locations",
access: "write",
complexity: "simple"
}
}
}, },
required: ['locationId', 'name', 'dataType'] required: ['locationId', 'name', 'dataType']
} }
@ -446,7 +537,14 @@ export class LocationTools {
customFieldId: { customFieldId: {
type: 'string', type: 'string',
description: 'The custom field ID to retrieve' description: 'The custom field ID to retrieve'
} },
_meta: {
labels: {
category: "locations",
access: "read",
complexity: "simple"
}
}
}, },
required: ['locationId', 'customFieldId'] required: ['locationId', 'customFieldId']
} }
@ -476,7 +574,14 @@ export class LocationTools {
position: { position: {
type: 'number', type: 'number',
description: 'Updated position/order' description: 'Updated position/order'
} },
_meta: {
labels: {
category: "locations",
access: "write",
complexity: "simple"
}
}
}, },
required: ['locationId', 'customFieldId', 'name'] required: ['locationId', 'customFieldId', 'name']
} }
@ -494,7 +599,14 @@ export class LocationTools {
customFieldId: { customFieldId: {
type: 'string', type: 'string',
description: 'The custom field ID to delete' description: 'The custom field ID to delete'
} },
_meta: {
labels: {
category: "locations",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['locationId', 'customFieldId'] required: ['locationId', 'customFieldId']
} }
@ -510,7 +622,14 @@ export class LocationTools {
locationId: { locationId: {
type: 'string', type: 'string',
description: 'The location ID' description: 'The location ID'
} },
_meta: {
labels: {
category: "locations",
access: "read",
complexity: "simple"
}
}
}, },
required: ['locationId'] required: ['locationId']
} }
@ -532,7 +651,14 @@ export class LocationTools {
value: { value: {
type: 'string', type: 'string',
description: 'Value to assign' description: 'Value to assign'
} },
_meta: {
labels: {
category: "locations",
access: "write",
complexity: "simple"
}
}
}, },
required: ['locationId', 'name', 'value'] required: ['locationId', 'name', 'value']
} }
@ -550,7 +676,14 @@ export class LocationTools {
customValueId: { customValueId: {
type: 'string', type: 'string',
description: 'The custom value ID to retrieve' description: 'The custom value ID to retrieve'
} },
_meta: {
labels: {
category: "locations",
access: "read",
complexity: "simple"
}
}
}, },
required: ['locationId', 'customValueId'] required: ['locationId', 'customValueId']
} }
@ -576,7 +709,14 @@ export class LocationTools {
value: { value: {
type: 'string', type: 'string',
description: 'Updated value' description: 'Updated value'
} },
_meta: {
labels: {
category: "locations",
access: "write",
complexity: "simple"
}
}
}, },
required: ['locationId', 'customValueId', 'name', 'value'] required: ['locationId', 'customValueId', 'name', 'value']
} }
@ -594,7 +734,14 @@ export class LocationTools {
customValueId: { customValueId: {
type: 'string', type: 'string',
description: 'The custom value ID to delete' description: 'The custom value ID to delete'
} },
_meta: {
labels: {
category: "locations",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['locationId', 'customValueId'] required: ['locationId', 'customValueId']
} }
@ -634,7 +781,14 @@ export class LocationTools {
type: 'string', type: 'string',
enum: ['sms', 'email', 'whatsapp'], enum: ['sms', 'email', 'whatsapp'],
description: 'Filter by template type' description: 'Filter by template type'
} },
_meta: {
labels: {
category: "locations",
access: "read",
complexity: "simple"
}
}
}, },
required: ['locationId', 'originId'] required: ['locationId', 'originId']
} }
@ -652,7 +806,14 @@ export class LocationTools {
templateId: { templateId: {
type: 'string', type: 'string',
description: 'The template ID to delete' description: 'The template ID to delete'
} },
_meta: {
labels: {
category: "locations",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['locationId', 'templateId'] required: ['locationId', 'templateId']
} }

View File

@ -80,7 +80,14 @@ export class MediaTools {
parentId: { parentId: {
type: 'string', type: 'string',
description: 'Parent folder ID to list files within a specific folder' description: 'Parent folder ID to list files within a specific folder'
} },
_meta: {
labels: {
category: "media",
access: "read",
complexity: "simple"
}
}
}, },
required: [] required: []
} }
@ -121,7 +128,14 @@ export class MediaTools {
altId: { altId: {
type: 'string', type: 'string',
description: 'Location or Agency ID (uses default location if not provided)' description: 'Location or Agency ID (uses default location if not provided)'
} },
_meta: {
labels: {
category: "media",
access: "write",
complexity: "simple"
}
}
}, },
required: [] required: []
} }
@ -145,7 +159,14 @@ export class MediaTools {
altId: { altId: {
type: 'string', type: 'string',
description: 'Location or Agency ID (uses default location if not provided)' description: 'Location or Agency ID (uses default location if not provided)'
} },
_meta: {
labels: {
category: "media",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['id'] required: ['id']
} }

View File

@ -20,6 +20,13 @@ export class OAuthTools {
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
companyId: { type: 'string', description: 'Company ID for agency-level apps' } companyId: { type: 'string', description: 'Company ID for agency-level apps' }
} }
},
_meta: {
labels: {
category: "oauth",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -29,7 +36,14 @@ export class OAuthTools {
type: 'object', type: 'object',
properties: { properties: {
appId: { type: 'string', description: 'OAuth App ID' }, appId: { type: 'string', description: 'OAuth App ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "oauth",
access: "read",
complexity: "simple"
}
}
}, },
required: ['appId'] required: ['appId']
} }
@ -45,7 +59,14 @@ export class OAuthTools {
skip: { type: 'number', description: 'Records to skip' }, skip: { type: 'number', description: 'Records to skip' },
limit: { type: 'number', description: 'Max results' }, limit: { type: 'number', description: 'Max results' },
query: { type: 'string', description: 'Search query' }, query: { type: 'string', description: 'Search query' },
isInstalled: { type: 'boolean', description: 'Filter by installation status' } isInstalled: { type: 'boolean', description: 'Filter by installation status' },
_meta: {
labels: {
category: "oauth",
access: "read",
complexity: "simple"
}
}
}, },
required: ['appId', 'companyId'] required: ['appId', 'companyId']
} }
@ -58,6 +79,13 @@ export class OAuthTools {
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: {} properties: {}
},
_meta: {
labels: {
category: "oauth",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -67,7 +95,14 @@ export class OAuthTools {
type: 'object', type: 'object',
properties: { properties: {
companyId: { type: 'string', description: 'Company/Agency ID' }, companyId: { type: 'string', description: 'Company/Agency ID' },
locationId: { type: 'string', description: 'Target Location ID' } locationId: { type: 'string', description: 'Target Location ID' },
_meta: {
labels: {
category: "oauth",
access: "read",
complexity: "simple"
}
}
}, },
required: ['companyId', 'locationId'] required: ['companyId', 'locationId']
} }
@ -82,6 +117,13 @@ export class OAuthTools {
properties: { properties: {
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' }
} }
},
_meta: {
labels: {
category: "oauth",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -91,7 +133,14 @@ export class OAuthTools {
type: 'object', type: 'object',
properties: { properties: {
integrationId: { type: 'string', description: 'Integration ID to disconnect' }, integrationId: { type: 'string', description: 'Integration ID to disconnect' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "oauth",
access: "read",
complexity: "simple"
}
}
}, },
required: ['integrationId'] required: ['integrationId']
} }
@ -106,6 +155,13 @@ export class OAuthTools {
properties: { properties: {
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' }
} }
},
_meta: {
labels: {
category: "oauth",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -120,7 +176,14 @@ export class OAuthTools {
type: 'array', type: 'array',
items: { type: 'string' }, items: { type: 'string' },
description: 'Permission scopes for the key' description: 'Permission scopes for the key'
} },
_meta: {
labels: {
category: "oauth",
access: "write",
complexity: "simple"
}
}
}, },
required: ['name'] required: ['name']
} }
@ -132,7 +195,14 @@ export class OAuthTools {
type: 'object', type: 'object',
properties: { properties: {
keyId: { type: 'string', description: 'API Key ID' }, keyId: { type: 'string', description: 'API Key ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "oauth",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['keyId'] required: ['keyId']
} }

View File

@ -48,7 +48,14 @@ export class ObjectTools {
locationId: { locationId: {
type: 'string', type: 'string',
description: 'Location ID (uses default if not provided)' description: 'Location ID (uses default if not provided)'
} },
_meta: {
labels: {
category: "objects",
access: "read",
complexity: "batch"
}
}
}, },
required: [] required: []
} }
@ -64,7 +71,14 @@ export class ObjectTools {
description: 'Singular and plural names for the custom object', description: 'Singular and plural names for the custom object',
properties: { properties: {
singular: { type: 'string', description: 'Singular name (e.g., "Pet")' }, singular: { type: 'string', description: 'Singular name (e.g., "Pet")' },
plural: { type: 'string', description: 'Plural name (e.g., "Pets")' } plural: { type: 'string', description: 'Plural name (e.g., "Pets")' },
_meta: {
labels: {
category: "objects",
access: "write",
complexity: "simple"
}
}
}, },
required: ['singular', 'plural'] required: ['singular', 'plural']
}, },
@ -112,7 +126,14 @@ export class ObjectTools {
type: 'boolean', type: 'boolean',
description: 'Whether to fetch all standard/custom fields of the object', description: 'Whether to fetch all standard/custom fields of the object',
default: true default: true
} },
_meta: {
labels: {
category: "objects",
access: "read",
complexity: "simple"
}
}
}, },
required: ['key'] required: ['key']
} }
@ -133,7 +154,14 @@ export class ObjectTools {
properties: { properties: {
singular: { type: 'string', description: 'Updated singular name' }, singular: { type: 'string', description: 'Updated singular name' },
plural: { type: 'string', description: 'Updated plural name' } plural: { type: 'string', description: 'Updated plural name' }
} },
_meta: {
labels: {
category: "objects",
access: "write",
complexity: "simple"
}
}
}, },
description: { description: {
type: 'string', type: 'string',
@ -181,7 +209,14 @@ export class ObjectTools {
description: 'Array of user IDs who follow this record (limited to 10)', description: 'Array of user IDs who follow this record (limited to 10)',
items: { type: 'string' }, items: { type: 'string' },
maxItems: 10 maxItems: 10
} },
_meta: {
labels: {
category: "objects",
access: "write",
complexity: "simple"
}
}
}, },
required: ['schemaKey', 'properties'] required: ['schemaKey', 'properties']
} }
@ -199,7 +234,14 @@ export class ObjectTools {
recordId: { recordId: {
type: 'string', type: 'string',
description: 'ID of the record to retrieve' description: 'ID of the record to retrieve'
} },
_meta: {
labels: {
category: "objects",
access: "read",
complexity: "simple"
}
}
}, },
required: ['schemaKey', 'recordId'] required: ['schemaKey', 'recordId']
} }
@ -237,7 +279,14 @@ export class ObjectTools {
description: 'Updated array of user IDs who follow this record', description: 'Updated array of user IDs who follow this record',
items: { type: 'string' }, items: { type: 'string' },
maxItems: 10 maxItems: 10
} },
_meta: {
labels: {
category: "objects",
access: "write",
complexity: "simple"
}
}
}, },
required: ['schemaKey', 'recordId'] required: ['schemaKey', 'recordId']
} }
@ -255,7 +304,14 @@ export class ObjectTools {
recordId: { recordId: {
type: 'string', type: 'string',
description: 'ID of the record to delete' description: 'ID of the record to delete'
} },
_meta: {
labels: {
category: "objects",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['schemaKey', 'recordId'] required: ['schemaKey', 'recordId']
} }
@ -295,7 +351,14 @@ export class ObjectTools {
type: 'array', type: 'array',
description: 'Cursor for pagination (returned from previous search)', description: 'Cursor for pagination (returned from previous search)',
items: { type: 'string' } items: { type: 'string' }
} },
_meta: {
labels: {
category: "objects",
access: "read",
complexity: "simple"
}
}
}, },
required: ['schemaKey', 'query'] required: ['schemaKey', 'query']
} }

View File

@ -69,6 +69,13 @@ export class OpportunityTools {
default: 20 default: 20
} }
} }
},
_meta: {
labels: {
category: "deals",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -77,6 +84,13 @@ export class OpportunityTools {
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: {} properties: {}
},
_meta: {
labels: {
category: "deals",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -88,7 +102,14 @@ export class OpportunityTools {
opportunityId: { opportunityId: {
type: 'string', type: 'string',
description: 'The unique ID of the opportunity to retrieve' description: 'The unique ID of the opportunity to retrieve'
} },
_meta: {
labels: {
category: "deals",
access: "read",
complexity: "simple"
}
}
}, },
required: ['opportunityId'] required: ['opportunityId']
} }
@ -124,7 +145,14 @@ export class OpportunityTools {
assignedTo: { assignedTo: {
type: 'string', type: 'string',
description: 'User ID to assign this opportunity to' description: 'User ID to assign this opportunity to'
} },
_meta: {
labels: {
category: "deals",
access: "write",
complexity: "simple"
}
}
}, },
required: ['name', 'pipelineId', 'contactId'] required: ['name', 'pipelineId', 'contactId']
} }
@ -143,7 +171,14 @@ export class OpportunityTools {
type: 'string', type: 'string',
description: 'New status for the opportunity', description: 'New status for the opportunity',
enum: ['open', 'won', 'lost', 'abandoned'] enum: ['open', 'won', 'lost', 'abandoned']
} },
_meta: {
labels: {
category: "deals",
access: "write",
complexity: "simple"
}
}
}, },
required: ['opportunityId', 'status'] required: ['opportunityId', 'status']
} }
@ -157,7 +192,14 @@ export class OpportunityTools {
opportunityId: { opportunityId: {
type: 'string', type: 'string',
description: 'The unique ID of the opportunity to delete' description: 'The unique ID of the opportunity to delete'
} },
_meta: {
labels: {
category: "deals",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['opportunityId'] required: ['opportunityId']
} }
@ -196,7 +238,14 @@ export class OpportunityTools {
assignedTo: { assignedTo: {
type: 'string', type: 'string',
description: 'Updated assigned user ID' description: 'Updated assigned user ID'
} },
_meta: {
labels: {
category: "deals",
access: "write",
complexity: "simple"
}
}
}, },
required: ['opportunityId'] required: ['opportunityId']
} }
@ -236,7 +285,14 @@ export class OpportunityTools {
assignedTo: { assignedTo: {
type: 'string', type: 'string',
description: 'User ID to assign this opportunity to' description: 'User ID to assign this opportunity to'
} },
_meta: {
labels: {
category: "deals",
access: "write",
complexity: "complex"
}
}
}, },
required: ['pipelineId', 'contactId'] required: ['pipelineId', 'contactId']
} }
@ -255,7 +311,14 @@ export class OpportunityTools {
type: 'array', type: 'array',
items: { type: 'string' }, items: { type: 'string' },
description: 'Array of user IDs to add as followers' description: 'Array of user IDs to add as followers'
} },
_meta: {
labels: {
category: "deals",
access: "write",
complexity: "simple"
}
}
}, },
required: ['opportunityId', 'followers'] required: ['opportunityId', 'followers']
} }
@ -274,7 +337,14 @@ export class OpportunityTools {
type: 'array', type: 'array',
items: { type: 'string' }, items: { type: 'string' },
description: 'Array of user IDs to remove as followers' description: 'Array of user IDs to remove as followers'
} },
_meta: {
labels: {
category: "deals",
access: "write",
complexity: "simple"
}
}
}, },
required: ['opportunityId', 'followers'] required: ['opportunityId', 'followers']
} }

View File

@ -69,7 +69,14 @@ export class PaymentsTools {
imageUrl: { imageUrl: {
type: 'string', type: 'string',
description: 'The URL to an image representing the integration provider' description: 'The URL to an image representing the integration provider'
} },
_meta: {
labels: {
category: "payments",
access: "write",
complexity: "simple"
}
}
}, },
required: ['altId', 'altType', 'uniqueName', 'title', 'provider', 'description', 'imageUrl'] required: ['altId', 'altType', 'uniqueName', 'title', 'provider', 'description', 'imageUrl']
} }
@ -98,7 +105,14 @@ export class PaymentsTools {
type: 'number', type: 'number',
description: 'Starting index for pagination', description: 'Starting index for pagination',
default: 0 default: 0
} },
_meta: {
labels: {
category: "payments",
access: "read",
complexity: "simple"
}
}
}, },
required: ['altId', 'altType'] required: ['altId', 'altType']
} }
@ -160,7 +174,14 @@ export class PaymentsTools {
type: 'number', type: 'number',
description: 'Starting index for pagination', description: 'Starting index for pagination',
default: 0 default: 0
} },
_meta: {
labels: {
category: "payments",
access: "read",
complexity: "simple"
}
}
}, },
required: ['altId', 'altType'] required: ['altId', 'altType']
} }
@ -186,7 +207,14 @@ export class PaymentsTools {
altType: { altType: {
type: 'string', type: 'string',
description: 'Alt Type (type of identifier)' description: 'Alt Type (type of identifier)'
} },
_meta: {
labels: {
category: "payments",
access: "read",
complexity: "simple"
}
}
}, },
required: ['orderId', 'altId', 'altType'] required: ['orderId', 'altId', 'altType']
} }
@ -231,7 +259,14 @@ export class PaymentsTools {
description: 'Tracking URL' description: 'Tracking URL'
} }
} }
} },
_meta: {
labels: {
category: "payments",
access: "write",
complexity: "simple"
}
}
}, },
items: { items: {
type: 'array', type: 'array',
@ -277,7 +312,14 @@ export class PaymentsTools {
type: 'string', type: 'string',
enum: ['location'], enum: ['location'],
description: 'Alt Type' description: 'Alt Type'
} },
_meta: {
labels: {
category: "payments",
access: "read",
complexity: "simple"
}
}
}, },
required: ['orderId', 'altId', 'altType'] required: ['orderId', 'altId', 'altType']
} }
@ -347,7 +389,14 @@ export class PaymentsTools {
type: 'number', type: 'number',
description: 'Starting index for pagination', description: 'Starting index for pagination',
default: 0 default: 0
} },
_meta: {
labels: {
category: "payments",
access: "read",
complexity: "simple"
}
}
}, },
required: ['altId', 'altType'] required: ['altId', 'altType']
} }
@ -373,7 +422,14 @@ export class PaymentsTools {
altType: { altType: {
type: 'string', type: 'string',
description: 'Alt Type (type of identifier)' description: 'Alt Type (type of identifier)'
} },
_meta: {
labels: {
category: "payments",
access: "read",
complexity: "simple"
}
}
}, },
required: ['transactionId', 'altId', 'altType'] required: ['transactionId', 'altId', 'altType']
} }
@ -436,7 +492,14 @@ export class PaymentsTools {
type: 'number', type: 'number',
description: 'Starting index for pagination', description: 'Starting index for pagination',
default: 0 default: 0
} },
_meta: {
labels: {
category: "payments",
access: "read",
complexity: "simple"
}
}
}, },
required: ['altId', 'altType'] required: ['altId', 'altType']
} }
@ -459,7 +522,14 @@ export class PaymentsTools {
type: 'string', type: 'string',
enum: ['location'], enum: ['location'],
description: 'Alt Type' description: 'Alt Type'
} },
_meta: {
labels: {
category: "payments",
access: "read",
complexity: "simple"
}
}
}, },
required: ['subscriptionId', 'altId', 'altType'] required: ['subscriptionId', 'altId', 'altType']
} }
@ -499,7 +569,14 @@ export class PaymentsTools {
search: { search: {
type: 'string', type: 'string',
description: 'Search term to filter coupons by name or code' description: 'Search term to filter coupons by name or code'
} },
_meta: {
labels: {
category: "payments",
access: "read",
complexity: "simple"
}
}
}, },
required: ['altId', 'altType'] required: ['altId', 'altType']
} }
@ -553,7 +630,14 @@ export class PaymentsTools {
description: 'Product IDs that the coupon applies to', description: 'Product IDs that the coupon applies to',
items: { items: {
type: 'string' type: 'string'
} },
_meta: {
labels: {
category: "payments",
access: "write",
complexity: "simple"
}
}
}, },
applyToFuturePayments: { applyToFuturePayments: {
type: 'boolean', type: 'boolean',
@ -643,7 +727,14 @@ export class PaymentsTools {
description: 'Product IDs that the coupon applies to', description: 'Product IDs that the coupon applies to',
items: { items: {
type: 'string' type: 'string'
} },
_meta: {
labels: {
category: "payments",
access: "write",
complexity: "simple"
}
}
}, },
applyToFuturePayments: { applyToFuturePayments: {
type: 'boolean', type: 'boolean',
@ -696,7 +787,14 @@ export class PaymentsTools {
id: { id: {
type: 'string', type: 'string',
description: 'Coupon ID' description: 'Coupon ID'
} },
_meta: {
labels: {
category: "payments",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['altId', 'altType', 'id'] required: ['altId', 'altType', 'id']
} }
@ -723,7 +821,14 @@ export class PaymentsTools {
code: { code: {
type: 'string', type: 'string',
description: 'Coupon code' description: 'Coupon code'
} },
_meta: {
labels: {
category: "payments",
access: "read",
complexity: "simple"
}
}
}, },
required: ['altId', 'altType', 'id', 'code'] required: ['altId', 'altType', 'id', 'code']
} }
@ -759,7 +864,14 @@ export class PaymentsTools {
imageUrl: { imageUrl: {
type: 'string', type: 'string',
description: 'Public image URL for the payment gateway logo' description: 'Public image URL for the payment gateway logo'
} },
_meta: {
labels: {
category: "payments",
access: "write",
complexity: "simple"
}
}
}, },
required: ['locationId', 'name', 'description', 'paymentsUrl', 'queryUrl', 'imageUrl'] required: ['locationId', 'name', 'description', 'paymentsUrl', 'queryUrl', 'imageUrl']
} }
@ -773,7 +885,14 @@ export class PaymentsTools {
locationId: { locationId: {
type: 'string', type: 'string',
description: 'Location ID' description: 'Location ID'
} },
_meta: {
labels: {
category: "payments",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['locationId'] required: ['locationId']
} }
@ -787,7 +906,14 @@ export class PaymentsTools {
locationId: { locationId: {
type: 'string', type: 'string',
description: 'Location ID' description: 'Location ID'
} },
_meta: {
labels: {
category: "payments",
access: "read",
complexity: "simple"
}
}
}, },
required: ['locationId'] required: ['locationId']
} }
@ -813,7 +939,14 @@ export class PaymentsTools {
publishableKey: { publishableKey: {
type: 'string', type: 'string',
description: 'Publishable key for live payments' description: 'Publishable key for live payments'
} },
_meta: {
labels: {
category: "payments",
access: "write",
complexity: "simple"
}
}
}, },
required: ['apiKey', 'publishableKey'] required: ['apiKey', 'publishableKey']
}, },
@ -849,7 +982,14 @@ export class PaymentsTools {
liveMode: { liveMode: {
type: 'boolean', type: 'boolean',
description: 'Whether to disconnect live or test mode config' description: 'Whether to disconnect live or test mode config'
} },
_meta: {
labels: {
category: "payments",
access: "read",
complexity: "simple"
}
}
}, },
required: ['locationId', 'liveMode'] required: ['locationId', 'liveMode']
} }

View File

@ -19,6 +19,13 @@ export class PhoneTools {
properties: { properties: {
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' }
} }
},
_meta: {
labels: {
category: "phone-numbers",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -28,7 +35,14 @@ export class PhoneTools {
type: 'object', type: 'object',
properties: { properties: {
phoneNumberId: { type: 'string', description: 'Phone Number ID' }, phoneNumberId: { type: 'string', description: 'Phone Number ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "phone-numbers",
access: "read",
complexity: "simple"
}
}
}, },
required: ['phoneNumberId'] required: ['phoneNumberId']
} }
@ -43,7 +57,14 @@ export class PhoneTools {
country: { type: 'string', description: 'Country code (e.g., US, CA)' }, country: { type: 'string', description: 'Country code (e.g., US, CA)' },
areaCode: { type: 'string', description: 'Area code to search' }, areaCode: { type: 'string', description: 'Area code to search' },
contains: { type: 'string', description: 'Number pattern to search for' }, contains: { type: 'string', description: 'Number pattern to search for' },
type: { type: 'string', enum: ['local', 'tollfree', 'mobile'], description: 'Number type' } type: { type: 'string', enum: ['local', 'tollfree', 'mobile'], description: 'Number type' },
_meta: {
labels: {
category: "phone-numbers",
access: "read",
complexity: "simple"
}
}
}, },
required: ['country'] required: ['country']
} }
@ -56,7 +77,14 @@ export class PhoneTools {
properties: { properties: {
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
phoneNumber: { type: 'string', description: 'Phone number to purchase' }, phoneNumber: { type: 'string', description: 'Phone number to purchase' },
name: { type: 'string', description: 'Friendly name for the number' } name: { type: 'string', description: 'Friendly name for the number' },
_meta: {
labels: {
category: "phone-numbers",
access: "read",
complexity: "simple"
}
}
}, },
required: ['phoneNumber'] required: ['phoneNumber']
} }
@ -72,7 +100,14 @@ export class PhoneTools {
name: { type: 'string', description: 'Friendly name' }, name: { type: 'string', description: 'Friendly name' },
forwardingNumber: { type: 'string', description: 'Number to forward calls to' }, forwardingNumber: { type: 'string', description: 'Number to forward calls to' },
callRecording: { type: 'boolean', description: 'Enable call recording' }, callRecording: { type: 'boolean', description: 'Enable call recording' },
whisperMessage: { type: 'string', description: 'Whisper message played to agent' } whisperMessage: { type: 'string', description: 'Whisper message played to agent' },
_meta: {
labels: {
category: "phone-numbers",
access: "write",
complexity: "simple"
}
}
}, },
required: ['phoneNumberId'] required: ['phoneNumberId']
} }
@ -84,7 +119,14 @@ export class PhoneTools {
type: 'object', type: 'object',
properties: { properties: {
phoneNumberId: { type: 'string', description: 'Phone Number ID' }, phoneNumberId: { type: 'string', description: 'Phone Number ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "phone-numbers",
access: "read",
complexity: "simple"
}
}
}, },
required: ['phoneNumberId'] required: ['phoneNumberId']
} }
@ -98,7 +140,14 @@ export class PhoneTools {
type: 'object', type: 'object',
properties: { properties: {
phoneNumberId: { type: 'string', description: 'Phone Number ID' }, phoneNumberId: { type: 'string', description: 'Phone Number ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "phone-numbers",
access: "write",
complexity: "batch"
}
}
}, },
required: ['phoneNumberId'] required: ['phoneNumberId']
} }
@ -114,7 +163,14 @@ export class PhoneTools {
enabled: { type: 'boolean', description: 'Enable forwarding' }, enabled: { type: 'boolean', description: 'Enable forwarding' },
forwardTo: { type: 'string', description: 'Number to forward to' }, forwardTo: { type: 'string', description: 'Number to forward to' },
ringTimeout: { type: 'number', description: 'Ring timeout in seconds' }, ringTimeout: { type: 'number', description: 'Ring timeout in seconds' },
voicemailEnabled: { type: 'boolean', description: 'Enable voicemail on no answer' } voicemailEnabled: { type: 'boolean', description: 'Enable voicemail on no answer' },
_meta: {
labels: {
category: "phone-numbers",
access: "write",
complexity: "batch"
}
}
}, },
required: ['phoneNumberId'] required: ['phoneNumberId']
} }
@ -129,6 +185,13 @@ export class PhoneTools {
properties: { properties: {
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' }
} }
},
_meta: {
labels: {
category: "phone-numbers",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -148,7 +211,14 @@ export class PhoneTools {
digit: { type: 'string', description: 'Digit to press (0-9, *, #)' }, digit: { type: 'string', description: 'Digit to press (0-9, *, #)' },
action: { type: 'string', description: 'Action type' }, action: { type: 'string', description: 'Action type' },
destination: { type: 'string', description: 'Action destination' } destination: { type: 'string', description: 'Action destination' }
} },
_meta: {
labels: {
category: "phone-numbers",
access: "write",
complexity: "simple"
}
}
}, },
description: 'Menu options' description: 'Menu options'
} }
@ -166,7 +236,14 @@ export class PhoneTools {
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
name: { type: 'string', description: 'Menu name' }, name: { type: 'string', description: 'Menu name' },
greeting: { type: 'string', description: 'Greeting message' }, greeting: { type: 'string', description: 'Greeting message' },
options: { type: 'array', description: 'Menu options' } options: { type: 'array', description: 'Menu options' },
_meta: {
labels: {
category: "phone-numbers",
access: "write",
complexity: "simple"
}
}
}, },
required: ['menuId'] required: ['menuId']
} }
@ -178,7 +255,14 @@ export class PhoneTools {
type: 'object', type: 'object',
properties: { properties: {
menuId: { type: 'string', description: 'IVR Menu ID' }, menuId: { type: 'string', description: 'IVR Menu ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "phone-numbers",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['menuId'] required: ['menuId']
} }
@ -193,6 +277,13 @@ export class PhoneTools {
properties: { properties: {
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' }
} }
},
_meta: {
labels: {
category: "phone-numbers",
access: "write",
complexity: "simple"
}
} }
}, },
{ {
@ -207,6 +298,13 @@ export class PhoneTools {
transcriptionEnabled: { type: 'boolean', description: 'Enable transcription' }, transcriptionEnabled: { type: 'boolean', description: 'Enable transcription' },
notificationEmail: { type: 'string', description: 'Email for voicemail notifications' } notificationEmail: { type: 'string', description: 'Email for voicemail notifications' }
} }
},
_meta: {
labels: {
category: "phone-numbers",
access: "write",
complexity: "simple"
}
} }
}, },
{ {
@ -221,6 +319,13 @@ export class PhoneTools {
limit: { type: 'number', description: 'Max results' }, limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' } offset: { type: 'number', description: 'Pagination offset' }
} }
},
_meta: {
labels: {
category: "phone-numbers",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -230,7 +335,14 @@ export class PhoneTools {
type: 'object', type: 'object',
properties: { properties: {
voicemailId: { type: 'string', description: 'Voicemail ID' }, voicemailId: { type: 'string', description: 'Voicemail ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "phone-numbers",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['voicemailId'] required: ['voicemailId']
} }
@ -245,6 +357,13 @@ export class PhoneTools {
properties: { properties: {
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' }
} }
},
_meta: {
labels: {
category: "phone-numbers",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -255,7 +374,14 @@ export class PhoneTools {
properties: { properties: {
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
phoneNumber: { type: 'string', description: 'Phone number to verify' }, phoneNumber: { type: 'string', description: 'Phone number to verify' },
name: { type: 'string', description: 'Friendly name' } name: { type: 'string', description: 'Friendly name' },
_meta: {
labels: {
category: "phone-numbers",
access: "write",
complexity: "simple"
}
}
}, },
required: ['phoneNumber'] required: ['phoneNumber']
} }
@ -268,7 +394,14 @@ export class PhoneTools {
properties: { properties: {
callerIdId: { type: 'string', description: 'Caller ID record ID' }, callerIdId: { type: 'string', description: 'Caller ID record ID' },
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
code: { type: 'string', description: 'Verification code' } code: { type: 'string', description: 'Verification code' },
_meta: {
labels: {
category: "phone-numbers",
access: "read",
complexity: "simple"
}
}
}, },
required: ['callerIdId', 'code'] required: ['callerIdId', 'code']
} }
@ -280,7 +413,14 @@ export class PhoneTools {
type: 'object', type: 'object',
properties: { properties: {
callerIdId: { type: 'string', description: 'Caller ID record ID' }, callerIdId: { type: 'string', description: 'Caller ID record ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "phone-numbers",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['callerIdId'] required: ['callerIdId']
} }

View File

@ -184,7 +184,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
description: { type: 'string', description: 'Product description' }, description: { type: 'string', description: 'Product description' },
image: { type: 'string', description: 'Product image URL' }, image: { type: 'string', description: 'Product image URL' },
availableInStore: { type: 'boolean', description: 'Whether product is available in store' }, availableInStore: { type: 'boolean', description: 'Whether product is available in store' },
slug: { type: 'string', description: 'Product URL slug' } slug: { type: 'string', description: 'Product URL slug' },
_meta: {
labels: {
category: "products",
access: "write",
complexity: "simple"
}
}
}, },
required: ['name', 'productType'] required: ['name', 'productType']
} }
@ -201,7 +208,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
search: { type: 'string', description: 'Search term for product names' }, search: { type: 'string', description: 'Search term for product names' },
storeId: { type: 'string', description: 'Filter by store ID' }, storeId: { type: 'string', description: 'Filter by store ID' },
includedInStore: { type: 'boolean', description: 'Filter by store inclusion status' }, includedInStore: { type: 'boolean', description: 'Filter by store inclusion status' },
availableInStore: { type: 'boolean', description: 'Filter by store availability' } availableInStore: { type: 'boolean', description: 'Filter by store availability' },
_meta: {
labels: {
category: "products",
access: "read",
complexity: "simple"
}
}
}, },
required: [] required: []
} }
@ -213,7 +227,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
type: 'object', type: 'object',
properties: { properties: {
productId: { type: 'string', description: 'Product ID to retrieve' }, productId: { type: 'string', description: 'Product ID to retrieve' },
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' } locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
_meta: {
labels: {
category: "products",
access: "read",
complexity: "simple"
}
}
}, },
required: ['productId'] required: ['productId']
} }
@ -234,7 +255,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
}, },
description: { type: 'string', description: 'Product description' }, description: { type: 'string', description: 'Product description' },
image: { type: 'string', description: 'Product image URL' }, image: { type: 'string', description: 'Product image URL' },
availableInStore: { type: 'boolean', description: 'Whether product is available in store' } availableInStore: { type: 'boolean', description: 'Whether product is available in store' },
_meta: {
labels: {
category: "products",
access: "write",
complexity: "simple"
}
}
}, },
required: ['productId'] required: ['productId']
} }
@ -246,7 +274,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
type: 'object', type: 'object',
properties: { properties: {
productId: { type: 'string', description: 'Product ID to delete' }, productId: { type: 'string', description: 'Product ID to delete' },
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' } locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
_meta: {
labels: {
category: "products",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['productId'] required: ['productId']
} }
@ -269,7 +304,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
currency: { type: 'string', description: 'Currency code (e.g., USD)' }, currency: { type: 'string', description: 'Currency code (e.g., USD)' },
amount: { type: 'number', description: 'Price amount in cents' }, amount: { type: 'number', description: 'Price amount in cents' },
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' }, locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
compareAtPrice: { type: 'number', description: 'Compare at price (for discounts)' } compareAtPrice: { type: 'number', description: 'Compare at price (for discounts)' },
_meta: {
labels: {
category: "products",
access: "write",
complexity: "simple"
}
}
}, },
required: ['productId', 'name', 'type', 'currency', 'amount'] required: ['productId', 'name', 'type', 'currency', 'amount']
} }
@ -283,7 +325,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
productId: { type: 'string', description: 'Product ID to list prices for' }, productId: { type: 'string', description: 'Product ID to list prices for' },
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' }, locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
limit: { type: 'number', description: 'Maximum number of prices to return' }, limit: { type: 'number', description: 'Maximum number of prices to return' },
offset: { type: 'number', description: 'Number of prices to skip' } offset: { type: 'number', description: 'Number of prices to skip' },
_meta: {
labels: {
category: "products",
access: "read",
complexity: "simple"
}
}
}, },
required: ['productId'] required: ['productId']
} }
@ -299,7 +348,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' }, locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
limit: { type: 'number', description: 'Maximum number of items to return' }, limit: { type: 'number', description: 'Maximum number of items to return' },
offset: { type: 'number', description: 'Number of items to skip' }, offset: { type: 'number', description: 'Number of items to skip' },
search: { type: 'string', description: 'Search term for inventory items' } search: { type: 'string', description: 'Search term for inventory items' },
_meta: {
labels: {
category: "products",
access: "read",
complexity: "simple"
}
}
}, },
required: [] required: []
} }
@ -322,7 +378,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
title: { type: 'string', description: 'SEO title' }, title: { type: 'string', description: 'SEO title' },
description: { type: 'string', description: 'SEO description' } description: { type: 'string', description: 'SEO description' }
} }
} },
_meta: {
labels: {
category: "products",
access: "write",
complexity: "simple"
}
}
}, },
required: ['name', 'slug'] required: ['name', 'slug']
} }
@ -336,7 +399,14 @@ ${params.includedInStore !== undefined ? `• **Store Status:** ${params.include
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' }, locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
limit: { type: 'number', description: 'Maximum number of collections to return' }, limit: { type: 'number', description: 'Maximum number of collections to return' },
offset: { type: 'number', description: 'Number of collections to skip' }, offset: { type: 'number', description: 'Number of collections to skip' },
name: { type: 'string', description: 'Search by collection name' } name: { type: 'string', description: 'Search by collection name' },
_meta: {
labels: {
category: "products",
access: "read",
complexity: "simple"
}
}
}, },
required: [] required: []
} }

View File

@ -19,7 +19,14 @@ export class ReportingTools {
properties: { properties: {
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' }, startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' } endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
_meta: {
labels: {
category: "analytics",
access: "read",
complexity: "simple"
}
}
}, },
required: ['startDate', 'endDate'] required: ['startDate', 'endDate']
} }
@ -36,7 +43,14 @@ export class ReportingTools {
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' }, startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' }, endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
userId: { type: 'string', description: 'Filter by user ID' }, userId: { type: 'string', description: 'Filter by user ID' },
type: { type: 'string', enum: ['inbound', 'outbound', 'all'], description: 'Call type filter' } type: { type: 'string', enum: ['inbound', 'outbound', 'all'], description: 'Call type filter' },
_meta: {
labels: {
category: "analytics",
access: "read",
complexity: "batch"
}
}
}, },
required: ['startDate', 'endDate'] required: ['startDate', 'endDate']
} }
@ -53,7 +67,14 @@ export class ReportingTools {
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' }, startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' }, endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
calendarId: { type: 'string', description: 'Filter by calendar ID' }, calendarId: { type: 'string', description: 'Filter by calendar ID' },
status: { type: 'string', enum: ['booked', 'confirmed', 'showed', 'noshow', 'cancelled'], description: 'Appointment status filter' } status: { type: 'string', enum: ['booked', 'confirmed', 'showed', 'noshow', 'cancelled'], description: 'Appointment status filter' },
_meta: {
labels: {
category: "analytics",
access: "read",
complexity: "simple"
}
}
}, },
required: ['startDate', 'endDate'] required: ['startDate', 'endDate']
} }
@ -70,7 +91,14 @@ export class ReportingTools {
pipelineId: { type: 'string', description: 'Filter by pipeline ID' }, pipelineId: { type: 'string', description: 'Filter by pipeline ID' },
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' }, startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' }, endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
userId: { type: 'string', description: 'Filter by assigned user' } userId: { type: 'string', description: 'Filter by assigned user' },
_meta: {
labels: {
category: "analytics",
access: "read",
complexity: "simple"
}
}
}, },
required: ['startDate', 'endDate'] required: ['startDate', 'endDate']
} }
@ -85,7 +113,14 @@ export class ReportingTools {
properties: { properties: {
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' }, startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' } endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
_meta: {
labels: {
category: "analytics",
access: "read",
complexity: "simple"
}
}
}, },
required: ['startDate', 'endDate'] required: ['startDate', 'endDate']
} }
@ -98,7 +133,14 @@ export class ReportingTools {
properties: { properties: {
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' }, startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' } endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
_meta: {
labels: {
category: "analytics",
access: "read",
complexity: "simple"
}
}
}, },
required: ['startDate', 'endDate'] required: ['startDate', 'endDate']
} }
@ -114,7 +156,14 @@ export class ReportingTools {
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
funnelId: { type: 'string', description: 'Filter by funnel ID' }, funnelId: { type: 'string', description: 'Filter by funnel ID' },
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' }, startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' } endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
_meta: {
labels: {
category: "analytics",
access: "read",
complexity: "simple"
}
}
}, },
required: ['startDate', 'endDate'] required: ['startDate', 'endDate']
} }
@ -130,7 +179,14 @@ export class ReportingTools {
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
platform: { type: 'string', enum: ['google', 'facebook', 'all'], description: 'Ad platform' }, platform: { type: 'string', enum: ['google', 'facebook', 'all'], description: 'Ad platform' },
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' }, startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' } endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
_meta: {
labels: {
category: "analytics",
access: "read",
complexity: "simple"
}
}
}, },
required: ['startDate', 'endDate'] required: ['startDate', 'endDate']
} }
@ -146,7 +202,14 @@ export class ReportingTools {
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
userId: { type: 'string', description: 'Filter by user ID' }, userId: { type: 'string', description: 'Filter by user ID' },
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' }, startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' } endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
_meta: {
labels: {
category: "analytics",
access: "read",
complexity: "simple"
}
}
}, },
required: ['startDate', 'endDate'] required: ['startDate', 'endDate']
} }
@ -164,6 +227,13 @@ export class ReportingTools {
startDate: { type: 'string', description: 'Start date for custom range' }, startDate: { type: 'string', description: 'Start date for custom range' },
endDate: { type: 'string', description: 'End date for custom range' } endDate: { type: 'string', description: 'End date for custom range' }
} }
},
_meta: {
labels: {
category: "analytics",
access: "read",
complexity: "simple"
}
} }
}, },
@ -177,7 +247,14 @@ export class ReportingTools {
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' }, startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' }, endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
source: { type: 'string', description: 'Filter by source' } source: { type: 'string', description: 'Filter by source' },
_meta: {
labels: {
category: "analytics",
access: "read",
complexity: "simple"
}
}
}, },
required: ['startDate', 'endDate'] required: ['startDate', 'endDate']
} }
@ -193,7 +270,14 @@ export class ReportingTools {
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' }, startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' }, endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
groupBy: { type: 'string', enum: ['day', 'week', 'month'], description: 'Group results by' } groupBy: { type: 'string', enum: ['day', 'week', 'month'], description: 'Group results by' },
_meta: {
labels: {
category: "analytics",
access: "read",
complexity: "simple"
}
}
}, },
required: ['startDate', 'endDate'] required: ['startDate', 'endDate']
} }

View File

@ -26,6 +26,13 @@ export class ReputationTools {
limit: { type: 'number', description: 'Max results' }, limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' } offset: { type: 'number', description: 'Pagination offset' }
} }
},
_meta: {
labels: {
category: "reputation",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -35,7 +42,14 @@ export class ReputationTools {
type: 'object', type: 'object',
properties: { properties: {
reviewId: { type: 'string', description: 'Review ID' }, reviewId: { type: 'string', description: 'Review ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "reputation",
access: "read",
complexity: "simple"
}
}
}, },
required: ['reviewId'] required: ['reviewId']
} }
@ -48,7 +62,14 @@ export class ReputationTools {
properties: { properties: {
reviewId: { type: 'string', description: 'Review ID' }, reviewId: { type: 'string', description: 'Review ID' },
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
reply: { type: 'string', description: 'Reply text' } reply: { type: 'string', description: 'Reply text' },
_meta: {
labels: {
category: "reputation",
access: "read",
complexity: "simple"
}
}
}, },
required: ['reviewId', 'reply'] required: ['reviewId', 'reply']
} }
@ -61,7 +82,14 @@ export class ReputationTools {
properties: { properties: {
reviewId: { type: 'string', description: 'Review ID' }, reviewId: { type: 'string', description: 'Review ID' },
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
reply: { type: 'string', description: 'Updated reply text' } reply: { type: 'string', description: 'Updated reply text' },
_meta: {
labels: {
category: "reputation",
access: "write",
complexity: "simple"
}
}
}, },
required: ['reviewId', 'reply'] required: ['reviewId', 'reply']
} }
@ -73,7 +101,14 @@ export class ReputationTools {
type: 'object', type: 'object',
properties: { properties: {
reviewId: { type: 'string', description: 'Review ID' }, reviewId: { type: 'string', description: 'Review ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "reputation",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['reviewId'] required: ['reviewId']
} }
@ -91,6 +126,13 @@ export class ReputationTools {
startDate: { type: 'string', description: 'Start date' }, startDate: { type: 'string', description: 'Start date' },
endDate: { type: 'string', description: 'End date' } endDate: { type: 'string', description: 'End date' }
} }
},
_meta: {
labels: {
category: "reputation",
access: "read",
complexity: "simple"
}
} }
}, },
@ -105,7 +147,14 @@ export class ReputationTools {
contactId: { type: 'string', description: 'Contact ID to request review from' }, contactId: { type: 'string', description: 'Contact ID to request review from' },
platform: { type: 'string', enum: ['google', 'facebook', 'yelp'], description: 'Platform to request review on' }, platform: { type: 'string', enum: ['google', 'facebook', 'yelp'], description: 'Platform to request review on' },
method: { type: 'string', enum: ['sms', 'email', 'both'], description: 'Delivery method' }, method: { type: 'string', enum: ['sms', 'email', 'both'], description: 'Delivery method' },
message: { type: 'string', description: 'Custom message (optional)' } message: { type: 'string', description: 'Custom message (optional)' },
_meta: {
labels: {
category: "reputation",
access: "write",
complexity: "simple"
}
}
}, },
required: ['contactId', 'platform', 'method'] required: ['contactId', 'platform', 'method']
} }
@ -122,6 +171,13 @@ export class ReputationTools {
limit: { type: 'number', description: 'Max results' }, limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' } offset: { type: 'number', description: 'Pagination offset' }
} }
},
_meta: {
labels: {
category: "reputation",
access: "read",
complexity: "simple"
}
} }
}, },
@ -134,6 +190,13 @@ export class ReputationTools {
properties: { properties: {
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' }
} }
},
_meta: {
labels: {
category: "reputation",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -144,6 +207,13 @@ export class ReputationTools {
properties: { properties: {
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' }
} }
},
_meta: {
labels: {
category: "reputation",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -153,7 +223,14 @@ export class ReputationTools {
type: 'object', type: 'object',
properties: { properties: {
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
platform: { type: 'string', enum: ['google', 'facebook', 'yelp'], description: 'Platform to disconnect' } platform: { type: 'string', enum: ['google', 'facebook', 'yelp'], description: 'Platform to disconnect' },
_meta: {
labels: {
category: "reputation",
access: "read",
complexity: "simple"
}
}
}, },
required: ['platform'] required: ['platform']
} }
@ -168,6 +245,13 @@ export class ReputationTools {
properties: { properties: {
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' }
} }
},
_meta: {
labels: {
category: "reputation",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -181,6 +265,13 @@ export class ReputationTools {
facebookLink: { type: 'string', description: 'Custom Facebook review link' }, facebookLink: { type: 'string', description: 'Custom Facebook review link' },
yelpLink: { type: 'string', description: 'Custom Yelp review link' } yelpLink: { type: 'string', description: 'Custom Yelp review link' }
} }
},
_meta: {
labels: {
category: "reputation",
access: "write",
complexity: "simple"
}
} }
}, },
@ -193,6 +284,13 @@ export class ReputationTools {
properties: { properties: {
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' }
} }
},
_meta: {
labels: {
category: "reputation",
access: "write",
complexity: "simple"
}
} }
}, },
{ {

View File

@ -36,7 +36,14 @@ export class SaasTools {
isActive: { isActive: {
type: 'boolean', type: 'boolean',
description: 'Filter by active status' description: 'Filter by active status'
} },
_meta: {
labels: {
category: "saas",
access: "read",
complexity: "simple"
}
}
}, },
required: ['companyId'] required: ['companyId']
} }
@ -54,7 +61,14 @@ export class SaasTools {
locationId: { locationId: {
type: 'string', type: 'string',
description: 'Location ID to retrieve' description: 'Location ID to retrieve'
} },
_meta: {
labels: {
category: "saas",
access: "read",
complexity: "simple"
}
}
}, },
required: ['companyId', 'locationId'] required: ['companyId', 'locationId']
} }
@ -81,7 +95,14 @@ export class SaasTools {
type: 'string', type: 'string',
enum: ['active', 'paused', 'cancelled'], enum: ['active', 'paused', 'cancelled'],
description: 'Subscription status' description: 'Subscription status'
} },
_meta: {
labels: {
category: "saas",
access: "write",
complexity: "simple"
}
}
}, },
required: ['companyId', 'locationId'] required: ['companyId', 'locationId']
} }
@ -103,7 +124,14 @@ export class SaasTools {
paused: { paused: {
type: 'boolean', type: 'boolean',
description: 'Whether to pause (true) or unpause (false)' description: 'Whether to pause (true) or unpause (false)'
} },
_meta: {
labels: {
category: "saas",
access: "read",
complexity: "simple"
}
}
}, },
required: ['companyId', 'locationId', 'paused'] required: ['companyId', 'locationId', 'paused']
} }
@ -125,7 +153,14 @@ export class SaasTools {
enabled: { enabled: {
type: 'boolean', type: 'boolean',
description: 'Whether to enable (true) or disable (false) SaaS' description: 'Whether to enable (true) or disable (false) SaaS'
} },
_meta: {
labels: {
category: "saas",
access: "read",
complexity: "simple"
}
}
}, },
required: ['companyId', 'locationId', 'enabled'] required: ['companyId', 'locationId', 'enabled']
} }
@ -151,7 +186,14 @@ export class SaasTools {
enabled: { enabled: {
type: 'boolean', type: 'boolean',
description: 'Whether rebilling is enabled' description: 'Whether rebilling is enabled'
} },
_meta: {
labels: {
category: "saas",
access: "write",
complexity: "simple"
}
}
}, },
required: ['companyId'] required: ['companyId']
} }

View File

@ -20,6 +20,13 @@ export class SmartListsTools {
limit: { type: 'number', description: 'Max results' }, limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' } offset: { type: 'number', description: 'Pagination offset' }
} }
},
_meta: {
labels: {
category: "smartlists",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -29,7 +36,14 @@ export class SmartListsTools {
type: 'object', type: 'object',
properties: { properties: {
smartListId: { type: 'string', description: 'Smart List ID' }, smartListId: { type: 'string', description: 'Smart List ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "smartlists",
access: "read",
complexity: "simple"
}
}
}, },
required: ['smartListId'] required: ['smartListId']
} }
@ -50,7 +64,14 @@ export class SmartListsTools {
field: { type: 'string', description: 'Field to filter on' }, field: { type: 'string', description: 'Field to filter on' },
operator: { type: 'string', description: 'Comparison operator (equals, contains, etc.)' }, operator: { type: 'string', description: 'Comparison operator (equals, contains, etc.)' },
value: { type: 'string', description: 'Filter value' } value: { type: 'string', description: 'Filter value' }
} },
_meta: {
labels: {
category: "smartlists",
access: "write",
complexity: "simple"
}
}
}, },
description: 'Filter conditions' description: 'Filter conditions'
}, },
@ -69,7 +90,14 @@ export class SmartListsTools {
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
name: { type: 'string', description: 'Smart list name' }, name: { type: 'string', description: 'Smart list name' },
filters: { type: 'array', description: 'Filter conditions' }, filters: { type: 'array', description: 'Filter conditions' },
filterOperator: { type: 'string', enum: ['AND', 'OR'], description: 'How to combine filters' } filterOperator: { type: 'string', enum: ['AND', 'OR'], description: 'How to combine filters' },
_meta: {
labels: {
category: "smartlists",
access: "write",
complexity: "simple"
}
}
}, },
required: ['smartListId'] required: ['smartListId']
} }
@ -81,7 +109,14 @@ export class SmartListsTools {
type: 'object', type: 'object',
properties: { properties: {
smartListId: { type: 'string', description: 'Smart List ID' }, smartListId: { type: 'string', description: 'Smart List ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "smartlists",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['smartListId'] required: ['smartListId']
} }
@ -95,7 +130,14 @@ export class SmartListsTools {
smartListId: { type: 'string', description: 'Smart List ID' }, smartListId: { type: 'string', description: 'Smart List ID' },
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
limit: { type: 'number', description: 'Max results' }, limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' } offset: { type: 'number', description: 'Pagination offset' },
_meta: {
labels: {
category: "smartlists",
access: "read",
complexity: "simple"
}
}
}, },
required: ['smartListId'] required: ['smartListId']
} }
@ -107,7 +149,14 @@ export class SmartListsTools {
type: 'object', type: 'object',
properties: { properties: {
smartListId: { type: 'string', description: 'Smart List ID' }, smartListId: { type: 'string', description: 'Smart List ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "smartlists",
access: "read",
complexity: "simple"
}
}
}, },
required: ['smartListId'] required: ['smartListId']
} }
@ -120,7 +169,14 @@ export class SmartListsTools {
properties: { properties: {
smartListId: { type: 'string', description: 'Smart List ID to duplicate' }, smartListId: { type: 'string', description: 'Smart List ID to duplicate' },
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
name: { type: 'string', description: 'Name for the duplicate' } name: { type: 'string', description: 'Name for the duplicate' },
_meta: {
labels: {
category: "smartlists",
access: "read",
complexity: "simple"
}
}
}, },
required: ['smartListId'] required: ['smartListId']
} }

View File

@ -27,7 +27,14 @@ export class SnapshotsTools {
limit: { limit: {
type: 'number', type: 'number',
description: 'Maximum number of snapshots to return' description: 'Maximum number of snapshots to return'
} },
_meta: {
labels: {
category: "snapshots",
access: "read",
complexity: "simple"
}
}
}, },
required: ['companyId'] required: ['companyId']
} }
@ -45,7 +52,14 @@ export class SnapshotsTools {
companyId: { companyId: {
type: 'string', type: 'string',
description: 'Company/Agency ID' description: 'Company/Agency ID'
} },
_meta: {
labels: {
category: "snapshots",
access: "read",
complexity: "simple"
}
}
}, },
required: ['snapshotId', 'companyId'] required: ['snapshotId', 'companyId']
} }
@ -71,7 +85,14 @@ export class SnapshotsTools {
description: { description: {
type: 'string', type: 'string',
description: 'Description of the snapshot' description: 'Description of the snapshot'
} },
_meta: {
labels: {
category: "snapshots",
access: "write",
complexity: "simple"
}
}
}, },
required: ['companyId', 'locationId', 'name'] required: ['companyId', 'locationId', 'name']
} }
@ -93,7 +114,14 @@ export class SnapshotsTools {
pushId: { pushId: {
type: 'string', type: 'string',
description: 'The push operation ID' description: 'The push operation ID'
} },
_meta: {
labels: {
category: "snapshots",
access: "read",
complexity: "simple"
}
}
}, },
required: ['snapshotId', 'companyId'] required: ['snapshotId', 'companyId']
} }
@ -115,7 +143,14 @@ export class SnapshotsTools {
locationId: { locationId: {
type: 'string', type: 'string',
description: 'Target location ID' description: 'Target location ID'
} },
_meta: {
labels: {
category: "snapshots",
access: "read",
complexity: "simple"
}
}
}, },
required: ['snapshotId', 'companyId', 'locationId'] required: ['snapshotId', 'companyId', 'locationId']
} }
@ -150,7 +185,14 @@ export class SnapshotsTools {
surveys: { type: 'boolean', description: 'Override existing surveys' }, surveys: { type: 'boolean', description: 'Override existing surveys' },
calendars: { type: 'boolean', description: 'Override existing calendars' }, calendars: { type: 'boolean', description: 'Override existing calendars' },
automations: { type: 'boolean', description: 'Override existing automations' }, automations: { type: 'boolean', description: 'Override existing automations' },
triggers: { type: 'boolean', description: 'Override existing triggers' } triggers: { type: 'boolean', description: 'Override existing triggers' },
_meta: {
labels: {
category: "snapshots",
access: "read",
complexity: "simple"
}
}
}, },
description: 'What to override vs skip' description: 'What to override vs skip'
} }

View File

@ -56,7 +56,14 @@ export class SocialMediaTools {
type: 'string', type: 'string',
enum: ['post', 'story', 'reel'], enum: ['post', 'story', 'reel'],
description: 'Type of post to search for' description: 'Type of post to search for'
} },
_meta: {
labels: {
category: "social-media",
access: "read",
complexity: "simple"
}
}
}, },
required: ['fromDate', 'toDate'] required: ['fromDate', 'toDate']
} }
@ -80,7 +87,14 @@ export class SocialMediaTools {
properties: { properties: {
url: { type: 'string', description: 'Media URL' }, url: { type: 'string', description: 'Media URL' },
caption: { type: 'string', description: 'Media caption' }, caption: { type: 'string', description: 'Media caption' },
type: { type: 'string', description: 'Media MIME type' } type: { type: 'string', description: 'Media MIME type' },
_meta: {
labels: {
category: "social-media",
access: "write",
complexity: "simple"
}
}
}, },
required: ['url'] required: ['url']
}, },
@ -116,7 +130,14 @@ export class SocialMediaTools {
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
postId: { type: 'string', description: 'Social media post ID' } postId: { type: 'string', description: 'Social media post ID' },
_meta: {
labels: {
category: "social-media",
access: "read",
complexity: "simple"
}
}
}, },
required: ['postId'] required: ['postId']
} }
@ -139,7 +160,14 @@ export class SocialMediaTools {
type: 'array', type: 'array',
items: { type: 'string' }, items: { type: 'string' },
description: 'Updated tag IDs' description: 'Updated tag IDs'
} },
_meta: {
labels: {
category: "social-media",
access: "write",
complexity: "simple"
}
}
}, },
required: ['postId'] required: ['postId']
} }
@ -150,7 +178,14 @@ export class SocialMediaTools {
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
postId: { type: 'string', description: 'Social media post ID to delete' } postId: { type: 'string', description: 'Social media post ID to delete' },
_meta: {
labels: {
category: "social-media",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['postId'] required: ['postId']
} }
@ -166,7 +201,14 @@ export class SocialMediaTools {
items: { type: 'string' }, items: { type: 'string' },
description: 'Array of post IDs to delete', description: 'Array of post IDs to delete',
maxItems: 50 maxItems: 50
} },
_meta: {
labels: {
category: "social-media",
access: "delete",
complexity: "batch"
}
}
}, },
required: ['postIds'] required: ['postIds']
} }
@ -180,6 +222,13 @@ export class SocialMediaTools {
type: 'object', type: 'object',
properties: {}, properties: {},
additionalProperties: false additionalProperties: false
},
_meta: {
labels: {
category: "social-media",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -190,7 +239,14 @@ export class SocialMediaTools {
properties: { properties: {
accountId: { type: 'string', description: 'Account ID to delete' }, accountId: { type: 'string', description: 'Account ID to delete' },
companyId: { type: 'string', description: 'Company ID' }, companyId: { type: 'string', description: 'Company ID' },
userId: { type: 'string', description: 'User ID' } userId: { type: 'string', description: 'User ID' },
_meta: {
labels: {
category: "social-media",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['accountId'] required: ['accountId']
} }
@ -203,7 +259,14 @@ export class SocialMediaTools {
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
file: { type: 'string', description: 'CSV file data (base64 or file path)' } file: { type: 'string', description: 'CSV file data (base64 or file path)' },
_meta: {
labels: {
category: "social-media",
access: "write",
complexity: "simple"
}
}
}, },
required: ['file'] required: ['file']
} }
@ -219,6 +282,13 @@ export class SocialMediaTools {
includeUsers: { type: 'boolean', description: 'Include user data' }, includeUsers: { type: 'boolean', description: 'Include user data' },
userId: { type: 'string', description: 'Filter by user ID' } userId: { type: 'string', description: 'Filter by user ID' }
} }
},
_meta: {
labels: {
category: "social-media",
access: "write",
complexity: "simple"
}
} }
}, },
{ {
@ -236,7 +306,14 @@ export class SocialMediaTools {
rowsCount: { type: 'number', description: 'Number of rows to process' }, rowsCount: { type: 'number', description: 'Number of rows to process' },
fileName: { type: 'string', description: 'CSV file name' }, fileName: { type: 'string', description: 'CSV file name' },
approver: { type: 'string', description: 'Approver user ID' }, approver: { type: 'string', description: 'Approver user ID' },
userId: { type: 'string', description: 'User ID' } userId: { type: 'string', description: 'User ID' },
_meta: {
labels: {
category: "social-media",
access: "write",
complexity: "simple"
}
}
}, },
required: ['accountIds', 'filePath', 'rowsCount', 'fileName'] required: ['accountIds', 'filePath', 'rowsCount', 'fileName']
} }
@ -253,6 +330,13 @@ export class SocialMediaTools {
limit: { type: 'number', description: 'Number to return', default: 10 }, limit: { type: 'number', description: 'Number to return', default: 10 },
skip: { type: 'number', description: 'Number to skip', default: 0 } skip: { type: 'number', description: 'Number to skip', default: 0 }
} }
},
_meta: {
labels: {
category: "social-media",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -261,7 +345,14 @@ export class SocialMediaTools {
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
categoryId: { type: 'string', description: 'Category ID' } categoryId: { type: 'string', description: 'Category ID' },
_meta: {
labels: {
category: "social-media",
access: "read",
complexity: "simple"
}
}
}, },
required: ['categoryId'] required: ['categoryId']
} }
@ -276,6 +367,13 @@ export class SocialMediaTools {
limit: { type: 'number', description: 'Number to return', default: 10 }, limit: { type: 'number', description: 'Number to return', default: 10 },
skip: { type: 'number', description: 'Number to skip', default: 0 } skip: { type: 'number', description: 'Number to skip', default: 0 }
} }
},
_meta: {
labels: {
category: "social-media",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -288,7 +386,14 @@ export class SocialMediaTools {
type: 'array', type: 'array',
items: { type: 'string' }, items: { type: 'string' },
description: 'Array of tag IDs' description: 'Array of tag IDs'
} },
_meta: {
labels: {
category: "social-media",
access: "read",
complexity: "simple"
}
}
}, },
required: ['tagIds'] required: ['tagIds']
} }
@ -308,7 +413,14 @@ export class SocialMediaTools {
}, },
userId: { type: 'string', description: 'User ID initiating OAuth' }, userId: { type: 'string', description: 'User ID initiating OAuth' },
page: { type: 'string', description: 'Page context' }, page: { type: 'string', description: 'Page context' },
reconnect: { type: 'boolean', description: 'Whether this is a reconnection' } reconnect: { type: 'boolean', description: 'Whether this is a reconnection' },
_meta: {
labels: {
category: "social-media",
access: "read",
complexity: "simple"
}
}
}, },
required: ['platform', 'userId'] required: ['platform', 'userId']
} }
@ -324,7 +436,14 @@ export class SocialMediaTools {
enum: ['google', 'facebook', 'instagram', 'linkedin', 'twitter', 'tiktok', 'tiktok-business'], enum: ['google', 'facebook', 'instagram', 'linkedin', 'twitter', 'tiktok', 'tiktok-business'],
description: 'Social media platform' description: 'Social media platform'
}, },
accountId: { type: 'string', description: 'OAuth account ID' } accountId: { type: 'string', description: 'OAuth account ID' },
_meta: {
labels: {
category: "social-media",
access: "read",
complexity: "simple"
}
}
}, },
required: ['platform', 'accountId'] required: ['platform', 'accountId']
} }

View File

@ -1068,7 +1068,14 @@ These settings control your store's shipping origin and email notification prefe
items: { items: {
type: 'object', type: 'object',
properties: { properties: {
code: { type: 'string', description: 'State code (e.g., CA, NY)' } code: { type: 'string', description: 'State code (e.g., CA, NY)' },
_meta: {
labels: {
category: "stores",
access: "write",
complexity: "simple"
}
}
}, },
required: ['code'] required: ['code']
} }
@ -1092,6 +1099,13 @@ These settings control your store's shipping origin and email notification prefe
offset: { type: 'number', description: 'Number of zones to skip (optional)' }, offset: { type: 'number', description: 'Number of zones to skip (optional)' },
withShippingRate: { type: 'boolean', description: 'Include shipping rates in response (optional)' } withShippingRate: { type: 'boolean', description: 'Include shipping rates in response (optional)' }
} }
},
_meta: {
labels: {
category: "stores",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -1102,7 +1116,14 @@ These settings control your store's shipping origin and email notification prefe
properties: { properties: {
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' }, locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
shippingZoneId: { type: 'string', description: 'ID of the shipping zone to retrieve' }, shippingZoneId: { type: 'string', description: 'ID of the shipping zone to retrieve' },
withShippingRate: { type: 'boolean', description: 'Include shipping rates in response (optional)' } withShippingRate: { type: 'boolean', description: 'Include shipping rates in response (optional)' },
_meta: {
labels: {
category: "stores",
access: "read",
complexity: "simple"
}
}
}, },
required: ['shippingZoneId'] required: ['shippingZoneId']
} }
@ -1129,7 +1150,14 @@ These settings control your store's shipping origin and email notification prefe
items: { items: {
type: 'object', type: 'object',
properties: { properties: {
code: { type: 'string', description: 'State code (e.g., CA, NY)' } code: { type: 'string', description: 'State code (e.g., CA, NY)' },
_meta: {
labels: {
category: "stores",
access: "write",
complexity: "simple"
}
}
}, },
required: ['code'] required: ['code']
} }
@ -1149,7 +1177,14 @@ These settings control your store's shipping origin and email notification prefe
type: 'object', type: 'object',
properties: { properties: {
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' }, locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
shippingZoneId: { type: 'string', description: 'ID of the shipping zone to delete' } shippingZoneId: { type: 'string', description: 'ID of the shipping zone to delete' },
_meta: {
labels: {
category: "stores",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['shippingZoneId'] required: ['shippingZoneId']
} }
@ -1170,7 +1205,14 @@ These settings control your store's shipping origin and email notification prefe
properties: { properties: {
street1: { type: 'string', description: 'Street address line 1' }, street1: { type: 'string', description: 'Street address line 1' },
city: { type: 'string', description: 'City' }, city: { type: 'string', description: 'City' },
country: { type: 'string', description: 'Country code' } country: { type: 'string', description: 'Country code' },
_meta: {
labels: {
category: "stores",
access: "read",
complexity: "simple"
}
}
}, },
required: ['street1', 'city', 'country'] required: ['street1', 'city', 'country']
}, },
@ -1203,7 +1245,14 @@ These settings control your store's shipping origin and email notification prefe
name: { type: 'string', description: 'Name of the shipping rate' }, name: { type: 'string', description: 'Name of the shipping rate' },
currency: { type: 'string', description: 'Currency code (e.g., USD)' }, currency: { type: 'string', description: 'Currency code (e.g., USD)' },
amount: { type: 'number', description: 'Shipping rate amount' }, amount: { type: 'number', description: 'Shipping rate amount' },
conditionType: { type: 'string', description: 'Condition type for rate calculation' } conditionType: { type: 'string', description: 'Condition type for rate calculation' },
_meta: {
labels: {
category: "stores",
access: "write",
complexity: "simple"
}
}
}, },
required: ['shippingZoneId', 'name', 'currency', 'amount', 'conditionType'] required: ['shippingZoneId', 'name', 'currency', 'amount', 'conditionType']
} }
@ -1215,7 +1264,14 @@ These settings control your store's shipping origin and email notification prefe
type: 'object', type: 'object',
properties: { properties: {
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' }, locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
shippingZoneId: { type: 'string', description: 'ID of the shipping zone' } shippingZoneId: { type: 'string', description: 'ID of the shipping zone' },
_meta: {
labels: {
category: "stores",
access: "read",
complexity: "simple"
}
}
}, },
required: ['shippingZoneId'] required: ['shippingZoneId']
} }
@ -1228,7 +1284,14 @@ These settings control your store's shipping origin and email notification prefe
properties: { properties: {
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' }, locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
shippingZoneId: { type: 'string', description: 'ID of the shipping zone' }, shippingZoneId: { type: 'string', description: 'ID of the shipping zone' },
shippingRateId: { type: 'string', description: 'ID of the shipping rate to retrieve' } shippingRateId: { type: 'string', description: 'ID of the shipping rate to retrieve' },
_meta: {
labels: {
category: "stores",
access: "read",
complexity: "simple"
}
}
}, },
required: ['shippingZoneId', 'shippingRateId'] required: ['shippingZoneId', 'shippingRateId']
} }
@ -1241,7 +1304,14 @@ These settings control your store's shipping origin and email notification prefe
properties: { properties: {
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' }, locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
shippingZoneId: { type: 'string', description: 'ID of the shipping zone' }, shippingZoneId: { type: 'string', description: 'ID of the shipping zone' },
shippingRateId: { type: 'string', description: 'ID of the shipping rate to update' } shippingRateId: { type: 'string', description: 'ID of the shipping rate to update' },
_meta: {
labels: {
category: "stores",
access: "write",
complexity: "simple"
}
}
}, },
required: ['shippingZoneId', 'shippingRateId'] required: ['shippingZoneId', 'shippingRateId']
} }
@ -1254,7 +1324,14 @@ These settings control your store's shipping origin and email notification prefe
properties: { properties: {
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' }, locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
shippingZoneId: { type: 'string', description: 'ID of the shipping zone' }, shippingZoneId: { type: 'string', description: 'ID of the shipping zone' },
shippingRateId: { type: 'string', description: 'ID of the shipping rate to delete' } shippingRateId: { type: 'string', description: 'ID of the shipping rate to delete' },
_meta: {
labels: {
category: "stores",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['shippingZoneId', 'shippingRateId'] required: ['shippingZoneId', 'shippingRateId']
} }
@ -1277,7 +1354,14 @@ These settings control your store's shipping origin and email notification prefe
type: 'object', type: 'object',
properties: { properties: {
name: { type: 'string', description: 'Service name' }, name: { type: 'string', description: 'Service name' },
value: { type: 'string', description: 'Service value' } value: { type: 'string', description: 'Service value' },
_meta: {
labels: {
category: "stores",
access: "write",
complexity: "simple"
}
}
}, },
required: ['name', 'value'] required: ['name', 'value']
} }
@ -1294,6 +1378,13 @@ These settings control your store's shipping origin and email notification prefe
properties: { properties: {
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' } locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' }
} }
},
_meta: {
labels: {
category: "stores",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -1303,7 +1394,14 @@ These settings control your store's shipping origin and email notification prefe
type: 'object', type: 'object',
properties: { properties: {
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' }, locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
shippingCarrierId: { type: 'string', description: 'ID of the shipping carrier to retrieve' } shippingCarrierId: { type: 'string', description: 'ID of the shipping carrier to retrieve' },
_meta: {
labels: {
category: "stores",
access: "read",
complexity: "simple"
}
}
}, },
required: ['shippingCarrierId'] required: ['shippingCarrierId']
} }
@ -1315,7 +1413,14 @@ These settings control your store's shipping origin and email notification prefe
type: 'object', type: 'object',
properties: { properties: {
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' }, locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
shippingCarrierId: { type: 'string', description: 'ID of the shipping carrier to update' } shippingCarrierId: { type: 'string', description: 'ID of the shipping carrier to update' },
_meta: {
labels: {
category: "stores",
access: "write",
complexity: "simple"
}
}
}, },
required: ['shippingCarrierId'] required: ['shippingCarrierId']
} }
@ -1327,7 +1432,14 @@ These settings control your store's shipping origin and email notification prefe
type: 'object', type: 'object',
properties: { properties: {
locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' }, locationId: { type: 'string', description: 'GHL Location ID (optional, uses default if not provided)' },
shippingCarrierId: { type: 'string', description: 'ID of the shipping carrier to delete' } shippingCarrierId: { type: 'string', description: 'ID of the shipping carrier to delete' },
_meta: {
labels: {
category: "stores",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['shippingCarrierId'] required: ['shippingCarrierId']
} }
@ -1349,7 +1461,14 @@ These settings control your store's shipping origin and email notification prefe
street1: { type: 'string', description: 'Street address line 1' }, street1: { type: 'string', description: 'Street address line 1' },
city: { type: 'string', description: 'City' }, city: { type: 'string', description: 'City' },
zip: { type: 'string', description: 'Postal/ZIP code' }, zip: { type: 'string', description: 'Postal/ZIP code' },
country: { type: 'string', description: 'Country code' } country: { type: 'string', description: 'Country code' },
_meta: {
labels: {
category: "stores",
access: "write",
complexity: "simple"
}
}
}, },
required: ['name', 'street1', 'city', 'zip', 'country'] required: ['name', 'street1', 'city', 'zip', 'country']
} }

View File

@ -31,7 +31,14 @@ export class SurveyTools {
type: { type: {
type: 'string', type: 'string',
description: 'Filter surveys by type (e.g., "folder")' description: 'Filter surveys by type (e.g., "folder")'
} },
_meta: {
labels: {
category: "surveys",
access: "read",
complexity: "simple"
}
}
}, },
additionalProperties: false additionalProperties: false
} }
@ -69,7 +76,14 @@ export class SurveyTools {
endAt: { endAt: {
type: 'string', type: 'string',
description: 'End date for filtering submissions (YYYY-MM-DD format)' description: 'End date for filtering submissions (YYYY-MM-DD format)'
} },
_meta: {
labels: {
category: "surveys",
access: "read",
complexity: "simple"
}
}
}, },
additionalProperties: false additionalProperties: false
} }

View File

@ -21,6 +21,13 @@ export class TemplatesTools {
limit: { type: 'number', description: 'Max results' }, limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' } offset: { type: 'number', description: 'Pagination offset' }
} }
},
_meta: {
labels: {
category: "templates",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -30,7 +37,14 @@ export class TemplatesTools {
type: 'object', type: 'object',
properties: { properties: {
templateId: { type: 'string', description: 'SMS Template ID' }, templateId: { type: 'string', description: 'SMS Template ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "templates",
access: "read",
complexity: "simple"
}
}
}, },
required: ['templateId'] required: ['templateId']
} }
@ -43,7 +57,14 @@ export class TemplatesTools {
properties: { properties: {
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
name: { type: 'string', description: 'Template name' }, name: { type: 'string', description: 'Template name' },
body: { type: 'string', description: 'SMS message body (can include merge fields like {{contact.first_name}})' } body: { type: 'string', description: 'SMS message body (can include merge fields like {{contact.first_name}})' },
_meta: {
labels: {
category: "templates",
access: "write",
complexity: "simple"
}
}
}, },
required: ['name', 'body'] required: ['name', 'body']
} }
@ -57,7 +78,14 @@ export class TemplatesTools {
templateId: { type: 'string', description: 'SMS Template ID' }, templateId: { type: 'string', description: 'SMS Template ID' },
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
name: { type: 'string', description: 'Template name' }, name: { type: 'string', description: 'Template name' },
body: { type: 'string', description: 'SMS message body' } body: { type: 'string', description: 'SMS message body' },
_meta: {
labels: {
category: "templates",
access: "write",
complexity: "simple"
}
}
}, },
required: ['templateId'] required: ['templateId']
} }
@ -69,7 +97,14 @@ export class TemplatesTools {
type: 'object', type: 'object',
properties: { properties: {
templateId: { type: 'string', description: 'SMS Template ID' }, templateId: { type: 'string', description: 'SMS Template ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "templates",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['templateId'] required: ['templateId']
} }
@ -84,6 +119,13 @@ export class TemplatesTools {
properties: { properties: {
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' }
} }
},
_meta: {
labels: {
category: "templates",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -94,7 +136,14 @@ export class TemplatesTools {
properties: { properties: {
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
name: { type: 'string', description: 'Template name' }, name: { type: 'string', description: 'Template name' },
audioUrl: { type: 'string', description: 'URL to audio file' } audioUrl: { type: 'string', description: 'URL to audio file' },
_meta: {
labels: {
category: "templates",
access: "write",
complexity: "simple"
}
}
}, },
required: ['name', 'audioUrl'] required: ['name', 'audioUrl']
} }
@ -106,7 +155,14 @@ export class TemplatesTools {
type: 'object', type: 'object',
properties: { properties: {
templateId: { type: 'string', description: 'Template ID' }, templateId: { type: 'string', description: 'Template ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "templates",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['templateId'] required: ['templateId']
} }
@ -123,6 +179,13 @@ export class TemplatesTools {
limit: { type: 'number', description: 'Max results' }, limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' } offset: { type: 'number', description: 'Pagination offset' }
} }
},
_meta: {
labels: {
category: "templates",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -135,7 +198,14 @@ export class TemplatesTools {
name: { type: 'string', description: 'Template name' }, name: { type: 'string', description: 'Template name' },
content: { type: 'string', description: 'Post content' }, content: { type: 'string', description: 'Post content' },
mediaUrls: { type: 'array', items: { type: 'string' }, description: 'Media URLs' }, mediaUrls: { type: 'array', items: { type: 'string' }, description: 'Media URLs' },
platforms: { type: 'array', items: { type: 'string' }, description: 'Target platforms' } platforms: { type: 'array', items: { type: 'string' }, description: 'Target platforms' },
_meta: {
labels: {
category: "templates",
access: "write",
complexity: "simple"
}
}
}, },
required: ['name', 'content'] required: ['name', 'content']
} }
@ -147,7 +217,14 @@ export class TemplatesTools {
type: 'object', type: 'object',
properties: { properties: {
templateId: { type: 'string', description: 'Template ID' }, templateId: { type: 'string', description: 'Template ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "templates",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['templateId'] required: ['templateId']
} }
@ -163,6 +240,13 @@ export class TemplatesTools {
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
status: { type: 'string', enum: ['approved', 'pending', 'rejected', 'all'], description: 'Template status' } status: { type: 'string', enum: ['approved', 'pending', 'rejected', 'all'], description: 'Template status' }
} }
},
_meta: {
labels: {
category: "templates",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -175,7 +259,14 @@ export class TemplatesTools {
name: { type: 'string', description: 'Template name' }, name: { type: 'string', description: 'Template name' },
category: { type: 'string', enum: ['marketing', 'utility', 'authentication'], description: 'Template category' }, category: { type: 'string', enum: ['marketing', 'utility', 'authentication'], description: 'Template category' },
language: { type: 'string', description: 'Language code (e.g., en_US)' }, language: { type: 'string', description: 'Language code (e.g., en_US)' },
components: { type: 'array', description: 'Template components (header, body, footer, buttons)' } components: { type: 'array', description: 'Template components (header, body, footer, buttons)' },
_meta: {
labels: {
category: "templates",
access: "write",
complexity: "simple"
}
}
}, },
required: ['name', 'category', 'language', 'components'] required: ['name', 'category', 'language', 'components']
} }
@ -187,7 +278,14 @@ export class TemplatesTools {
type: 'object', type: 'object',
properties: { properties: {
templateId: { type: 'string', description: 'Template ID' }, templateId: { type: 'string', description: 'Template ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "templates",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['templateId'] required: ['templateId']
} }
@ -203,6 +301,13 @@ export class TemplatesTools {
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
type: { type: 'string', enum: ['sms', 'email', 'all'], description: 'Snippet type' } type: { type: 'string', enum: ['sms', 'email', 'all'], description: 'Snippet type' }
} }
},
_meta: {
labels: {
category: "templates",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -215,7 +320,14 @@ export class TemplatesTools {
name: { type: 'string', description: 'Snippet name' }, name: { type: 'string', description: 'Snippet name' },
shortcut: { type: 'string', description: 'Keyboard shortcut (e.g., /thanks)' }, shortcut: { type: 'string', description: 'Keyboard shortcut (e.g., /thanks)' },
content: { type: 'string', description: 'Snippet content' }, content: { type: 'string', description: 'Snippet content' },
type: { type: 'string', enum: ['sms', 'email', 'both'], description: 'Snippet type' } type: { type: 'string', enum: ['sms', 'email', 'both'], description: 'Snippet type' },
_meta: {
labels: {
category: "templates",
access: "write",
complexity: "simple"
}
}
}, },
required: ['name', 'content'] required: ['name', 'content']
} }
@ -230,7 +342,14 @@ export class TemplatesTools {
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
name: { type: 'string', description: 'Snippet name' }, name: { type: 'string', description: 'Snippet name' },
shortcut: { type: 'string', description: 'Keyboard shortcut' }, shortcut: { type: 'string', description: 'Keyboard shortcut' },
content: { type: 'string', description: 'Snippet content' } content: { type: 'string', description: 'Snippet content' },
_meta: {
labels: {
category: "templates",
access: "write",
complexity: "simple"
}
}
}, },
required: ['snippetId'] required: ['snippetId']
} }
@ -242,7 +361,14 @@ export class TemplatesTools {
type: 'object', type: 'object',
properties: { properties: {
snippetId: { type: 'string', description: 'Snippet ID' }, snippetId: { type: 'string', description: 'Snippet ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "templates",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['snippetId'] required: ['snippetId']
} }

View File

@ -22,6 +22,13 @@ export class TriggersTools {
limit: { type: 'number', description: 'Max results' }, limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' } offset: { type: 'number', description: 'Pagination offset' }
} }
},
_meta: {
labels: {
category: "triggers",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -31,7 +38,14 @@ export class TriggersTools {
type: 'object', type: 'object',
properties: { properties: {
triggerId: { type: 'string', description: 'Trigger ID' }, triggerId: { type: 'string', description: 'Trigger ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "triggers",
access: "read",
complexity: "simple"
}
}
}, },
required: ['triggerId'] required: ['triggerId']
} }
@ -63,7 +77,14 @@ export class TriggersTools {
field: { type: 'string', description: 'Field to filter' }, field: { type: 'string', description: 'Field to filter' },
operator: { type: 'string', description: 'Comparison operator' }, operator: { type: 'string', description: 'Comparison operator' },
value: { type: 'string', description: 'Filter value' } value: { type: 'string', description: 'Filter value' }
} },
_meta: {
labels: {
category: "triggers",
access: "write",
complexity: "simple"
}
}
}, },
description: 'Conditions that must be met' description: 'Conditions that must be met'
}, },
@ -93,7 +114,14 @@ export class TriggersTools {
name: { type: 'string', description: 'Trigger name' }, name: { type: 'string', description: 'Trigger name' },
filters: { type: 'array', description: 'Filter conditions' }, filters: { type: 'array', description: 'Filter conditions' },
actions: { type: 'array', description: 'Actions to perform' }, actions: { type: 'array', description: 'Actions to perform' },
status: { type: 'string', enum: ['active', 'inactive'], description: 'Trigger status' } status: { type: 'string', enum: ['active', 'inactive'], description: 'Trigger status' },
_meta: {
labels: {
category: "triggers",
access: "write",
complexity: "simple"
}
}
}, },
required: ['triggerId'] required: ['triggerId']
} }
@ -105,7 +133,14 @@ export class TriggersTools {
type: 'object', type: 'object',
properties: { properties: {
triggerId: { type: 'string', description: 'Trigger ID' }, triggerId: { type: 'string', description: 'Trigger ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "triggers",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['triggerId'] required: ['triggerId']
} }
@ -117,7 +152,14 @@ export class TriggersTools {
type: 'object', type: 'object',
properties: { properties: {
triggerId: { type: 'string', description: 'Trigger ID' }, triggerId: { type: 'string', description: 'Trigger ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "triggers",
access: "read",
complexity: "simple"
}
}
}, },
required: ['triggerId'] required: ['triggerId']
} }
@ -129,7 +171,14 @@ export class TriggersTools {
type: 'object', type: 'object',
properties: { properties: {
triggerId: { type: 'string', description: 'Trigger ID' }, triggerId: { type: 'string', description: 'Trigger ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "triggers",
access: "read",
complexity: "simple"
}
}
}, },
required: ['triggerId'] required: ['triggerId']
} }
@ -142,6 +191,13 @@ export class TriggersTools {
properties: { properties: {
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' }
} }
},
_meta: {
labels: {
category: "triggers",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -156,7 +212,14 @@ export class TriggersTools {
startDate: { type: 'string', description: 'Start date' }, startDate: { type: 'string', description: 'Start date' },
endDate: { type: 'string', description: 'End date' }, endDate: { type: 'string', description: 'End date' },
limit: { type: 'number', description: 'Max results' }, limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' } offset: { type: 'number', description: 'Pagination offset' },
_meta: {
labels: {
category: "triggers",
access: "read",
complexity: "simple"
}
}
}, },
required: ['triggerId'] required: ['triggerId']
} }
@ -169,7 +232,14 @@ export class TriggersTools {
properties: { properties: {
triggerId: { type: 'string', description: 'Trigger ID' }, triggerId: { type: 'string', description: 'Trigger ID' },
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
testData: { type: 'object', description: 'Sample data to test with' } testData: { type: 'object', description: 'Sample data to test with' },
_meta: {
labels: {
category: "triggers",
access: "read",
complexity: "simple"
}
}
}, },
required: ['triggerId'] required: ['triggerId']
} }
@ -182,7 +252,14 @@ export class TriggersTools {
properties: { properties: {
triggerId: { type: 'string', description: 'Trigger ID to duplicate' }, triggerId: { type: 'string', description: 'Trigger ID to duplicate' },
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
name: { type: 'string', description: 'Name for the duplicate' } name: { type: 'string', description: 'Name for the duplicate' },
_meta: {
labels: {
category: "triggers",
access: "read",
complexity: "simple"
}
}
}, },
required: ['triggerId'] required: ['triggerId']
} }

View File

@ -50,6 +50,13 @@ export class UsersTools {
description: 'Sort direction' description: 'Sort direction'
} }
} }
},
_meta: {
labels: {
category: "users",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -65,7 +72,14 @@ export class UsersTools {
locationId: { locationId: {
type: 'string', type: 'string',
description: 'Location ID (uses default if not provided)' description: 'Location ID (uses default if not provided)'
} },
_meta: {
labels: {
category: "users",
access: "read",
complexity: "simple"
}
}
}, },
required: ['userId'] required: ['userId']
} }
@ -117,7 +131,14 @@ export class UsersTools {
type: 'array', type: 'array',
items: { type: 'string' }, items: { type: 'string' },
description: 'Scopes only assigned to this user' description: 'Scopes only assigned to this user'
} },
_meta: {
labels: {
category: "users",
access: "write",
complexity: "simple"
}
}
}, },
required: ['firstName', 'lastName', 'email'] required: ['firstName', 'lastName', 'email']
} }
@ -163,7 +184,14 @@ export class UsersTools {
permissions: { permissions: {
type: 'object', type: 'object',
description: 'User permissions object' description: 'User permissions object'
} },
_meta: {
labels: {
category: "users",
access: "write",
complexity: "simple"
}
}
}, },
required: ['userId'] required: ['userId']
} }
@ -181,7 +209,14 @@ export class UsersTools {
locationId: { locationId: {
type: 'string', type: 'string',
description: 'Location ID (uses default if not provided)' description: 'Location ID (uses default if not provided)'
} },
_meta: {
labels: {
category: "users",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['userId'] required: ['userId']
} }

View File

@ -18,6 +18,13 @@ export class WebhooksTools {
properties: { properties: {
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' }
} }
},
_meta: {
labels: {
category: "webhooks",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -27,7 +34,14 @@ export class WebhooksTools {
type: 'object', type: 'object',
properties: { properties: {
webhookId: { type: 'string', description: 'Webhook ID' }, webhookId: { type: 'string', description: 'Webhook ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "webhooks",
access: "read",
complexity: "simple"
}
}
}, },
required: ['webhookId'] required: ['webhookId']
} }
@ -46,7 +60,14 @@ export class WebhooksTools {
items: { type: 'string' }, items: { type: 'string' },
description: 'Events to subscribe to (e.g., contact.created, opportunity.updated)' description: 'Events to subscribe to (e.g., contact.created, opportunity.updated)'
}, },
secret: { type: 'string', description: 'Secret key for webhook signature verification' } secret: { type: 'string', description: 'Secret key for webhook signature verification' },
_meta: {
labels: {
category: "webhooks",
access: "write",
complexity: "simple"
}
}
}, },
required: ['name', 'url', 'events'] required: ['name', 'url', 'events']
} }
@ -66,7 +87,14 @@ export class WebhooksTools {
items: { type: 'string' }, items: { type: 'string' },
description: 'Events to subscribe to' description: 'Events to subscribe to'
}, },
active: { type: 'boolean', description: 'Whether webhook is active' } active: { type: 'boolean', description: 'Whether webhook is active' },
_meta: {
labels: {
category: "webhooks",
access: "write",
complexity: "simple"
}
}
}, },
required: ['webhookId'] required: ['webhookId']
} }
@ -78,7 +106,14 @@ export class WebhooksTools {
type: 'object', type: 'object',
properties: { properties: {
webhookId: { type: 'string', description: 'Webhook ID' }, webhookId: { type: 'string', description: 'Webhook ID' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "webhooks",
access: "delete",
complexity: "simple"
}
}
}, },
required: ['webhookId'] required: ['webhookId']
} }
@ -89,6 +124,13 @@ export class WebhooksTools {
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: {} properties: {}
},
_meta: {
labels: {
category: "webhooks",
access: "read",
complexity: "simple"
}
} }
}, },
{ {
@ -101,7 +143,14 @@ export class WebhooksTools {
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
limit: { type: 'number', description: 'Max results' }, limit: { type: 'number', description: 'Max results' },
offset: { type: 'number', description: 'Pagination offset' }, offset: { type: 'number', description: 'Pagination offset' },
status: { type: 'string', enum: ['success', 'failed', 'pending'], description: 'Filter by delivery status' } status: { type: 'string', enum: ['success', 'failed', 'pending'], description: 'Filter by delivery status' },
_meta: {
labels: {
category: "webhooks",
access: "read",
complexity: "simple"
}
}
}, },
required: ['webhookId'] required: ['webhookId']
} }
@ -114,7 +163,14 @@ export class WebhooksTools {
properties: { properties: {
webhookId: { type: 'string', description: 'Webhook ID' }, webhookId: { type: 'string', description: 'Webhook ID' },
logId: { type: 'string', description: 'Webhook log entry ID to retry' }, logId: { type: 'string', description: 'Webhook log entry ID to retry' },
locationId: { type: 'string', description: 'Location ID' } locationId: { type: 'string', description: 'Location ID' },
_meta: {
labels: {
category: "webhooks",
access: "read",
complexity: "simple"
}
}
}, },
required: ['webhookId', 'logId'] required: ['webhookId', 'logId']
} }
@ -127,7 +183,14 @@ export class WebhooksTools {
properties: { properties: {
webhookId: { type: 'string', description: 'Webhook ID' }, webhookId: { type: 'string', description: 'Webhook ID' },
locationId: { type: 'string', description: 'Location ID' }, locationId: { type: 'string', description: 'Location ID' },
eventType: { type: 'string', description: 'Event type to test' } eventType: { type: 'string', description: 'Event type to test' },
_meta: {
labels: {
category: "webhooks",
access: "read",
complexity: "simple"
}
}
}, },
required: ['webhookId', 'eventType'] required: ['webhookId', 'eventType']
} }

View File

@ -18,7 +18,14 @@ export class WorkflowTools {
locationId: { locationId: {
type: 'string', type: 'string',
description: 'The location ID to get workflows for. If not provided, uses the default location from configuration.' description: 'The location ID to get workflows for. If not provided, uses the default location from configuration.'
} },
_meta: {
labels: {
category: "workflows",
access: "read",
complexity: "simple"
}
}
}, },
additionalProperties: false additionalProperties: false
} }

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GHL Dynamic View</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./src/main.ts"></script>
</body>
</html>

1637
src/ui/json-render-app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,16 @@
{
"name": "ghl-json-render-app",
"private": true,
"type": "module",
"scripts": {
"build": "vite build"
},
"devDependencies": {
"vite": "^6.3.5",
"vite-plugin-singlefile": "^2.0.3",
"typescript": "^5.8.3"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.39.0"
}
}

View File

@ -0,0 +1,250 @@
/**
* Chart Components for GHL Dynamic View
* Pure CSS/SVG charts no external libraries
*/
type ChartFn = (props: any, children: string) => string;
function esc(s: unknown): string {
if (s === null || s === undefined) return '';
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
const chartPalette = ['#4f46e5', '#7c3aed', '#16a34a', '#3b82f6', '#eab308', '#ef4444', '#ec4899', '#f97316'];
export const BarChart: ChartFn = (props) => {
const {
bars = [], orientation = 'vertical', maxValue, showValues = true, title,
} = props;
const max = maxValue || Math.max(...bars.map((b: any) => b.value), 1);
if (orientation === 'horizontal') {
return `
<div class="chart-container">
${title ? `<div class="chart-title">${esc(title)}</div>` : ''}
<div class="bar-chart-h">
${bars.map((b: any, i: number) => {
const pct = Math.min(100, (b.value / max) * 100);
const color = b.color || chartPalette[i % chartPalette.length];
return `
<div class="bar-h-row">
<span class="bar-h-label">${esc(b.label)}</span>
<div class="bar-h-track">
<div class="bar-h-fill" style="width:${pct}%;background:${color}"></div>
</div>
${showValues ? `<span class="bar-h-value">${Number(b.value).toLocaleString()}</span>` : ''}
</div>`;
}).join('')}
</div>
</div>`;
}
// Vertical bars via SVG
const svgW = Math.max(bars.length * 60, 200);
const svgH = 180;
const padTop = 10;
const padBot = 30;
const barW = Math.min(36, (svgW / bars.length) * 0.6);
const gap = svgW / bars.length;
const plotH = svgH - padTop - padBot;
const barsSvg = bars.map((b: any, i: number) => {
const pct = Math.min(1, b.value / max);
const h = pct * plotH;
const x = gap * i + (gap - barW) / 2;
const y = padTop + plotH - h;
const color = b.color || chartPalette[i % chartPalette.length];
return `
<rect x="${x}" y="${y}" width="${barW}" height="${h}" rx="4" fill="${color}" class="bar-v-rect"/>
${showValues ? `<text x="${x + barW / 2}" y="${y - 4}" text-anchor="middle" class="bar-v-val">${Number(b.value).toLocaleString()}</text>` : ''}
<text x="${x + barW / 2}" y="${svgH - 6}" text-anchor="middle" class="bar-v-label">${esc(b.label)}</text>`;
}).join('');
return `
<div class="chart-container">
${title ? `<div class="chart-title">${esc(title)}</div>` : ''}
<div class="chart-scroll">
<svg viewBox="0 0 ${svgW} ${svgH}" class="bar-chart-svg" preserveAspectRatio="xMinYMid meet">
<line x1="0" y1="${padTop + plotH}" x2="${svgW}" y2="${padTop + plotH}" stroke="#e5e7eb" stroke-width="1"/>
${barsSvg}
</svg>
</div>
</div>`;
};
export const LineChart: ChartFn = (props) => {
const {
points = [], color = '#4f46e5', showPoints = true, showArea = false, title, yAxisLabel,
} = props;
if (points.length === 0) return '<div class="chart-container chart-empty">No data</div>';
const vals = points.map((p: any) => p.value);
const minV = Math.min(...vals);
const maxV = Math.max(...vals);
const range = maxV - minV || 1;
const svgW = Math.max(points.length * 60, 200);
const svgH = 180;
const padL = 40;
const padR = 10;
const padT = 16;
const padB = 30;
const plotW = svgW - padL - padR;
const plotH = svgH - padT - padB;
const pts = points.map((p: any, i: number) => {
const x = padL + (plotW / Math.max(points.length - 1, 1)) * i;
const y = padT + plotH - ((p.value - minV) / range) * plotH;
return { x, y, label: p.label, value: p.value };
});
const linePath = pts.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' ');
const areaPath = showArea
? `${linePath} L${pts[pts.length - 1].x},${padT + plotH} L${pts[0].x},${padT + plotH} Z`
: '';
// Y-axis ticks (5 ticks)
const ticks = Array.from({ length: 5 }, (_, i) => {
const val = minV + (range * i) / 4;
const y = padT + plotH - (plotH * i) / 4;
return { val, y };
});
return `
<div class="chart-container">
${title ? `<div class="chart-title">${esc(title)}</div>` : ''}
<div class="chart-scroll">
<svg viewBox="0 0 ${svgW} ${svgH}" class="line-chart-svg" preserveAspectRatio="xMinYMid meet">
${ticks.map(t => `
<line x1="${padL}" y1="${t.y}" x2="${svgW - padR}" y2="${t.y}" stroke="#f3f4f6" stroke-width="1"/>
<text x="${padL - 6}" y="${t.y + 3}" text-anchor="end" class="chart-axis-text">${Math.round(t.val).toLocaleString()}</text>
`).join('')}
${yAxisLabel ? `<text x="10" y="${padT + plotH / 2}" transform="rotate(-90,10,${padT + plotH / 2})" class="chart-axis-label">${esc(yAxisLabel)}</text>` : ''}
${showArea ? `<path d="${areaPath}" fill="${color}" opacity="0.1"/>` : ''}
<path d="${linePath}" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
${showPoints ? pts.map(p => `<circle cx="${p.x}" cy="${p.y}" r="4" fill="white" stroke="${color}" stroke-width="2.5"/>`).join('') : ''}
${pts.map((p, i) => `<text x="${p.x}" y="${svgH - 6}" text-anchor="middle" class="chart-axis-text">${esc(points[i].label)}</text>`).join('')}
</svg>
</div>
</div>`;
};
export const PieChart: ChartFn = (props) => {
const {
segments = [], donut = false, title, showLegend = true,
} = props;
const total = segments.reduce((s: number, seg: any) => s + seg.value, 0) || 1;
const r = 70;
const cx = 90;
const cy = 90;
const svgSize = 180;
let cumAngle = -Math.PI / 2; // start at 12 o'clock
const arcs = segments.map((seg: any, i: number) => {
const frac = seg.value / total;
const angle = frac * Math.PI * 2;
const startAngle = cumAngle;
const endAngle = cumAngle + angle;
cumAngle = endAngle;
const x1 = cx + r * Math.cos(startAngle);
const y1 = cy + r * Math.sin(startAngle);
const x2 = cx + r * Math.cos(endAngle);
const y2 = cy + r * Math.sin(endAngle);
const largeArc = angle > Math.PI ? 1 : 0;
const color = seg.color || chartPalette[i % chartPalette.length];
// Single full segment
if (frac >= 0.9999) {
return `<circle cx="${cx}" cy="${cy}" r="${r}" fill="${color}"/>`;
}
return `<path d="M${cx},${cy} L${x1},${y1} A${r},${r} 0 ${largeArc},1 ${x2},${y2} Z" fill="${color}"/>`;
});
const donutHole = donut
? `<circle cx="${cx}" cy="${cy}" r="${r * 0.55}" fill="white"/>`
: '';
const legendHtml = showLegend ? `
<div class="pie-legend">
${segments.map((seg: any, i: number) => {
const color = seg.color || chartPalette[i % chartPalette.length];
const pct = ((seg.value / total) * 100).toFixed(1);
return `<div class="pie-legend-item"><span class="pie-legend-dot" style="background:${color}"></span><span class="pie-legend-label">${esc(seg.label)}</span><span class="pie-legend-value">${pct}%</span></div>`;
}).join('')}
</div>` : '';
return `
<div class="chart-container">
${title ? `<div class="chart-title">${esc(title)}</div>` : ''}
<div class="pie-chart-layout">
<svg viewBox="0 0 ${svgSize} ${svgSize}" class="pie-chart-svg">
${arcs.join('')}
${donutHole}
${donut ? `<text x="${cx}" y="${cy + 5}" text-anchor="middle" class="pie-center-text">${total.toLocaleString()}</text>` : ''}
</svg>
${legendHtml}
</div>
</div>`;
};
export const FunnelChart: ChartFn = (props) => {
const {
stages = [], showDropoff = true, title,
} = props;
if (stages.length === 0) return '<div class="chart-container chart-empty">No data</div>';
const maxVal = stages[0]?.value || 1;
return `
<div class="chart-container">
${title ? `<div class="chart-title">${esc(title)}</div>` : ''}
<div class="funnel-chart">
${stages.map((s: any, i: number) => {
const pct = Math.max(20, (s.value / maxVal) * 100);
const color = s.color || chartPalette[i % chartPalette.length];
const dropoff = i > 0
? (((stages[i - 1].value - s.value) / stages[i - 1].value) * 100).toFixed(1)
: null;
return `
<div class="funnel-stage">
<div class="funnel-label-col">
<span class="funnel-label">${esc(s.label)}</span>
<span class="funnel-value">${Number(s.value).toLocaleString()}</span>
</div>
<div class="funnel-bar-col">
<div class="funnel-bar" style="width:${pct}%;background:${color}"></div>
</div>
${showDropoff && dropoff !== null ? `<span class="funnel-dropoff">-${dropoff}%</span>` : '<span class="funnel-dropoff"></span>'}
</div>`;
}).join('')}
</div>
</div>`;
};
export const SparklineChart: ChartFn = (props) => {
const {
values = [], color = '#4f46e5', height = 24, width = 80,
} = props;
if (values.length < 2) {
return `<span class="sparkline-empty" style="width:${width}px;height:${height}px">\u2014</span>`;
}
const minV = Math.min(...values);
const maxV = Math.max(...values);
const range = maxV - minV || 1;
const pad = 2;
const pts = values.map((v: number, i: number) => {
const x = pad + ((width - pad * 2) / (values.length - 1)) * i;
const y = pad + (height - pad * 2) - ((v - minV) / range) * (height - pad * 2);
return `${x},${y}`;
});
return `<svg class="sparkline-svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><polyline points="${pts.join(' ')}" fill="none" stroke="${color}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,479 @@
/**
* GHL Dynamic View - MCP App Entry Point
* Receives UI trees from the generate_ghl_view tool via MCP ext-apps protocol
* and renders them using the component library.
*
* Implements the MCP ext-apps handshake:
* 1. View sends ui/initialize Host responds
* 2. View sends ui/notifications/initialized Host acks
* 3. Host sends ui/notifications/tool-result View renders
*/
import { COMPONENTS } from './components';
import { STYLES } from './styles';
// ─── Inject Styles ──────────────────────────────────────────
const styleEl = document.createElement('style');
styleEl.textContent = STYLES;
document.head.appendChild(styleEl);
// ─── Tree Renderer ──────────────────────────────────────────
interface UIElement {
key: string;
type: string;
props: Record<string, any>;
children?: string[];
}
interface UITree {
root: string;
elements: Record<string, UIElement>;
}
function renderElement(el: UIElement, elements: Record<string, UIElement>): string {
const component = COMPONENTS[el.type];
if (!component) {
return `<div class="error-state"><p>Unknown component: ${el.type}</p></div>`;
}
const childrenHtml = (el.children || [])
.map(key => {
const childEl = elements[key];
if (!childEl) return `<div class="text-muted">Missing element: ${key}</div>`;
return renderElement(childEl, elements);
})
.join('');
return component(el.props || {}, childrenHtml);
}
function renderTree(tree: UITree): void {
const appEl = document.getElementById('app');
if (!appEl) return;
try {
const rootEl = tree.elements[tree.root];
if (!rootEl) {
appEl.innerHTML = `<div class="error-state"><h3>Render Error</h3><p>Root element "${tree.root}" not found in tree</p></div>`;
return;
}
appEl.innerHTML = renderElement(rootEl, tree.elements);
// After rendering, report actual content size to host
requestAnimationFrame(() => {
reportSize();
});
} catch (err: any) {
appEl.innerHTML = `<div class="error-state"><h3>Render Error</h3><p>${err.message || 'Unknown error'}</p></div>`;
}
}
/** Measure actual content and notify host of preferred size */
function reportSize(): void {
const appEl = document.getElementById('app');
if (!appEl) return;
const height = Math.min(appEl.scrollHeight + 16, 600); // Cap at 600px
const width = appEl.scrollWidth + 8;
try {
window.parent.postMessage({
jsonrpc: '2.0',
method: 'ui/notifications/size-changed',
params: { width, height },
}, '*');
} catch {
// Not in iframe
}
// Also set body height so the iframe can auto-size if the host supports it
document.body.style.height = height + 'px';
document.body.style.overflow = 'auto';
}
// ─── Loading State ──────────────────────────────────────────
function showLoading(): void {
const appEl = document.getElementById('app');
if (appEl) {
appEl.innerHTML = `
<div class="loading-state">
<div class="loading-spinner"></div>
<p>Generating view...</p>
</div>`;
}
}
function showError(message: string): void {
const appEl = document.getElementById('app');
if (appEl) {
appEl.innerHTML = `<div class="error-state"><h3>Error</h3><p>${message}</p></div>`;
}
}
// ─── Helpers ────────────────────────────────────────────────
/** Try to extract a uiTree from any data shape and render it */
function tryRenderFromData(data: any): boolean {
if (!data || typeof data !== 'object') return false;
// structuredContent.uiTree
if (data.structuredContent?.uiTree) {
renderTree(data.structuredContent.uiTree);
return true;
}
// Direct uiTree
if (data.uiTree) {
renderTree(data.uiTree);
return true;
}
// Direct tree shape
if (data.root && data.elements) {
renderTree(data as UITree);
return true;
}
// Nested in content array
if (Array.isArray(data.content)) {
for (const item of data.content) {
if (item.structuredContent?.uiTree) {
renderTree(item.structuredContent.uiTree);
return true;
}
}
}
return false;
}
// ─── MCP ext-apps Protocol (JSON-RPC over postMessage) ──────
let rpcId = 1;
let initialized = false;
function sendToHost(message: Record<string, any>): void {
try {
window.parent.postMessage(message, '*');
} catch {
// Not in an iframe or parent unavailable
}
}
function sendJsonRpcRequest(method: string, params: Record<string, any> = {}): number {
const id = rpcId++;
sendToHost({ jsonrpc: '2.0', id, method, params });
return id;
}
function sendJsonRpcNotification(method: string, params: Record<string, any> = {}): void {
sendToHost({ jsonrpc: '2.0', method, params });
}
// ─── Message Handler ────────────────────────────────────────
showLoading();
window.addEventListener('message', (event: MessageEvent) => {
try {
const data = event.data;
if (!data || typeof data !== 'object') return;
// ── JSON-RPC protocol (ext-apps standard) ──
if (data.jsonrpc === '2.0') {
// JSON-RPC response (to our requests)
if ('id' in data && (data.result !== undefined || data.error !== undefined)) {
const id = data.id as number;
// Check if it's a response to a tool call
const pending = pendingToolCalls.get(id);
if (pending) {
pendingToolCalls.delete(id);
if (data.error) {
pending.reject(new Error(data.error.message || 'Tool call failed'));
} else {
pending.resolve(data.result);
}
return;
}
// Otherwise it's likely the initialize response
if (!initialized) {
initialized = true;
sendJsonRpcNotification('ui/notifications/initialized', {});
}
return;
}
// Host sends tool result
if (data.method === 'ui/notifications/tool-result') {
if (tryRenderFromData(data.params)) return;
// Try params directly as structured content
if (data.params && tryRenderFromData(data.params)) return;
return;
}
// Host sends tool input (args before result — could render partial)
if (data.method === 'ui/notifications/tool-input') {
// Tool is still executing; keep showing loading
return;
}
// Host sends partial input (streaming)
if (data.method === 'ui/notifications/tool-input-partial') {
return;
}
// Host sends tool cancelled
if (data.method === 'ui/notifications/tool-cancelled') {
showError('View generation was cancelled.');
return;
}
// Host context changed (theme, etc.)
if (data.method === 'ui/notifications/host-context-changed') {
// Could apply theme here in the future
return;
}
// Host sends teardown request
if (data.method === 'ui/teardown' && data.id) {
sendToHost({ jsonrpc: '2.0', id: data.id, result: {} });
return;
}
// Ping
if (data.method === 'ping' && data.id) {
sendToHost({ jsonrpc: '2.0', id: data.id, result: {} });
return;
}
return;
}
// ── Legacy / non-JSON-RPC fallbacks ──
// Some hosts may send custom formats
if (data.type === 'tool-result' || data.type === 'mcp-tool-result') {
if (tryRenderFromData(data)) return;
const content = data.structuredContent || data.content || data.data || data;
if (tryRenderFromData(content)) return;
}
if (data.type === 'mcp-app-init' && data.data) {
if (tryRenderFromData(data.data)) return;
}
// Direct data passthrough
if (tryRenderFromData(data)) return;
} catch (err: any) {
showError(err.message || 'Failed to process message');
}
});
// ─── Check pre-injected data ────────────────────────────────
const preInjected = (window as any).__MCP_APP_DATA__;
if (preInjected) {
tryRenderFromData(preInjected);
}
// ─── MCP Tool Calling ───────────────────────────────────────
const pendingToolCalls = new Map<number, { resolve: (v: any) => void; reject: (e: Error) => void }>();
function callTool(toolName: string, args: Record<string, any>): Promise<any> {
return new Promise((resolve, reject) => {
const id = rpcId++;
pendingToolCalls.set(id, { resolve, reject });
sendToHost({ jsonrpc: '2.0', id, method: 'tools/call', params: { name: toolName, arguments: args } });
// Timeout after 30s
setTimeout(() => {
if (pendingToolCalls.has(id)) {
pendingToolCalls.delete(id);
reject(new Error('Tool call timed out'));
}
}, 30000);
});
}
// ─── Toast Notifications ────────────────────────────────────
function showToast(message: string, type: 'success' | 'error' | 'info' = 'info'): void {
const existing = document.getElementById('mcp-toast');
if (existing) existing.remove();
const toast = document.createElement('div');
toast.id = 'mcp-toast';
toast.className = `mcp-toast mcp-toast-${type}`;
toast.textContent = message;
document.body.appendChild(toast);
requestAnimationFrame(() => toast.classList.add('mcp-toast-show'));
setTimeout(() => { toast.classList.remove('mcp-toast-show'); setTimeout(() => toast.remove(), 300); }, 2500);
}
// ─── Edit Modal ─────────────────────────────────────────────
function showEditModal(title: string, fields: Array<{key: string, label: string, value: string, type?: string}>, onSave: (values: Record<string, string>) => void): void {
const existing = document.getElementById('mcp-modal');
if (existing) existing.remove();
const modal = document.createElement('div');
modal.id = 'mcp-modal';
modal.className = 'mcp-modal-overlay';
modal.innerHTML = `
<div class="mcp-modal">
<div class="mcp-modal-header">
<span class="mcp-modal-title">${title}</span>
<button class="mcp-modal-close" data-modal-close>&times;</button>
</div>
<div class="mcp-modal-body">
${fields.map(f => `
<div class="mcp-field">
<label class="mcp-field-label">${f.label}</label>
<input class="mcp-field-input" data-field="${f.key}" type="${f.type || 'text'}" value="${f.value}" />
</div>`).join('')}
</div>
<div class="mcp-modal-footer">
<button class="btn btn-secondary btn-sm" data-modal-close>Cancel</button>
<button class="btn btn-primary btn-sm" data-modal-save>Save</button>
</div>
</div>`;
document.body.appendChild(modal);
modal.querySelectorAll('[data-modal-close]').forEach(el => el.addEventListener('click', () => modal.remove()));
modal.querySelector('[data-modal-save]')?.addEventListener('click', () => {
const values: Record<string, string> = {};
modal.querySelectorAll<HTMLInputElement>('[data-field]').forEach(inp => {
values[inp.dataset.field!] = inp.value;
});
onSave(values);
modal.remove();
});
modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); });
}
// ─── Drag & Drop (Kanban) ───────────────────────────────────
let draggedCardId: string | null = null;
let draggedFromStage: string | null = null;
document.addEventListener('dragstart', (e) => {
const card = (e.target as HTMLElement).closest?.('.kanban-card[draggable]') as HTMLElement | null;
if (!card) return;
draggedCardId = card.dataset.cardId || null;
draggedFromStage = card.dataset.stageId || null;
card.classList.add('kanban-card-dragging');
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', draggedCardId || '');
}
});
document.addEventListener('dragend', (e) => {
const card = (e.target as HTMLElement).closest?.('.kanban-card') as HTMLElement | null;
if (card) card.classList.remove('kanban-card-dragging');
document.querySelectorAll('.kanban-col-body').forEach(el => el.classList.remove('kanban-drop-target'));
draggedCardId = null;
draggedFromStage = null;
});
document.addEventListener('dragover', (e) => {
const colBody = (e.target as HTMLElement).closest?.('.kanban-col-body') as HTMLElement | null;
if (colBody) {
e.preventDefault();
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
colBody.classList.add('kanban-drop-target');
}
});
document.addEventListener('dragleave', (e) => {
const colBody = (e.target as HTMLElement).closest?.('.kanban-col-body') as HTMLElement | null;
if (colBody) colBody.classList.remove('kanban-drop-target');
});
document.addEventListener('drop', async (e) => {
e.preventDefault();
const colBody = (e.target as HTMLElement).closest?.('.kanban-col-body[data-stage-id]') as HTMLElement | null;
if (!colBody || !draggedCardId) return;
const newStageId = colBody.dataset.stageId;
if (!newStageId || newStageId === draggedFromStage) return;
// Move the card in the DOM immediately for snappy feel
const draggedEl = document.querySelector(`.kanban-card[data-card-id="${draggedCardId}"]`) as HTMLElement;
if (draggedEl) {
draggedEl.classList.remove('kanban-card-dragging');
draggedEl.dataset.stageId = newStageId;
colBody.appendChild(draggedEl);
showToast('Moving deal...', 'info');
}
// Call the MCP server to persist the change
try {
await callTool('update_opportunity', {
opportunityId: draggedCardId,
pipelineStageId: newStageId,
});
showToast('Deal moved!', 'success');
} catch (err: any) {
showToast(`Failed: ${err.message}`, 'error');
// TODO: revert DOM on failure
}
colBody.classList.remove('kanban-drop-target');
});
// ─── Double-click to Edit (Kanban cards) ────────────────────
document.addEventListener('dblclick', (e) => {
const card = (e.target as HTMLElement).closest?.('.kanban-card[data-card-id]') as HTMLElement | null;
if (card && card.dataset.cardId) {
const titleEl = card.querySelector('.kanban-card-title');
const valueEl = card.querySelector('.kanban-card-value');
showEditModal('Edit Opportunity', [
{ key: 'name', label: 'Name', value: titleEl?.textContent || '' },
{ key: 'monetaryValue', label: 'Value ($)', value: (valueEl?.textContent || '').replace(/[^0-9.]/g, ''), type: 'number' },
{ key: 'status', label: 'Status', value: 'open' },
], async (values) => {
showToast('Updating...', 'info');
try {
const args: Record<string, any> = { opportunityId: card.dataset.cardId! };
if (values.name) args.name = values.name;
if (values.monetaryValue) args.monetaryValue = parseFloat(values.monetaryValue);
if (values.status) args.status = values.status;
await callTool('update_opportunity', args);
// Update DOM
if (titleEl && values.name) titleEl.textContent = values.name;
if (valueEl && values.monetaryValue) valueEl.textContent = `$${parseFloat(values.monetaryValue).toLocaleString()}`;
showToast('Updated!', 'success');
} catch (err: any) {
showToast(`Failed: ${err.message}`, 'error');
}
});
return;
}
// Double-click on table row
const row = (e.target as HTMLElement).closest?.('tr[data-row-id]') as HTMLElement | null;
if (row && row.dataset.rowId) {
const cells = row.querySelectorAll('td');
const headers = row.closest('table')?.querySelectorAll('th');
const fields: Array<{key: string, label: string, value: string}> = [];
cells.forEach((cell, i) => {
if (cell.classList.contains('checkbox-col')) return;
const label = headers?.[i]?.textContent?.trim() || `Field ${i}`;
fields.push({ key: `field_${i}`, label, value: cell.textContent?.trim() || '' });
});
showEditModal('Row Details', fields, () => {
showToast('Edit via table is read-only for now', 'info');
});
}
});
// ─── Initiate ext-apps handshake ────────────────────────────
// Send ui/initialize to the host to start the protocol
// The host will respond, then we send initialized, then it sends tool data
try {
if (window.parent && window.parent !== window) {
sendJsonRpcRequest('ui/initialize', {
protocolVersion: '2026-01-26',
appInfo: {
name: 'GHL Dynamic View',
version: '1.0.0',
},
appCapabilities: {
tools: { listChanged: false },
},
});
}
} catch {
// Not in an iframe context — rely on __MCP_APP_DATA__
}

View File

@ -0,0 +1,973 @@
/**
* Inlined CSS styles for the GHL Dynamic View app.
* Matches the polish of existing MCP App UIs.
*/
export const STYLES = `
/* ─── Reset & Base ────────────────────────────────────── */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: white;
color: #1f2937;
line-height: 1.4;
font-size: 12px;
-webkit-font-smoothing: antialiased;
overflow: hidden;
}
#app { padding: 4px; }
/* ─── Compact Chat Overrides ──────────────────────────── */
/* Scale everything down for inline chat MCP App display */
a { color: #4f46e5; text-decoration: none; }
a:hover { text-decoration: underline; }
.font-medium { font-weight: 500; }
.font-mono { font-family: 'SF Mono', 'Fira Code', monospace; }
.text-sm { font-size: 13px; }
.text-muted { color: #9ca3af; }
.text-secondary { color: #6b7280; }
.text-left { text-align: left; }
.text-center { text-align: center; }
.text-right { text-align: right; }
/* ─── Loading & Error States ──────────────────────────── */
.loading-state {
display: flex; flex-direction: column; align-items: center; justify-content: center;
min-height: 120px; color: #6b7280;
}
.loading-spinner {
width: 24px; height: 24px; border: 2px solid #e5e7eb; border-top-color: #4f46e5;
border-radius: 50%; animation: spin 0.8s linear infinite; margin-bottom: 8px;
}
@keyframes spin { to { transform: rotate(360deg); } }
.loading-state p { font-size: 12px; }
.error-state {
padding: 12px; margin: 8px; background: #fef2f2; border: 1px solid #fecaca;
border-radius: 8px; color: #dc2626;
}
.error-state h3 { font-size: 13px; font-weight: 600; margin-bottom: 4px; }
.error-state p { font-size: 12px; color: #991b1b; }
/* ─── Status Badges ───────────────────────────────────── */
.status-badge, .status-badge-sm {
display: inline-flex; align-items: center; border-radius: 9999px;
font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em;
}
.status-badge { padding: 4px 12px; font-size: 11px; }
.status-badge-sm { padding: 2px 6px; font-size: 9px; }
.status-active { background: #dbeafe; color: #1e40af; }
.status-complete { background: #dcfce7; color: #166534; }
.status-paused { background: #fef3c7; color: #92400e; }
.status-draft { background: #e5e7eb; color: #4b5563; }
.status-error { background: #fee2e2; color: #991b1b; }
.status-sent { background: #dbeafe; color: #1e40af; }
.status-paid { background: #dcfce7; color: #166534; }
.status-pending { background: #fef3c7; color: #92400e; }
.status-open { background: #dbeafe; color: #1d4ed8; }
.status-won { background: #dcfce7; color: #15803d; }
.status-lost { background: #fee2e2; color: #b91c1c; }
/* ─── Page Header ─────────────────────────────────────── */
.page-header {
background: white; border-radius: 8px; padding: 10px 12px; margin-bottom: 8px;
box-shadow: 0 1px 2px rgba(0,0,0,0.06);
}
.page-header-gradient {
background: linear-gradient(135deg, #4f46e5, #7c3aed);
color: white; padding: 8px 12px; position: sticky; top: 0; z-index: 50;
border-radius: 8px; margin-bottom: 8px;
box-shadow: 0 1px 4px rgba(79,70,229,0.3);
}
.page-header-top { display: flex; align-items: center; justify-content: space-between; }
.page-header-title { font-size: 14px; font-weight: 700; color: #111827; }
.page-header-title-light { font-size: 13px; font-weight: 600; }
.page-header-subtitle { font-size: 11px; color: #6b7280; margin-top: 2px; }
.page-header-subtitle-light { font-size: 10px; opacity: 0.8; margin-top: 1px; }
.badge-light { padding: 2px 8px; border-radius: 9999px; font-size: 10px; font-weight: 600; background: rgba(255,255,255,0.2); }
.page-header-stats { display: flex; gap: 12px; margin-top: 6px; flex-wrap: wrap; }
.page-header-stats-light { display: flex; gap: 10px; margin-top: 4px; }
.stat-item { font-size: 11px; color: #6b7280; }
.stat-value { font-weight: 500; color: #374151; }
.stat-item-light { font-size: 10px; display: flex; align-items: center; gap: 4px; }
.stat-value-light { font-weight: 600; }
.stat-label-light { opacity: 0.7; }
/* ─── Card ────────────────────────────────────────────── */
.card {
background: white; border-radius: 8px; box-shadow: 0 1px 2px rgba(0,0,0,0.06);
overflow: hidden; margin-bottom: 8px;
}
.card.no-border { box-shadow: none; }
.card-header { padding: 8px 12px; border-bottom: 1px solid #e5e7eb; }
.card-title { font-size: 13px; font-weight: 600; color: #111827; }
.card-subtitle { font-size: 11px; color: #6b7280; margin-top: 1px; }
.card-body.p-0 { padding: 0; }
.card-body.p-sm { padding: 8px; }
.card-body.p-md { padding: 10px; }
.card-body.p-lg { padding: 12px; }
/* ─── Stats Grid ──────────────────────────────────────── */
.stats-grid { display: grid; gap: 8px; margin-bottom: 8px; }
.stats-grid-2 { grid-template-columns: repeat(2, 1fr); }
.stats-grid-3 { grid-template-columns: repeat(2, 1fr); }
.stats-grid-4 { grid-template-columns: repeat(2, 1fr); }
.stats-grid-6 { grid-template-columns: repeat(2, 1fr); }
@media (min-width: 640px) {
.stats-grid-3 { grid-template-columns: repeat(3, 1fr); }
.stats-grid-4 { grid-template-columns: repeat(4, 1fr); }
.stats-grid-6 { grid-template-columns: repeat(3, 1fr); }
}
@media (min-width: 1024px) {
.stats-grid-6 { grid-template-columns: repeat(6, 1fr); }
}
/* ─── Split Layout ────────────────────────────────────── */
.split-layout { display: grid; }
.split-50-50 { grid-template-columns: 1fr; }
.split-33-67 { grid-template-columns: 1fr; }
.split-67-33 { grid-template-columns: 1fr; }
.gap-sm { gap: 6px; }
.gap-md { gap: 10px; }
.gap-lg { gap: 14px; }
@media (min-width: 640px) {
.split-50-50 { grid-template-columns: 1fr 1fr; }
.split-33-67 { grid-template-columns: 1fr 2fr; }
.split-67-33 { grid-template-columns: 2fr 1fr; }
}
/* ─── Section ─────────────────────────────────────────── */
.section { margin-bottom: 8px; }
.section-header { margin-bottom: 6px; }
.section-title { font-size: 13px; font-weight: 600; color: #111827; }
.section-desc { font-size: 11px; color: #6b7280; margin-top: 1px; }
/* ─── Metric Card ─────────────────────────────────────── */
.metric-card {
background: rgba(255,255,255,0.95); border-radius: 8px; padding: 10px;
text-align: center; box-shadow: 0 1px 2px rgba(0,0,0,0.06);
}
.metric-value { font-size: 18px; font-weight: 700; color: #111827; }
.metric-green { color: #059669; }
.metric-blue { color: #2563eb; }
.metric-purple { color: #7c3aed; }
.metric-yellow { color: #d97706; }
.metric-red { color: #dc2626; }
.metric-label { font-size: 10px; color: #6b7280; margin-top: 2px; }
.metric-trend { font-size: 11px; font-weight: 600; margin-top: 4px; }
.trend-up { color: #059669; }
.trend-down { color: #dc2626; }
.trend-flat { color: #6b7280; }
/* ─── Data Table ──────────────────────────────────────── */
.data-table-wrap {
background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);
overflow: hidden;
}
.table-container { overflow-x: auto; }
.data-table { width: 100%; border-collapse: collapse; font-size: 11px; }
.data-table th {
text-align: left; padding: 6px 8px; background: #f9fafb; color: #6b7280;
font-weight: 500; font-size: 10px; text-transform: uppercase; letter-spacing: 0.3px;
border-bottom: 1px solid #e5e7eb; white-space: nowrap;
}
.data-table th.sortable { cursor: pointer; user-select: none; }
.data-table th.sortable:hover { background: #f3f4f6; }
.data-table td { padding: 5px 8px; border-bottom: 1px solid #f3f4f6; vertical-align: middle; }
.data-table tr:hover { background: #f9fafb; }
.data-table .checkbox-col { width: 40px; }
.data-table input[type="checkbox"] { width: 16px; height: 16px; accent-color: #4f46e5; cursor: pointer; }
.avatar-cell { display: flex; align-items: center; gap: 12px; }
.avatar {
width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center;
justify-content: center; color: white; font-weight: 600; font-size: 12px; flex-shrink: 0;
}
.tags { display: flex; flex-wrap: wrap; gap: 4px; max-width: 200px; }
.tag {
display: inline-block; padding: 2px 8px; background: #eef2ff; color: #4f46e5;
border-radius: 12px; font-size: 11px; font-weight: 500;
}
.tag-more { background: #f3f4f6; color: #6b7280; }
.link { color: #4f46e5; text-decoration: none; }
.link:hover { text-decoration: underline; }
.table-pagination {
padding: 12px 20px; border-top: 1px solid #e5e7eb; display: flex;
justify-content: space-between; align-items: center;
}
.pagination-info { font-size: 13px; color: #6b7280; }
.pagination-buttons { display: flex; gap: 8px; }
.empty-state { padding: 60px 20px; text-align: center; }
.empty-icon { font-size: 48px; margin-bottom: 16px; }
.empty-state p { color: #6b7280; font-size: 15px; }
/* ─── Kanban Board ────────────────────────────────────── */
.kanban-wrap { padding: 8px; overflow-x: auto; overflow-y: auto; max-height: calc(100vh - 80px); }
.kanban-cols { display: flex; gap: 8px; min-width: max-content; }
.kanban-col {
background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);
display: flex; flex-direction: column; width: 210px; min-width: 210px;
max-height: calc(100vh - 100px);
}
.kanban-col-header {
padding: 10px 12px; border-bottom: 1px solid #e5e7eb;
position: sticky; top: 0; background: white; border-radius: 8px 8px 0 0;
}
.kanban-col-title-row { display: flex; align-items: center; justify-content: space-between; }
.kanban-col-title { font-weight: 600; font-size: 12px; color: #1f2937; }
.kanban-col-count {
background: #e5e7eb; color: #4b5563; padding: 1px 6px; border-radius: 9999px;
font-size: 10px; font-weight: 500;
}
.kanban-col-value { font-size: 10px; color: #6b7280; margin-top: 2px; }
.kanban-col-body { padding: 6px; overflow-y: auto; flex: 1; min-height: 60px; max-height: 320px; }
.kanban-empty { text-align: center; padding: 16px; color: #9ca3af; font-size: 10px; }
.kanban-card {
background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 6px;
padding: 8px 10px; margin-bottom: 6px; cursor: pointer;
transition: box-shadow 0.15s, border-color 0.15s;
}
.kanban-card:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.1); border-color: #4f46e5; }
.kanban-card-title { font-weight: 600; font-size: 11px; color: #111827; margin-bottom: 4px; line-height: 1.3; }
.kanban-card-subtitle { display: flex; align-items: center; gap: 4px; font-size: 10px; color: #6b7280; margin-bottom: 4px; }
.kanban-avatar {
width: 16px; height: 16px; background: #e5e7eb; border-radius: 50%;
display: flex; align-items: center; justify-content: center; font-size: 7px;
font-weight: 700; color: #4b5563;
}
.kanban-card-value { font-size: 12px; font-weight: 700; color: #059669; margin-bottom: 4px; }
.kanban-card-footer { display: flex; align-items: center; justify-content: space-between; font-size: 9px; color: #9ca3af; }
/* ─── Timeline ────────────────────────────────────────── */
.timeline { position: relative; padding-left: 24px; }
.timeline-line { position: absolute; left: 8px; top: 8px; bottom: 8px; width: 2px; background: #e5e7eb; }
.timeline-item { position: relative; margin-bottom: 16px; }
.timeline-item:last-child { margin-bottom: 0; }
.timeline-dot {
position: absolute; left: -16px; top: 4px; width: 16px; height: 16px;
border-radius: 50%; border: 2px solid; background: white;
display: flex; align-items: center; justify-content: center; font-size: 8px;
}
.tl-border-default { border-color: #d1d5db; }
.tl-border-success { border-color: #4ade80; }
.tl-border-warning { border-color: #facc15; }
.tl-border-error { border-color: #f87171; }
.timeline-content { margin-left: 8px; }
.timeline-title { font-weight: 500; font-size: 14px; color: #111827; }
.timeline-desc { font-size: 14px; color: #6b7280; margin-top: 2px; }
.timeline-time { font-size: 12px; color: #9ca3af; margin-top: 2px; }
/* ─── Progress Bar ────────────────────────────────────── */
.progress-wrap { margin-bottom: 20px; }
.progress-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.progress-label { font-size: 14px; font-weight: 500; color: #374151; }
.progress-value { font-size: 14px; color: #6b7280; }
.progress-value strong { color: #111827; }
.progress-track { position: relative; height: 24px; background: #e5e7eb; border-radius: 12px; overflow: hidden; }
.progress-bar { height: 100%; border-radius: 12px; transition: width 0.5s; }
.bar-green { background: linear-gradient(to right, #10b981, #059669); }
.bar-blue { background: linear-gradient(to right, #3b82f6, #2563eb); }
.bar-purple { background: linear-gradient(to right, #8b5cf6, #7c3aed); }
.bar-yellow { background: linear-gradient(to right, #f59e0b, #d97706); }
.bar-red { background: linear-gradient(to right, #ef4444, #dc2626); }
.progress-benchmark { position: absolute; top: 0; height: 100%; width: 2px; background: rgba(0,0,0,0.35); }
.progress-benchmark-label { position: absolute; top: -16px; font-size: 10px; color: #6b7280; transform: translateX(-50%); }
/* ─── Detail Header ───────────────────────────────────── */
.detail-header {
background: white; border-radius: 12px; padding: 24px; margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.detail-header-top { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 4px; }
.detail-title { font-size: 24px; font-weight: 700; color: #111827; }
.detail-entity-id { font-size: 14px; color: #9ca3af; margin-top: 2px; }
.detail-subtitle { font-size: 14px; color: #6b7280; margin-top: 4px; }
/* ─── Key-Value List ──────────────────────────────────── */
.kv-list { background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); overflow: hidden; }
.kv-row { display: flex; justify-content: space-between; align-items: center; padding: 12px 24px; border-bottom: 1px solid #f3f4f6; }
.kv-row:last-child { border-bottom: none; }
.kv-compact { padding: 8px 24px; }
.kv-total { background: #1e3a5f; color: white; padding: 16px 24px; border-bottom: none; }
.kv-success { background: #f0fdf4; }
.kv-highlight { background: #f9fafb; font-weight: 600; }
.kv-muted .kv-label, .kv-muted .kv-value { color: #9ca3af; }
.kv-label { font-size: 14px; color: #6b7280; }
.kv-total .kv-label { font-size: 16px; font-weight: 600; color: white; }
.kv-value { font-family: 'SF Mono', 'Fira Code', monospace; font-weight: 500; color: #111827; }
.kv-value-bold { font-family: 'SF Mono', 'Fira Code', monospace; font-weight: 600; color: #111827; }
.kv-value-total { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 20px; font-weight: 700; color: white; }
.kv-value-success { font-family: 'SF Mono', 'Fira Code', monospace; font-weight: 500; color: #059669; }
.kv-value-danger { font-family: 'SF Mono', 'Fira Code', monospace; font-weight: 500; color: #dc2626; }
.kv-success .kv-label { color: #15803d; }
/* ─── Line Items Table ────────────────────────────────── */
.line-items-wrap {
background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);
overflow: hidden; margin-bottom: 20px;
}
.line-items-table { width: 100%; border-collapse: collapse; }
.line-items-table th {
background: #f9fafb; padding: 12px 16px; font-size: 12px; font-weight: 600;
color: #6b7280; text-transform: uppercase; letter-spacing: 0.05em;
border-bottom: 1px solid #e5e7eb;
}
.line-items-table td { padding: 16px; border-bottom: 1px solid #f3f4f6; }
.line-items-table tr:last-child td { border-bottom: none; }
/* ─── Info Block ──────────────────────────────────────── */
.info-block { margin-bottom: 16px; }
.info-block-label { font-size: 11px; text-transform: uppercase; color: #9ca3af; letter-spacing: 0.05em; font-weight: 500; margin-bottom: 8px; }
.info-block-name { font-size: 16px; font-weight: 600; color: #111827; }
.info-block-lines { font-size: 14px; color: #6b7280; line-height: 1.6; margin-top: 4px; }
/* ─── Search Bar ──────────────────────────────────────── */
.search-bar { padding: 12px 20px; background: #f9fafb; border-bottom: 1px solid #e5e7eb; }
.search-input {
width: 100%; padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 6px;
font-size: 14px; outline: none; transition: border-color 0.2s, box-shadow 0.2s;
}
.search-input:focus { border-color: #4f46e5; box-shadow: 0 0 0 3px rgba(79,70,229,0.1); }
/* ─── Filter Chips ────────────────────────────────────── */
.filter-chips { display: flex; flex-wrap: wrap; gap: 8px; padding: 8px 20px; border-bottom: 1px solid #e5e7eb; }
.chip {
padding: 4px 12px; border-radius: 9999px; font-size: 12px; font-weight: 500;
background: #f3f4f6; color: #4b5563; border: none; cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.chip:hover { background: #e5e7eb; }
.chip-active { background: #4f46e5; color: white; }
.chip-active:hover { background: #4338ca; }
/* ─── Tab Group ───────────────────────────────────────── */
.tab-group { display: flex; border-bottom: 1px solid #e5e7eb; margin-bottom: 16px; }
.tab {
padding: 8px 16px; font-size: 14px; font-weight: 500; border: none; background: none;
border-bottom: 2px solid transparent; color: #6b7280; cursor: pointer;
transition: color 0.15s, border-color 0.15s;
}
.tab:hover { color: #374151; border-bottom-color: #d1d5db; }
.tab-active { color: #4f46e5; border-bottom-color: #4f46e5; }
.tab-count {
margin-left: 6px; padding: 1px 6px; background: #f3f4f6; color: #6b7280;
border-radius: 9999px; font-size: 12px;
}
/* ─── Buttons ─────────────────────────────────────────── */
.btn {
display: inline-flex; align-items: center; border-radius: 8px; font-weight: 500;
cursor: pointer; transition: background 0.15s, box-shadow 0.15s; border: none;
}
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-primary { background: #4f46e5; color: white; box-shadow: 0 1px 2px rgba(0,0,0,0.1); }
.btn-primary:hover:not(:disabled) { background: #4338ca; }
.btn-secondary { background: white; color: #374151; border: 1px solid #d1d5db; box-shadow: 0 1px 2px rgba(0,0,0,0.05); }
.btn-secondary:hover:not(:disabled) { background: #f9fafb; }
.btn-danger { background: #dc2626; color: white; box-shadow: 0 1px 2px rgba(0,0,0,0.1); }
.btn-danger:hover:not(:disabled) { background: #b91c1c; }
.btn-ghost { background: none; color: #6b7280; }
.btn-ghost:hover:not(:disabled) { background: #f3f4f6; color: #111827; }
.btn-sm { padding: 6px 12px; font-size: 12px; }
.btn-md { padding: 8px 16px; font-size: 14px; }
.btn-lg { padding: 10px 24px; font-size: 16px; }
/* ─── Action Bar ──────────────────────────────────────── */
.action-bar { display: flex; gap: 12px; margin-top: 20px; }
.align-left { justify-content: flex-start; }
.align-center { justify-content: center; }
.align-right { justify-content: flex-end; }
/* ─── Currency Display ────────────────────────────────── */
.currency-display { font-family: 'SF Mono', 'Fira Code', monospace; font-weight: 600; }
.currency-sm { font-size: 14px; }
.currency-md { font-size: 20px; }
.currency-lg { font-size: 32px; }
.currency-positive { color: #059669; }
.currency-negative { color: #dc2626; }
/* ─── Tag List ────────────────────────────────────────── */
.tag-list { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.tag-list-sm .tag-pill { padding: 2px 8px; font-size: 11px; }
.tag-list-md .tag-pill { padding: 4px 10px; font-size: 12px; }
.tag-pill {
display: inline-flex; align-items: center; border-radius: 9999px;
font-weight: 500; transition: opacity 0.15s;
}
.tag-pill-blue { background: #dbeafe; color: #1e40af; }
.tag-pill-green { background: #dcfce7; color: #166534; }
.tag-pill-red { background: #fee2e2; color: #991b1b; }
.tag-pill-yellow { background: #fef3c7; color: #92400e; }
.tag-pill-purple { background: #ede9fe; color: #5b21b6; }
.tag-pill-gray { background: #e5e7eb; color: #4b5563; }
.tag-pill-indigo { background: #e0e7ff; color: #3730a3; }
.tag-pill-pink { background: #fce7f3; color: #9d174d; }
.tag-pill-outlined { background: transparent; border: 1.5px solid currentColor; }
.tag-pill-more { background: #f3f4f6; color: #6b7280; }
/* ─── Card Grid ───────────────────────────────────────── */
.card-grid { display: grid; gap: 16px; }
.card-grid-1 { grid-template-columns: 1fr; }
.card-grid-2 { grid-template-columns: repeat(2, 1fr); }
.card-grid-3 { grid-template-columns: repeat(2, 1fr); }
.card-grid-4 { grid-template-columns: repeat(2, 1fr); }
@media (min-width: 640px) {
.card-grid-3 { grid-template-columns: repeat(3, 1fr); }
.card-grid-4 { grid-template-columns: repeat(4, 1fr); }
}
.card-grid-item {
background: white; border-radius: 10px; overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.08); transition: box-shadow 0.2s, transform 0.2s;
}
.card-grid-item:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.12); transform: translateY(-2px); }
.card-grid-image {
height: 140px; background-size: cover; background-position: center; background-color: #f3f4f6;
}
.card-grid-image-placeholder {
display: flex; align-items: center; justify-content: center;
background: linear-gradient(135deg, #eef2ff, #e0e7ff); font-size: 32px;
}
.card-grid-body { padding: 12px 14px; }
.card-grid-title { font-weight: 600; font-size: 14px; color: #111827; margin-bottom: 2px; }
.card-grid-subtitle { font-size: 12px; color: #6b7280; margin-bottom: 4px; }
.card-grid-desc { font-size: 13px; color: #6b7280; line-height: 1.4; margin-bottom: 8px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.card-grid-footer { display: flex; align-items: center; justify-content: space-between; }
.cg-status-active { background: #dbeafe; color: #1e40af; }
.cg-status-complete { background: #dcfce7; color: #166534; }
.cg-status-draft { background: #e5e7eb; color: #4b5563; }
.cg-status-error { background: #fee2e2; color: #991b1b; }
.cg-status-pending { background: #fef3c7; color: #92400e; }
/* ─── Avatar Group ────────────────────────────────────── */
.avatar-group { display: flex; align-items: center; }
.ag-avatar {
border-radius: 50%; display: flex; align-items: center; justify-content: center;
color: white; font-weight: 600; border: 2px solid white; margin-left: -8px;
overflow: hidden; flex-shrink: 0;
}
.ag-avatar:first-child { margin-left: 0; }
.ag-sm .ag-avatar { width: 28px; height: 28px; font-size: 10px; }
.ag-md .ag-avatar { width: 36px; height: 36px; font-size: 12px; }
.ag-lg .ag-avatar { width: 44px; height: 44px; font-size: 14px; }
.ag-img { width: 100%; height: 100%; object-fit: cover; }
.ag-initials { line-height: 1; }
.ag-overflow { background: #e5e7eb; color: #4b5563; }
/* ─── Star Rating ─────────────────────────────────────── */
.star-rating-wrap { display: flex; flex-direction: column; gap: 10px; }
.star-rating-summary { display: flex; align-items: center; gap: 8px; }
.star-rating-value { font-size: 24px; font-weight: 700; color: #111827; }
.star-rating-stars { font-size: 18px; color: #f59e0b; letter-spacing: 2px; }
.star-rating-count { font-size: 13px; color: #6b7280; }
.star-rating-distribution { display: flex; flex-direction: column; gap: 4px; }
.star-dist-row { display: flex; align-items: center; gap: 8px; }
.star-dist-label { font-size: 12px; color: #6b7280; width: 24px; text-align: right; flex-shrink: 0; }
.star-dist-track { flex: 1; height: 8px; background: #e5e7eb; border-radius: 4px; overflow: hidden; }
.star-dist-bar { height: 100%; background: #f59e0b; border-radius: 4px; transition: width 0.3s; }
.star-dist-count { font-size: 12px; color: #6b7280; width: 32px; flex-shrink: 0; }
/* ─── Stock Indicator ─────────────────────────────────── */
.stock-indicator {
display: inline-flex; align-items: center; gap: 10px; padding: 10px 16px;
border-radius: 10px; border: 1px solid #e5e7eb; background: white;
}
.stock-icon { font-size: 20px; }
.stock-info { display: flex; flex-direction: column; }
.stock-label { font-size: 13px; font-weight: 600; color: #111827; }
.stock-qty { font-size: 14px; font-weight: 500; color: #374151; }
.stock-level { font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; }
.stock-ok .stock-level { color: #059669; }
.stock-ok { border-color: #bbf7d0; background: #f0fdf4; }
.stock-low .stock-level { color: #d97706; }
.stock-low { border-color: #fde68a; background: #fffbeb; }
.stock-critical .stock-level { color: #dc2626; }
.stock-critical { border-color: #fecaca; background: #fef2f2; }
/* ─── Chat Thread ─────────────────────────────────────── */
.chat-thread {
background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); overflow: hidden;
}
.chat-thread-header {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 20px; border-bottom: 1px solid #e5e7eb; background: #f9fafb;
}
.chat-thread-title { font-size: 16px; font-weight: 600; color: #111827; }
.chat-thread-count { font-size: 12px; color: #6b7280; background: #e5e7eb; padding: 2px 8px; border-radius: 9999px; }
.chat-thread-body { padding: 16px 20px; display: flex; flex-direction: column; gap: 12px; max-height: 600px; overflow-y: auto; }
.chat-msg { display: flex; align-items: flex-end; gap: 8px; }
.chat-msg-outbound { flex-direction: row; justify-content: flex-end; }
.chat-msg-inbound { flex-direction: row; }
.chat-avatar {
width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center;
justify-content: center; color: white; font-weight: 600; font-size: 11px; flex-shrink: 0;
}
.chat-avatar-outbound { order: 2; }
.chat-avatar-img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }
.chat-bubble-wrap { display: flex; flex-direction: column; max-width: 70%; }
.chat-bubble-wrap-right { align-items: flex-end; }
.chat-sender { font-size: 11px; font-weight: 600; color: #6b7280; margin-bottom: 2px; padding: 0 4px; }
.chat-bubble {
padding: 10px 14px; border-radius: 16px; font-size: 14px; line-height: 1.45;
word-break: break-word;
}
.chat-bubble-inbound {
background: #f3f4f6; color: #1f2937;
border-bottom-left-radius: 4px;
}
.chat-bubble-outbound {
background: #4f46e5; color: white;
border-bottom-right-radius: 4px;
}
.chat-meta { font-size: 11px; color: #9ca3af; margin-top: 3px; padding: 0 4px; }
/* ─── Email Preview ───────────────────────────────────── */
.email-preview {
background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); overflow: hidden;
}
.email-header {
padding: 16px 20px; border-bottom: 1px solid #e5e7eb; background: #f9fafb;
}
.email-subject { font-size: 18px; font-weight: 600; color: #111827; margin-bottom: 10px; }
.email-header-row { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; font-size: 13px; }
.email-header-row:last-child { margin-bottom: 0; }
.email-label {
font-weight: 600; color: #6b7280; min-width: 40px; text-transform: uppercase;
font-size: 10px; letter-spacing: 0.05em;
}
.email-value { color: #374151; }
.email-attachments {
padding: 10px 20px; border-bottom: 1px solid #e5e7eb; display: flex; flex-wrap: wrap; gap: 8px;
}
.email-attachment {
display: inline-flex; align-items: center; gap: 4px; padding: 4px 10px;
background: #f3f4f6; border-radius: 6px; font-size: 12px; color: #374151;
}
.email-body { padding: 20px; font-size: 14px; line-height: 1.6; color: #374151; }
.email-body img { max-width: 100%; height: auto; }
/* ─── Content Preview ─────────────────────────────────── */
.content-preview {
background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); overflow: hidden;
}
.content-preview-header {
padding: 14px 20px; border-bottom: 1px solid #e5e7eb;
}
.content-preview-title { font-size: 16px; font-weight: 600; color: #111827; }
.content-preview-body {
padding: 20px; font-size: 14px; line-height: 1.7; color: #374151;
}
.content-preview-body h1 { font-size: 24px; font-weight: 700; margin: 16px 0 8px; color: #111827; }
.content-preview-body h2 { font-size: 20px; font-weight: 600; margin: 14px 0 6px; color: #111827; }
.content-preview-body h3 { font-size: 16px; font-weight: 600; margin: 12px 0 4px; color: #111827; }
.content-preview-body a { color: #4f46e5; }
.content-preview-body a:hover { text-decoration: underline; }
.content-preview-body img { max-width: 100%; height: auto; border-radius: 8px; margin: 8px 0; }
.content-preview-pre {
background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px;
padding: 16px; font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 13px; white-space: pre-wrap; word-break: break-word; overflow-x: auto;
}
/* ─── Transcript View ─────────────────────────────────── */
.transcript-view {
background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); overflow: hidden;
}
.transcript-header {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 20px; border-bottom: 1px solid #e5e7eb; background: #f9fafb;
}
.transcript-title { font-size: 16px; font-weight: 600; color: #111827; }
.transcript-meta { display: flex; align-items: center; gap: 12px; }
.transcript-duration { font-size: 13px; color: #4f46e5; font-weight: 500; }
.transcript-count { font-size: 12px; color: #6b7280; background: #e5e7eb; padding: 2px 8px; border-radius: 9999px; }
.transcript-body { padding: 8px 0; }
.transcript-entry {
display: flex; gap: 16px; padding: 10px 20px;
transition: background 0.15s;
}
.transcript-entry:hover { background: #f9fafb; }
.transcript-timestamp {
font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; color: #9ca3af;
min-width: 60px; flex-shrink: 0; padding-top: 2px;
}
.transcript-content { flex: 1; min-width: 0; }
.transcript-speaker { display: flex; align-items: center; gap: 6px; margin-bottom: 2px; }
.transcript-speaker-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.transcript-speaker-name { font-size: 13px; font-weight: 600; color: #374151; }
.transcript-role-badge {
font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 4px;
text-transform: uppercase; letter-spacing: 0.03em;
}
.transcript-text { font-size: 14px; color: #4b5563; line-height: 1.5; }
/* ─── Audio Player ────────────────────────────────────── */
.audio-player {
background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);
padding: 16px 20px; display: flex; flex-direction: column; gap: 12px;
}
.audio-player-info { display: flex; align-items: center; gap: 10px; }
.audio-player-icon { font-size: 24px; }
.audio-player-title { font-size: 14px; font-weight: 600; color: #111827; }
.audio-player-type { font-size: 12px; color: #6b7280; }
.audio-player-controls { display: flex; align-items: center; gap: 12px; }
.audio-play-btn {
width: 40px; height: 40px; border-radius: 50%; border: none;
background: #4f46e5; color: white; display: flex; align-items: center;
justify-content: center; cursor: pointer; flex-shrink: 0;
transition: background 0.15s, transform 0.1s;
box-shadow: 0 2px 6px rgba(79,70,229,0.3);
}
.audio-play-btn:hover { background: #4338ca; transform: scale(1.05); }
.audio-play-btn:active { transform: scale(0.97); }
.audio-waveform {
flex: 1; display: flex; align-items: flex-end; gap: 2px; height: 36px;
padding: 0 4px;
}
.audio-bar {
flex: 1; min-width: 2px; border-radius: 1px;
background: #d1d5db; transition: background 0.2s;
}
.audio-bar-played { background: #4f46e5; }
.audio-duration {
font-family: 'SF Mono', 'Fira Code', monospace; font-size: 13px;
color: #6b7280; min-width: 40px; text-align: right; flex-shrink: 0;
}
/* ─── Checklist View ──────────────────────────────────── */
.checklist-view {
background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); overflow: hidden;
}
.checklist-header {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 20px; border-bottom: 1px solid #e5e7eb;
}
.checklist-title { font-size: 16px; font-weight: 600; color: #111827; }
.checklist-progress-wrap { display: flex; align-items: center; gap: 10px; }
.checklist-progress-text { font-size: 12px; color: #6b7280; white-space: nowrap; }
.checklist-progress-track { width: 80px; height: 6px; background: #e5e7eb; border-radius: 3px; overflow: hidden; }
.checklist-progress-bar { height: 100%; background: #4f46e5; border-radius: 3px; transition: width 0.3s; }
.checklist-body { padding: 4px 0; }
.checklist-item {
display: flex; align-items: flex-start; gap: 12px; padding: 12px 20px;
border-bottom: 1px solid #f3f4f6; transition: background 0.15s;
}
.checklist-item:last-child { border-bottom: none; }
.checklist-item:hover { background: #f9fafb; }
.checklist-item-done { opacity: 0.7; }
.checklist-checkbox {
width: 20px; height: 20px; border-radius: 6px; border: 2px solid #d1d5db;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0; margin-top: 1px; transition: all 0.15s;
}
.checklist-checkbox-checked {
background: #4f46e5; border-color: #4f46e5; color: white;
}
.checklist-item-content { flex: 1; min-width: 0; }
.checklist-item-title { font-size: 14px; font-weight: 500; color: #111827; }
.checklist-item-title-done { text-decoration: line-through; color: #9ca3af; }
.checklist-item-meta { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 4px; }
.checklist-due, .checklist-assignee { font-size: 12px; color: #6b7280; }
.checklist-priority {
font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em;
padding: 1px 6px; border: 1px solid; border-radius: 4px;
}
/* ─── CalendarView ────────────────────────────────────── */
.cal-view {
background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);
padding: 20px; margin-bottom: 16px;
}
.cal-title { font-size: 18px; font-weight: 600; color: #111827; margin-bottom: 4px; }
.cal-header-month { font-size: 15px; font-weight: 500; color: #4f46e5; margin-bottom: 12px; }
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 1px; background: #e5e7eb; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; }
.cal-day-header {
background: #f9fafb; padding: 8px 4px; text-align: center; font-size: 11px;
font-weight: 600; color: #6b7280; text-transform: uppercase; letter-spacing: 0.05em;
}
.cal-cell {
background: white; min-height: 80px; padding: 4px 6px; position: relative;
display: flex; flex-direction: column;
}
.cal-cell-empty { background: #f9fafb; }
.cal-today { background: #eef2ff; }
.cal-day-num { font-size: 12px; font-weight: 500; color: #374151; margin-bottom: 2px; }
.cal-day-today {
background: #4f46e5; color: white; border-radius: 50%; width: 22px; height: 22px;
display: inline-flex; align-items: center; justify-content: center; font-weight: 600;
}
.cal-evts { display: flex; flex-direction: column; gap: 2px; flex: 1; overflow: hidden; }
.cal-evt {
padding: 1px 4px; border-radius: 3px; font-size: 10px; color: white;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.4;
}
.cal-evt-more { font-size: 9px; color: #6b7280; padding-left: 2px; }
/* ─── FlowDiagram ─────────────────────────────────────── */
.flow-diagram {
background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);
padding: 20px; margin-bottom: 16px; overflow-x: auto;
}
.flow-title { font-size: 18px; font-weight: 600; color: #111827; margin-bottom: 16px; }
.flow-container { display: flex; align-items: center; gap: 0; padding: 12px 0; }
.flow-horizontal { flex-direction: row; }
.flow-vertical { flex-direction: column; }
.flow-node {
padding: 12px 20px; border-radius: 10px; border: 2px solid; text-align: center;
min-width: 120px; flex-shrink: 0; position: relative;
}
.flow-node-start { border-color: #059669; background: #ecfdf5; }
.flow-node-action { border-color: #3b82f6; background: #eff6ff; }
.flow-node-condition { border-color: #d97706; background: #fffbeb; }
.flow-diamond { border-radius: 4px; border-style: dashed; }
.flow-node-end { border-color: #6b7280; background: #f3f4f6; }
.flow-node-label { font-size: 13px; font-weight: 600; color: #111827; }
.flow-node-desc { font-size: 11px; color: #6b7280; margin-top: 4px; }
.flow-arrow {
display: flex; flex-direction: column; align-items: center; justify-content: center;
font-size: 20px; color: #9ca3af; padding: 0 8px; flex-shrink: 0;
}
.flow-arrow-vert { padding: 8px 0; }
.flow-edge-label { font-size: 10px; color: #6b7280; white-space: nowrap; }
/* ─── TreeView ────────────────────────────────────────── */
.tree-view {
background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);
padding: 16px 20px; margin-bottom: 16px;
}
.tree-title { font-size: 18px; font-weight: 600; color: #111827; margin-bottom: 12px; }
.tree-list { display: flex; flex-direction: column; }
.tree-item {
display: flex; align-items: center; gap: 6px; padding: 6px 8px; border-radius: 6px;
cursor: default; transition: background 0.1s;
}
.tree-item:hover { background: #f9fafb; }
.tree-chevron { width: 16px; font-size: 12px; color: #9ca3af; flex-shrink: 0; text-align: center; }
.tree-icon { font-size: 14px; flex-shrink: 0; }
.tree-label { font-size: 14px; color: #111827; font-weight: 500; }
.tree-badge {
padding: 1px 8px; border-radius: 9999px; font-size: 10px; font-weight: 600;
background: #eef2ff; color: #4f46e5; margin-left: 6px;
}
/* ─── MediaGallery ────────────────────────────────────── */
.mg-gallery {
background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);
padding: 20px; margin-bottom: 16px;
}
.mg-title { font-size: 18px; font-weight: 600; color: #111827; margin-bottom: 12px; }
.mg-grid { display: grid; gap: 12px; }
.mg-card {
border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden;
transition: box-shadow 0.15s, border-color 0.15s;
}
.mg-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); border-color: #4f46e5; }
.mg-thumb {
width: 100%; aspect-ratio: 4/3; overflow: hidden; background: #f3f4f6; position: relative;
}
.mg-img { width: 100%; height: 100%; object-fit: cover; }
.mg-placeholder {
width: 100%; height: 100%; display: flex; flex-direction: column;
align-items: center; justify-content: center; gap: 6px; color: #9ca3af;
}
.mg-placeholder-icon { font-size: 32px; }
.mg-type-badge {
padding: 2px 8px; border-radius: 4px; font-size: 10px; font-weight: 700;
background: #e5e7eb; color: #4b5563;
}
.mg-info { padding: 10px 12px; }
.mg-name {
font-size: 13px; font-weight: 600; color: #111827; white-space: nowrap;
overflow: hidden; text-overflow: ellipsis;
}
.mg-meta { display: flex; gap: 12px; margin-top: 4px; font-size: 11px; color: #9ca3af; }
/* ─── DuplicateCompare ────────────────────────────────── */
.dc-compare {
background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);
overflow: hidden; margin-bottom: 16px;
}
.dc-title { font-size: 18px; font-weight: 600; color: #111827; padding: 16px 20px 0; }
.dc-header-row {
display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1px;
background: #f9fafb; border-bottom: 2px solid #e5e7eb;
}
.dc-header-label {
padding: 10px 16px; font-size: 12px; font-weight: 600; color: #6b7280;
text-transform: uppercase; letter-spacing: 0.05em;
}
.dc-body { }
.dc-row {
display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1px;
border-bottom: 1px solid #f3f4f6;
}
.dc-row:last-child { border-bottom: none; }
.dc-field { padding: 10px 16px; font-size: 13px; color: #6b7280; font-weight: 500; background: #f9fafb; }
.dc-val { padding: 10px 16px; font-size: 13px; color: #111827; }
.dc-diff { background: #fefce8; }
/* ─── Chart Components ────────────────────────────────── */
.chart-container {
background: white; border-radius: 12px; padding: 16px;
box-shadow: 0 1px 3px rgba(0,0,0,0.08); margin-bottom: 16px;
}
.chart-title {
font-size: 15px; font-weight: 600; color: #111827; margin-bottom: 12px;
}
.chart-scroll { overflow-x: auto; }
.chart-empty { text-align: center; color: #9ca3af; padding: 32px; font-size: 14px; }
/* Bar Chart - Horizontal */
.bar-chart-h { display: flex; flex-direction: column; gap: 10px; }
.bar-h-row { display: flex; align-items: center; gap: 10px; }
.bar-h-label { width: 90px; font-size: 13px; color: #374151; font-weight: 500; text-align: right; flex-shrink: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bar-h-track { flex: 1; height: 24px; background: #f3f4f6; border-radius: 6px; overflow: hidden; }
.bar-h-fill { height: 100%; border-radius: 6px; transition: width 0.4s ease; min-width: 2px; }
.bar-h-value { width: 60px; font-size: 13px; font-weight: 600; color: #111827; font-family: 'SF Mono', 'Fira Code', monospace; }
/* Bar Chart - Vertical SVG */
.bar-chart-svg { width: 100%; height: auto; max-height: 200px; }
.bar-v-rect { transition: opacity 0.2s; }
.bar-v-rect:hover { opacity: 0.8; }
.bar-v-val { font-size: 11px; fill: #374151; font-weight: 600; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
.bar-v-label { font-size: 10px; fill: #6b7280; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
/* Line Chart SVG */
.line-chart-svg { width: 100%; height: auto; max-height: 200px; }
.chart-axis-text { font-size: 10px; fill: #9ca3af; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
.chart-axis-label { font-size: 10px; fill: #6b7280; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
/* Pie Chart */
.pie-chart-layout { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; justify-content: center; }
.pie-chart-svg { width: 160px; height: 160px; flex-shrink: 0; }
.pie-center-text { font-size: 18px; font-weight: 700; fill: #111827; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
.pie-legend { display: flex; flex-direction: column; gap: 6px; min-width: 120px; }
.pie-legend-item { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.pie-legend-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.pie-legend-label { color: #374151; flex: 1; }
.pie-legend-value { color: #6b7280; font-weight: 500; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; }
/* Funnel Chart */
.funnel-chart { display: flex; flex-direction: column; gap: 8px; }
.funnel-stage { display: flex; align-items: center; gap: 12px; }
.funnel-label-col { width: 100px; flex-shrink: 0; text-align: right; }
.funnel-label { display: block; font-size: 13px; font-weight: 500; color: #374151; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.funnel-value { display: block; font-size: 11px; color: #6b7280; font-family: 'SF Mono', 'Fira Code', monospace; }
.funnel-bar-col { flex: 1; }
.funnel-bar { height: 28px; border-radius: 6px; transition: width 0.4s ease; min-width: 4px; }
.funnel-dropoff { width: 50px; font-size: 11px; color: #ef4444; font-weight: 600; text-align: left; flex-shrink: 0; }
/* Sparkline */
.sparkline-svg { display: inline-block; vertical-align: middle; }
.sparkline-empty { display: inline-flex; align-items: center; justify-content: center; color: #9ca3af; font-size: 12px; vertical-align: middle; }
/* ─── Interactive: Drag & Drop ────────────────────────── */
.kanban-card[draggable] { cursor: grab; transition: transform 0.15s, box-shadow 0.15s, opacity 0.15s; }
.kanban-card[draggable]:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.12); transform: translateY(-1px); }
.kanban-card-dragging { opacity: 0.4; transform: scale(0.95); }
.kanban-drop-target { background: #eef2ff; border: 2px dashed #818cf8; border-radius: 6px; min-height: 40px; transition: background 0.15s; }
/* ─── Interactive: Clickable Rows ─────────────────────── */
.clickable-row { cursor: pointer; transition: background 0.1s; }
.clickable-row:hover { background: #f0f4ff !important; }
/* ─── Modal ───────────────────────────────────────────── */
.mcp-modal-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.4); display: flex;
align-items: center; justify-content: center; z-index: 1000; backdrop-filter: blur(2px);
}
.mcp-modal {
background: white; border-radius: 10px; width: 90%; max-width: 360px;
box-shadow: 0 8px 30px rgba(0,0,0,0.2); overflow: hidden;
}
.mcp-modal-header {
display: flex; align-items: center; justify-content: space-between;
padding: 10px 14px; border-bottom: 1px solid #e5e7eb;
}
.mcp-modal-title { font-size: 13px; font-weight: 600; color: #111827; }
.mcp-modal-close { background: none; border: none; font-size: 18px; cursor: pointer; color: #6b7280; padding: 0 4px; }
.mcp-modal-close:hover { color: #111827; }
.mcp-modal-body { padding: 12px 14px; }
.mcp-modal-footer { padding: 8px 14px; border-top: 1px solid #e5e7eb; display: flex; gap: 8px; justify-content: flex-end; }
.mcp-field { margin-bottom: 10px; }
.mcp-field-label { display: block; font-size: 11px; font-weight: 500; color: #374151; margin-bottom: 3px; }
.mcp-field-input {
width: 100%; padding: 6px 8px; border: 1px solid #d1d5db; border-radius: 6px;
font-size: 12px; outline: none; transition: border-color 0.15s;
}
.mcp-field-input:focus { border-color: #4f46e5; box-shadow: 0 0 0 2px rgba(79,70,229,0.15); }
/* ─── Toast ───────────────────────────────────────────── */
.mcp-toast {
position: fixed; bottom: 12px; left: 50%; transform: translateX(-50%) translateY(20px);
padding: 6px 16px; border-radius: 8px; font-size: 11px; font-weight: 500;
z-index: 2000; opacity: 0; transition: opacity 0.2s, transform 0.2s;
box-shadow: 0 4px 12px rgba(0,0,0,0.15); pointer-events: none;
}
.mcp-toast-show { opacity: 1; transform: translateX(-50%) translateY(0); }
.mcp-toast-success { background: #059669; color: white; }
.mcp-toast-error { background: #dc2626; color: white; }
.mcp-toast-info { background: #1f2937; color: white; }
/* ─── Interactive Components ──────────────────────────── */
.interactive-wrap {
background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);
overflow: hidden; margin-bottom: 8px;
}
/* Contact Picker */
.contact-picker { padding: 12px; }
.cp-search-wrap { position: relative; }
.cp-input { width: 100%; }
.cp-results {
position: absolute; top: 100%; left: 0; right: 0; z-index: 50;
background: white; border: 1px solid #e5e7eb; border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15); max-height: 200px; overflow-y: auto; margin-top: 4px;
}
.cp-result-item { padding: 8px 12px; cursor: pointer; font-size: 13px; border-bottom: 1px solid #f3f4f6; }
.cp-result-item:hover { background: #f0f4ff; }
.cp-selected {
display: flex; align-items: center; justify-content: space-between;
padding: 6px 10px; background: #eef2ff; border-radius: 6px; margin-bottom: 8px;
font-size: 13px; font-weight: 500; color: #4f46e5;
}
.cp-clear { background: none; border: none; cursor: pointer; font-size: 16px; color: #6b7280; padding: 0 4px; }
.cp-clear:hover { color: #dc2626; }
/* Invoice Builder */
.invoice-builder .ib-items td { padding: 6px 4px; }
.invoice-builder .ib-items input { font-size: 12px; }
.invoice-builder .ib-line-total { font-family: 'SF Mono', monospace; font-weight: 500; text-align: right; padding-right: 8px; white-space: nowrap; }
.invoice-builder .ib-grand-total { font-size: 14px; }
.invoice-builder .ib-remove-row { color: #dc2626; }
.invoice-builder .ib-add-row { color: #4f46e5; }
/* Opportunity Editor */
.opportunity-editor select.mcp-field-input {
appearance: auto; padding: 6px 8px; background: white;
}
/* Editable Field */
.editable-field {
display: inline-flex; align-items: center; gap: 4px; cursor: pointer;
padding: 2px 4px; border-radius: 4px; transition: background 0.15s;
}
.editable-field:hover { background: #f3f4f6; }
.ef-edit-icon { font-size: 12px; opacity: 0; transition: opacity 0.15s; }
.editable-field:hover .ef-edit-icon { opacity: 0.6; }
.ef-input { font-size: inherit; }
/* Amount Input */
.amount-input { display: inline-flex; align-items: center; cursor: pointer; }
.ai-display { cursor: pointer; }
.ai-raw { font-size: 16px; width: 120px; }
/* Select Dropdown */
.select-dropdown { appearance: auto; background: white; }
`;

View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist"
},
"include": ["src/**/*.ts", "*.ts"]
}

View File

@ -0,0 +1,32 @@
import { defineConfig } from "vite";
import { viteSingleFile } from "vite-plugin-singlefile";
import path from "path";
import fs from "fs";
export default defineConfig({
plugins: [
viteSingleFile(),
{
name: 'copy-output',
closeBundle() {
const outDir = path.resolve(__dirname, "../../../dist/app-ui");
const src = path.join(outDir, "index.html");
const dstDynamic = path.join(outDir, "dynamic-view.html");
const dstUniversal = path.join(outDir, "universal-renderer.html");
if (fs.existsSync(src)) {
// Copy to both names — universal-renderer is the canonical one
fs.copyFileSync(src, dstUniversal);
fs.renameSync(src, dstDynamic);
}
}
}
],
root: path.resolve(__dirname),
build: {
outDir: path.resolve(__dirname, "../../../dist/app-ui"),
emptyOutDir: false,
rollupOptions: {
input: path.resolve(__dirname, "index.html"),
},
},
});

View File

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MCP UI Kit</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

3190
src/ui/react-app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
{
"name": "mcp-ui-kit-react",
"version": "1.0.0",
"private": true,
"type": "module",
"description": "MCP UI Kit — Generic React component library for MCP App UIs",
"scripts": {
"build": "tsc --noEmit && vite build",
"dev": "vite build --watch"
},
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
"@modelcontextprotocol/sdk": "^1.24.0",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
"devDependencies": {
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.9.3",
"vite": "^6.0.0",
"vite-plugin-singlefile": "^2.3.0"
}
}

View File

@ -0,0 +1,211 @@
/**
* App.tsx Root component for the MCP UI Kit React app.
*
* Uses useApp from ext-apps/react to connect to the MCP host.
* Receives UI trees via ontoolresult and renders them via UITreeRenderer.
*
* HYBRID INTERACTIVITY: Uses mergeUITrees to preserve local component state
* across tool result updates. Wraps with ChangeTrackerProvider for shared
* change tracking across all interactive components.
*/
import React, { useState, useEffect } from "react";
import type { McpUiHostContext } from "@modelcontextprotocol/ext-apps";
import { useApp } from "@modelcontextprotocol/ext-apps/react";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { MCPAppProvider } from "./context/MCPAppContext.js";
import { ChangeTrackerProvider } from "./context/ChangeTrackerContext.js";
import { UITreeRenderer } from "./renderer/UITreeRenderer.js";
import { ToastProvider } from "./components/shared/Toast.js";
import { SaveIndicator } from "./components/shared/SaveIndicator.js";
import { mergeUITrees } from "./utils/mergeUITrees.js";
import type { UITree } from "./types.js";
import "./styles/base.css";
import "./styles/interactive.css";
// ─── Parse UI Tree from tool result ─────────────────────────
function extractUITree(result: CallToolResult): UITree | null {
// 1. Check structuredContent first — this is where generateDynamicView puts the uiTree
const sc = (result as any).structuredContent;
if (sc) {
if (sc.uiTree && sc.uiTree.root && sc.uiTree.elements) {
return sc.uiTree as UITree;
}
// structuredContent might BE the tree directly
if (sc.root && sc.elements) {
return sc as UITree;
}
}
// 2. Check content array for JSON text containing a UI tree
if (result.content) {
for (const item of result.content) {
if (item.type === "text") {
try {
const parsed = JSON.parse(item.text);
if (parsed && parsed.root && parsed.elements) {
return parsed as UITree;
}
// Might be wrapped: { uiTree: { root, elements } }
if (parsed?.uiTree?.root && parsed?.uiTree?.elements) {
return parsed.uiTree as UITree;
}
} catch {
// Not JSON — skip
}
}
}
}
return null;
}
/** Check for server-injected data via window.__MCP_APP_DATA__ */
function getPreInjectedTree(): UITree | null {
try {
const data = (window as any).__MCP_APP_DATA__;
if (!data) return null;
if (data.uiTree?.root && data.uiTree?.elements) return data.uiTree as UITree;
if (data.root && data.elements) return data as UITree;
} catch { /* ignore */ }
return null;
}
// ─── Main App ───────────────────────────────────────────────
export function App() {
const [uiTree, setUITree] = useState<UITree | null>(null);
const [hostContext, setHostContext] = useState<McpUiHostContext | undefined>();
const [toolInput, setToolInput] = useState<string | null>(null);
// Check for pre-injected data (server injects via window.__MCP_APP_DATA__)
useEffect(() => {
const preInjected = getPreInjectedTree();
if (preInjected && !uiTree) {
console.info("[MCPApp] Found pre-injected UI tree");
setUITree(preInjected);
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const { app, isConnected, error } = useApp({
appInfo: { name: "MCP UI Kit", version: "1.0.0" },
capabilities: {},
onAppCreated: (app) => {
app.ontoolresult = async (result) => {
console.info("[MCPApp] Tool result received:", result);
const newTree = extractUITree(result);
if (newTree) {
// CRITICAL FIX: Merge trees instead of replacing.
// This preserves React component state (form inputs, drag state, etc.)
// by keeping exact old references for unchanged elements.
setUITree((prevTree) => {
if (!prevTree) return newTree;
return mergeUITrees(prevTree, newTree);
});
setToolInput(null);
}
};
app.ontoolinput = async (input) => {
console.info("[MCPApp] Tool input received:", input);
setToolInput("Loading view...");
};
app.ontoolcancelled = (params) => {
console.info("[MCPApp] Tool cancelled:", params.reason);
setToolInput(null);
};
app.onerror = (err) => {
console.error("[MCPApp] Error:", err);
};
app.onhostcontextchanged = (params) => {
setHostContext((prev) => ({ ...prev, ...params }));
};
},
});
useEffect(() => {
if (app) {
setHostContext(app.getHostContext());
}
}, [app]);
// Error state
if (error) {
return (
<div className="error-state">
<h3>Connection Error</h3>
<p>{error.message}</p>
</div>
);
}
// Connecting state
if (!isConnected || !app) {
return (
<div className="loading-state">
<div className="loading-spinner" />
<p>Connecting to host...</p>
</div>
);
}
const safeAreaStyle = {
paddingTop: hostContext?.safeAreaInsets?.top,
paddingRight: hostContext?.safeAreaInsets?.right,
paddingBottom: hostContext?.safeAreaInsets?.bottom,
paddingLeft: hostContext?.safeAreaInsets?.left,
};
// Tool call in progress (no tree yet)
if (toolInput && !uiTree) {
return (
<MCPAppProvider app={app}>
<ChangeTrackerProvider>
<ToastProvider>
<div id="app" style={safeAreaStyle}>
<div className="loading-state">
<div className="loading-spinner" />
<p>{toolInput}</p>
</div>
</div>
</ToastProvider>
</ChangeTrackerProvider>
</MCPAppProvider>
);
}
// Waiting for first tool result
if (!uiTree) {
return (
<MCPAppProvider app={app}>
<ChangeTrackerProvider>
<ToastProvider>
<div id="app" style={safeAreaStyle}>
<div className="loading-state">
<div className="loading-spinner" />
<p>Waiting for data...</p>
</div>
</div>
</ToastProvider>
</ChangeTrackerProvider>
</MCPAppProvider>
);
}
// Render the UI tree
return (
<MCPAppProvider app={app}>
<ChangeTrackerProvider>
<ToastProvider>
<div id="app" style={safeAreaStyle}>
<UITreeRenderer tree={uiTree} />
<SaveIndicator />
</div>
</ToastProvider>
</ChangeTrackerProvider>
</MCPAppProvider>
);
}

View File

@ -0,0 +1,149 @@
import React, { useState, useMemo } from 'react';
import { useApp } from '@modelcontextprotocol/ext-apps/react';
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
import { PageHeader } from '../../components/layout/PageHeader';
import { StatsGrid } from '../../components/layout/StatsGrid';
import { MetricCard } from '../../components/data/MetricCard';
import { DataTable } from '../../components/data/DataTable';
import { StatusBadge } from '../../components/data/StatusBadge';
import { CurrencyDisplay } from '../../components/data/CurrencyDisplay';
import '../../styles/base.css';
import '../../styles/interactive.css';
function extractData(result: CallToolResult): any {
const sc = (result as any).structuredContent;
if (sc) return sc;
for (const item of result.content || []) {
if (item.type === 'text') {
try { return JSON.parse(item.text); } catch {}
}
}
return null;
}
function formatCurrency(amount: number | string | undefined): string {
if (amount === undefined || amount === null) return '$0.00';
const n = typeof amount === 'string' ? parseFloat(amount) : amount;
if (isNaN(n)) return '$0.00';
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(n);
}
function formatPercent(val: number | string | undefined): string {
if (val === undefined || val === null) return '0%';
const n = typeof val === 'string' ? parseFloat(val) : val;
if (isNaN(n)) return '0%';
return `${n.toFixed(1)}%`;
}
const statusVariantMap: Record<string, string> = {
active: 'active',
paused: 'paused',
draft: 'draft',
completed: 'complete',
ended: 'complete',
inactive: 'draft',
};
export function App() {
const [data, setData] = useState<any>((window as any).__MCP_APP_DATA__ || null);
const [search, setSearch] = useState('');
const { isConnected, error } = useApp({
appInfo: { name: 'Affiliate Dashboard', version: '1.0.0' },
capabilities: {},
onAppCreated: (app) => {
app.ontoolresult = async (result) => {
const parsed = extractData(result);
if (parsed) setData(parsed);
};
},
});
const stats = useMemo(() => {
const s = data?.stats || data?.summary || {};
return {
totalAffiliates: s.totalAffiliates || s.affiliateCount || 0,
activeCampaigns: s.activeCampaigns || s.campaignCount || 0,
totalCommissions: s.totalCommissions || s.commissions || 0,
conversionRate: s.conversionRate || s.conversion || 0,
};
}, [data]);
const campaigns = useMemo(() => {
const items: any[] = data?.campaigns || [];
return items
.map((c) => {
const status = (c.status || 'active').toLowerCase();
return {
id: c.id || '',
name: c.name || c.title || 'Untitled Campaign',
status: status.charAt(0).toUpperCase() + status.slice(1),
statusVariant: statusVariantMap[status] || 'draft',
commissionType: c.commissionType || c.type || 'Percentage',
earnings: formatCurrency(c.earnings || c.totalEarnings || c.revenue || 0),
affiliates: c.affiliateCount || c.affiliates || 0,
};
})
.filter((c) => {
if (!search) return true;
const q = search.toLowerCase();
return c.name.toLowerCase().includes(q) || c.commissionType.toLowerCase().includes(q);
});
}, [data, search]);
if (error) return <div className="error-state"><h3>Error</h3><p>{error.message}</p></div>;
if (!isConnected) return <div className="loading-state"><div className="loading-spinner" /><p>Connecting...</p></div>;
if (!data) return <div className="loading-state"><div className="loading-spinner" /><p>Waiting for data...</p></div>;
return (
<PageHeader title="Affiliate Dashboard" subtitle="Manage your affiliate program">
<StatsGrid columns={4}>
<MetricCard
label="Total Affiliates"
value={stats.totalAffiliates.toLocaleString()}
color="blue"
/>
<MetricCard
label="Active Campaigns"
value={stats.activeCampaigns.toLocaleString()}
color="green"
/>
<MetricCard
label="Total Commissions"
value={formatCurrency(stats.totalCommissions)}
color="purple"
/>
<MetricCard
label="Conversion Rate"
value={formatPercent(stats.conversionRate)}
color="yellow"
/>
</StatsGrid>
<div style={{ marginTop: 24 }}>
<h3 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 600, color: '#1f2937' }}>Campaigns</h3>
<div className="search-bar" style={{ marginBottom: 12 }}>
<input
type="text"
className="search-input"
placeholder="Search campaigns…"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
<DataTable
columns={[
{ key: 'name', label: 'Campaign Name', sortable: true },
{ key: 'status', label: 'Status', format: 'status', sortable: true },
{ key: 'commissionType', label: 'Commission Type', sortable: true },
{ key: 'earnings', label: 'Earnings', format: 'currency', sortable: true },
{ key: 'affiliates', label: 'Affiliates', sortable: true },
]}
rows={campaigns}
pageSize={20}
emptyMessage="No campaigns found"
/>
</div>
</PageHeader>
);
}

View File

@ -0,0 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Affiliate Dashboard</title></head>
<body><div id="root"></div><script type="module" src="./main.tsx"></script></body>
</html>

View File

@ -0,0 +1,4 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App';
createRoot(document.getElementById('root')!).render(<React.StrictMode><App /></React.StrictMode>);

View File

@ -0,0 +1,22 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { viteSingleFile } from 'vite-plugin-singlefile';
import path from 'path';
export default defineConfig({
plugins: [react(), viteSingleFile()],
root: __dirname,
build: {
outDir: path.resolve(__dirname, '../../../../dist/apps/affiliate-dashboard'),
emptyOutDir: false,
rollupOptions: { input: path.resolve(__dirname, 'index.html') },
},
resolve: {
alias: {
'@components': path.resolve(__dirname, '../../components'),
'@hooks': path.resolve(__dirname, '../../hooks'),
'@styles': path.resolve(__dirname, '../../styles'),
'@context': path.resolve(__dirname, '../../context'),
},
},
});

View File

@ -0,0 +1,226 @@
/**
* Agent Stats User/agent performance metrics dashboard.
* Shows calls made, emails sent, tasks completed, appointments booked.
* Line chart: activity over time. Bar chart: performance by metric.
* Table: detailed activity log.
*/
import React, { useMemo } from "react";
import { useApp } from "@modelcontextprotocol/ext-apps/react";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { PageHeader } from "../../components/layout/PageHeader.js";
import { StatsGrid } from "../../components/layout/StatsGrid.js";
import { Section } from "../../components/layout/Section.js";
import { MetricCard } from "../../components/data/MetricCard.js";
import { LineChart } from "../../components/charts/LineChart.js";
import { BarChart } from "../../components/charts/BarChart.js";
import { DataTable } from "../../components/data/DataTable.js";
import { SparklineChart } from "../../components/charts/SparklineChart.js";
import "../../styles/base.css";
import "../../styles/interactive.css";
// ─── Types ──────────────────────────────────────────────────
interface ActivityEntry {
date: string;
type: string;
description: string;
contact?: string;
outcome?: string;
}
interface LocationData {
name?: string;
id?: string;
}
interface AgentStatsData {
userId?: string;
location: LocationData;
dateRange: string;
callsMade?: number;
emailsSent?: number;
tasksCompleted?: number;
appointmentsBooked?: number;
activityLog?: ActivityEntry[];
activityOverTime?: { label: string; value: number }[];
}
// ─── Data Extraction ────────────────────────────────────────
function extractData(result: CallToolResult): AgentStatsData | null {
const sc = (result as any).structuredContent;
if (sc) return sc as AgentStatsData;
if (result.content) {
for (const item of result.content) {
if (item.type === "text") {
try {
return JSON.parse(item.text) as AgentStatsData;
} catch {
/* skip */
}
}
}
}
return null;
}
// ─── App ────────────────────────────────────────────────────
export function App() {
const [data, setData] = React.useState<AgentStatsData | null>(null);
const { app, isConnected, error } = useApp({
appInfo: { name: "agent-stats", version: "1.0.0" },
capabilities: {},
onAppCreated: (app) => {
app.ontoolresult = async (result) => {
const extracted = extractData(result);
if (extracted) setData(extracted);
};
},
});
// Check for pre-injected data
React.useEffect(() => {
const preInjected = (window as any).__MCP_APP_DATA__;
if (preInjected && !data) setData(preInjected as AgentStatsData);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// Derived data
const callsMade = data?.callsMade ?? 0;
const emailsSent = data?.emailsSent ?? 0;
const tasksCompleted = data?.tasksCompleted ?? 0;
const appointmentsBooked = data?.appointmentsBooked ?? 0;
const performanceBars = useMemo(() => [
{ label: "Calls", value: callsMade, color: "#4f46e5" },
{ label: "Emails", value: emailsSent, color: "#7c3aed" },
{ label: "Tasks", value: tasksCompleted, color: "#16a34a" },
{ label: "Appts", value: appointmentsBooked, color: "#3b82f6" },
], [callsMade, emailsSent, tasksCompleted, appointmentsBooked]);
const activityPoints = useMemo(
() => data?.activityOverTime ?? [],
[data?.activityOverTime],
);
const activityLog = useMemo(() => data?.activityLog ?? [], [data?.activityLog]);
const tableColumns = useMemo(() => [
{ key: "date", label: "Date", sortable: true, format: "date" },
{ key: "type", label: "Type", sortable: true },
{ key: "description", label: "Description" },
{ key: "contact", label: "Contact" },
{ key: "outcome", label: "Outcome" },
], []);
// Sparkline values from activity over time
const sparklineValues = useMemo(
() => activityPoints.map((p) => p.value),
[activityPoints],
);
if (error) {
return (
<div className="error-state">
<h3>Connection Error</h3>
<p>{error.message}</p>
</div>
);
}
if (!data) {
return (
<div className="loading-state">
<div className="loading-spinner" />
<p>{isConnected ? "Waiting for data..." : "Connecting..."}</p>
</div>
);
}
const totalActivities = callsMade + emailsSent + tasksCompleted + appointmentsBooked;
return (
<div id="app" style={{ padding: 4 }}>
<PageHeader
title="Agent Performance"
subtitle={
data.userId
? `Agent: ${data.userId} · ${data.dateRange}`
: `All Agents · ${data.dateRange}`
}
stats={[
{ label: "Location", value: data.location?.name ?? "—" },
{ label: "Total Activities", value: totalActivities.toLocaleString() },
]}
/>
<StatsGrid columns={4}>
<MetricCard
label="Calls Made"
value={callsMade.toLocaleString()}
color="blue"
trend={callsMade > 0 ? "up" : "flat"}
trendValue={`${callsMade}`}
/>
<MetricCard
label="Emails Sent"
value={emailsSent.toLocaleString()}
color="purple"
trend={emailsSent > 0 ? "up" : "flat"}
trendValue={`${emailsSent}`}
/>
<MetricCard
label="Tasks Completed"
value={tasksCompleted.toLocaleString()}
color="green"
trend={tasksCompleted > 0 ? "up" : "flat"}
trendValue={`${tasksCompleted}`}
/>
<MetricCard
label="Appointments Booked"
value={appointmentsBooked.toLocaleString()}
color="blue"
trend={appointmentsBooked > 0 ? "up" : "flat"}
trendValue={`${appointmentsBooked}`}
/>
</StatsGrid>
{sparklineValues.length > 1 && (
<div style={{ margin: "12px 0", textAlign: "right" }}>
<span style={{ fontSize: 11, color: "#6b7280", marginRight: 4 }}>Trend:</span>
<SparklineChart values={sparklineValues} color="#4f46e5" height={20} width={100} />
</div>
)}
<Section title="Activity Over Time">
<LineChart
points={activityPoints}
color="#4f46e5"
showArea
showPoints
title="Activities"
yAxisLabel="Count"
/>
</Section>
<Section title="Performance by Metric">
<BarChart
bars={performanceBars}
orientation="vertical"
showValues
title="Metric Breakdown"
/>
</Section>
<Section title="Activity Log">
<DataTable
columns={tableColumns}
rows={activityLog}
pageSize={10}
emptyMessage="No activities recorded"
/>
</Section>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Agent Stats</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "./App.js";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>,
);

View File

@ -0,0 +1,22 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { viteSingleFile } from 'vite-plugin-singlefile';
import path from 'path';
export default defineConfig({
plugins: [react(), viteSingleFile()],
root: __dirname,
build: {
outDir: path.resolve(__dirname, '../../../../dist/apps/agent-stats'),
emptyOutDir: false,
rollupOptions: { input: path.resolve(__dirname, 'index.html') },
},
resolve: {
alias: {
'@components': path.resolve(__dirname, '../../components'),
'@hooks': path.resolve(__dirname, '../../hooks'),
'@styles': path.resolve(__dirname, '../../styles'),
'@context': path.resolve(__dirname, '../../context'),
},
},
});

View File

@ -0,0 +1,93 @@
import React, { useState } from 'react';
import { useApp } from '@modelcontextprotocol/ext-apps/react';
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
import { PageHeader } from '../../components/layout/PageHeader';
import { Card } from '../../components/layout/Card';
import { AppointmentBooker } from '../../components/interactive/AppointmentBooker';
import { MCPAppProvider } from '../../context/MCPAppContext';
import { ChangeTrackerProvider } from '../../context/ChangeTrackerContext';
import '../../styles/base.css';
import '../../styles/interactive.css';
function extractData(result: CallToolResult): any {
const sc = (result as any).structuredContent;
if (sc) return sc;
for (const item of result.content || []) {
if (item.type === 'text') {
try { return JSON.parse(item.text); } catch {}
}
}
return null;
}
export function App() {
const [data, setData] = useState<any>((window as any).__MCP_APP_DATA__ || null);
const [appInstance, setAppInstance] = useState<any>(null);
const { isConnected, error } = useApp({
appInfo: { name: 'Appointment Booker', version: '1.0.0' },
capabilities: {},
onAppCreated: (app) => {
setAppInstance(app);
app.ontoolresult = async (result) => {
const d = extractData(result);
if (d) setData(d);
};
},
});
const calendar = data?.calendar;
const contact = data?.contact;
const locationId = data?.locationId;
const slots = data?.slots;
if (error) {
return <div className="error-state"><h3>Error</h3><p>{error.message}</p></div>;
}
if (!isConnected && !data) {
return <div className="loading-state"><div className="loading-spinner" /><p>Connecting...</p></div>;
}
if (!data) {
return <div className="loading-state"><div className="loading-spinner" /><p>Waiting for calendar data...</p></div>;
}
return (
<ChangeTrackerProvider>
<MCPAppProvider app={appInstance}>
<div>
<PageHeader
title="Book Appointment"
subtitle={calendar?.name || 'Select a date and time'}
stats={[
...(calendar?.name ? [{ label: 'Calendar', value: calendar.name }] : []),
...(contact ? [{ label: 'Contact', value: contact.name || contact.firstName || contact.email || 'Selected' }] : []),
]}
/>
{/* Contact info card if pre-selected */}
{contact && (
<Card title="Contact" padding="sm">
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: 13 }}>
<div style={{ fontWeight: 600 }}>
{contact.name || [contact.firstName, contact.lastName].filter(Boolean).join(' ')}
</div>
{contact.email && <div style={{ color: '#6b7280' }}>📧 {contact.email}</div>}
{contact.phone && <div style={{ color: '#6b7280' }}>📞 {contact.phone}</div>}
</div>
</Card>
)}
<Card padding="sm">
<AppointmentBooker
slots={slots}
calendarId={calendar?.id || locationId}
calendarTool="get_calendar_free_slots"
bookTool="create_appointment"
contactSearchTool="search_contacts"
/>
</Card>
</div>
</MCPAppProvider>
</ChangeTrackerProvider>
);
}

View File

@ -0,0 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /></head>
<body><div id="root"></div><script type="module" src="./main.tsx"></script></body>
</html>

View File

@ -0,0 +1,9 @@
import React, { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App.js';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
);

View File

@ -0,0 +1,22 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { viteSingleFile } from 'vite-plugin-singlefile';
import path from 'path';
export default defineConfig({
plugins: [react(), viteSingleFile()],
root: __dirname,
build: {
outDir: path.resolve(__dirname, '../../../../dist/apps/appointment-booker'),
emptyOutDir: false,
rollupOptions: { input: path.resolve(__dirname, 'index.html') },
},
resolve: {
alias: {
'@components': path.resolve(__dirname, '../../components'),
'@hooks': path.resolve(__dirname, '../../hooks'),
'@styles': path.resolve(__dirname, '../../styles'),
'@context': path.resolve(__dirname, '../../context'),
},
},
});

View File

@ -0,0 +1,177 @@
import React, { useState, useMemo } from 'react';
import { useApp } from '@modelcontextprotocol/ext-apps/react';
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
import { DetailHeader } from '../../components/data/DetailHeader';
import { KeyValueList } from '../../components/data/KeyValueList';
import { Timeline } from '../../components/data/Timeline';
import { Card } from '../../components/layout/Card';
import { ActionBar } from '../../components/shared/ActionBar';
import { ActionButton } from '../../components/shared/ActionButton';
import { MCPAppProvider } from '../../context/MCPAppContext';
import { ChangeTrackerProvider } from '../../context/ChangeTrackerContext';
import type { KeyValueItem, TimelineEvent, StatusVariant } from '../../types';
import '../../styles/base.css';
import '../../styles/interactive.css';
function formatDate(d?: string): string {
if (!d) return '—';
try {
return new Date(d).toLocaleDateString('en-US', {
month: 'short', day: 'numeric', year: 'numeric',
});
} catch { return d; }
}
function extractData(result: CallToolResult): any {
const sc = (result as any).structuredContent;
if (sc) return sc;
for (const item of result.content || []) {
if (item.type === 'text') {
try { return JSON.parse(item.text); } catch {}
}
}
return null;
}
const statusMap: Record<string, StatusVariant> = {
confirmed: 'active',
booked: 'active',
completed: 'complete',
cancelled: 'error',
no_show: 'error',
pending: 'pending',
rescheduled: 'paused',
};
export function App() {
const [data, setData] = useState<any>((window as any).__MCP_APP_DATA__ || null);
const [appInstance, setAppInstance] = useState<any>(null);
const { isConnected, error } = useApp({
appInfo: { name: 'Appointment Detail', version: '1.0.0' },
capabilities: {},
onAppCreated: (app) => {
setAppInstance(app);
app.ontoolresult = async (result) => {
const d = extractData(result);
if (d) setData(d);
};
},
});
const appt = data?.appointment;
const notes: any[] = data?.notes || [];
const kvItems: KeyValueItem[] = useMemo(() => {
if (!appt) return [];
const items: KeyValueItem[] = [];
if (appt.title || appt.name) items.push({ label: 'Title', value: appt.title || appt.name });
// Date/time
if (appt.startTime) {
const start = new Date(appt.startTime);
items.push({ label: 'Date', value: start.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }) });
items.push({ label: 'Time', value: start.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }) });
}
if (appt.endTime && appt.startTime) {
const start = new Date(appt.startTime).getTime();
const end = new Date(appt.endTime).getTime();
const mins = Math.round((end - start) / 60000);
items.push({ label: 'Duration', value: mins >= 60 ? `${Math.floor(mins / 60)}h ${mins % 60}m` : `${mins}m` });
}
// Contact
const contactName = appt.contactName || appt.contact?.name ||
[appt.contact?.firstName, appt.contact?.lastName].filter(Boolean).join(' ');
if (contactName) items.push({ label: 'Contact', value: contactName, bold: true });
if (appt.contact?.email || appt.email) items.push({ label: 'Email', value: appt.contact?.email || appt.email });
if (appt.contact?.phone || appt.phone) items.push({ label: 'Phone', value: appt.contact?.phone || appt.phone });
// Status
if (appt.status || appt.appointmentStatus) {
const s = appt.status || appt.appointmentStatus;
items.push({ label: 'Status', value: s.charAt(0).toUpperCase() + s.slice(1), variant: statusMap[s.toLowerCase()] === 'error' ? 'danger' : statusMap[s.toLowerCase()] === 'active' ? 'success' : undefined });
}
// Calendar / location
if (appt.calendarName || appt.calendar?.name) items.push({ label: 'Calendar', value: appt.calendarName || appt.calendar?.name });
if (appt.locationName || appt.location) items.push({ label: 'Location', value: appt.locationName || appt.location });
return items;
}, [appt]);
const timelineEvents: TimelineEvent[] = useMemo(() => {
return notes.map((note) => ({
title: note.title || 'Note',
description: note.body || note.content || note.text || '',
timestamp: note.dateAdded || note.createdAt || note.date || '',
icon: 'note' as const,
variant: 'default' as const,
}));
}, [notes]);
if (error) {
return <div className="error-state"><h3>Error</h3><p>{error.message}</p></div>;
}
if (!isConnected && !data) {
return <div className="loading-state"><div className="loading-spinner" /><p>Connecting...</p></div>;
}
if (!data) {
return <div className="loading-state"><div className="loading-spinner" /><p>Waiting for appointment data...</p></div>;
}
const status = appt?.status || appt?.appointmentStatus || '';
const statusVariant = statusMap[status.toLowerCase()] || 'active';
return (
<ChangeTrackerProvider>
<MCPAppProvider app={appInstance}>
<div>
<DetailHeader
title={appt?.title || appt?.name || 'Appointment'}
subtitle={formatDate(appt?.startTime)}
entityId={appt?.id}
status={status ? status.charAt(0).toUpperCase() + status.slice(1) : undefined}
statusVariant={statusVariant}
/>
<Card title="Details" padding="sm">
<KeyValueList items={kvItems} />
</Card>
{notes.length > 0 && (
<Card title="Notes" padding="sm">
<Timeline events={timelineEvents} />
</Card>
)}
<ActionBar align="right">
<ActionButton
label="Edit"
variant="secondary"
size="sm"
toolName="update_appointment"
toolArgs={{ appointmentId: appt?.id }}
/>
<ActionButton
label="Cancel Appointment"
variant="danger"
size="sm"
toolName="update_appointment"
toolArgs={{ appointmentId: appt?.id, status: 'cancelled' }}
/>
<ActionButton
label="Add Note"
variant="primary"
size="sm"
toolName="create_note"
toolArgs={{ contactId: appt?.contactId, body: '' }}
/>
</ActionBar>
</div>
</MCPAppProvider>
</ChangeTrackerProvider>
);
}

View File

@ -0,0 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /></head>
<body><div id="root"></div><script type="module" src="./main.tsx"></script></body>
</html>

View File

@ -0,0 +1,9 @@
import React, { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App.js';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
);

View File

@ -0,0 +1,22 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { viteSingleFile } from 'vite-plugin-singlefile';
import path from 'path';
export default defineConfig({
plugins: [react(), viteSingleFile()],
root: __dirname,
build: {
outDir: path.resolve(__dirname, '../../../../dist/apps/appointment-detail'),
emptyOutDir: false,
rollupOptions: { input: path.resolve(__dirname, 'index.html') },
},
resolve: {
alias: {
'@components': path.resolve(__dirname, '../../components'),
'@hooks': path.resolve(__dirname, '../../hooks'),
'@styles': path.resolve(__dirname, '../../styles'),
'@context': path.resolve(__dirname, '../../context'),
},
},
});

View File

@ -0,0 +1,320 @@
/**
* blog-manager Blog posts table with search and filters.
* Shows posts with title, author, status, site, dates. Client-side filtering and search.
*/
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { useApp } from '@modelcontextprotocol/ext-apps/react';
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
import { MCPAppProvider } from '../../context/MCPAppContext';
import { ChangeTrackerProvider } from '../../context/ChangeTrackerContext';
import { PageHeader } from '../../components/layout/PageHeader';
import { DataTable } from '../../components/data/DataTable';
import { StatusBadge } from '../../components/data/StatusBadge';
import { SearchBar } from '../../components/shared/SearchBar';
import { FilterChips } from '../../components/shared/FilterChips';
import '../../styles/base.css';
import '../../styles/interactive.css';
// ─── Types ──────────────────────────────────────────────────
interface BlogPost {
id?: string;
title?: string;
author?: string;
status?: string;
site?: string;
siteId?: string;
publishedAt?: string;
updatedAt?: string;
slug?: string;
}
interface BlogSite {
id: string;
name: string;
}
interface BlogData {
posts: BlogPost[];
sites?: BlogSite[];
}
// ─── Extract data from tool result ──────────────────────────
function extractData(result: CallToolResult): BlogData | null {
const sc = (result as any).structuredContent;
if (sc) return sc as BlogData;
if (result.content) {
for (const item of result.content) {
if (item.type === 'text') {
try { return JSON.parse(item.text) as BlogData; } catch { /* skip */ }
}
}
}
return null;
}
// ─── App ────────────────────────────────────────────────────
export function App() {
const [data, setData] = useState<BlogData | null>(null);
useEffect(() => {
const d = (window as any).__MCP_APP_DATA__;
if (d && !data) setData(d as BlogData);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const { app, isConnected, error } = useApp({
appInfo: { name: 'blog-manager', version: '1.0.0' },
capabilities: {},
onAppCreated: (createdApp) => {
createdApp.ontoolresult = async (result) => {
const d = extractData(result);
if (d) setData(d);
};
},
});
if (error) {
return <div className="error-state"><h3>Connection Error</h3><p>{error.message}</p></div>;
}
if (!isConnected || !app) {
return <div className="loading-state"><div className="loading-spinner" /><p>Connecting...</p></div>;
}
if (!data) {
return <div className="loading-state"><div className="loading-spinner" /><p>Waiting for data...</p></div>;
}
return (
<MCPAppProvider app={app}>
<ChangeTrackerProvider>
<div id="app">
<BlogManagerView data={data} app={app} />
</div>
</ChangeTrackerProvider>
</MCPAppProvider>
);
}
// ─── View ───────────────────────────────────────────────────
function BlogManagerView({ data, app }: { data: BlogData; app: any }) {
const { posts, sites = [] } = data;
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState<Set<string>>(new Set());
const [siteFilter, setSiteFilter] = useState<Set<string>>(new Set());
const [authorFilter, setAuthorFilter] = useState<Set<string>>(new Set());
const [actionResult, setActionResult] = useState<{ type: 'success' | 'error'; msg: string } | null>(null);
const [isActing, setIsActing] = useState(false);
const handleBlogAction = useCallback(async (action: string, blogData: Record<string, any>) => {
if (!app) return;
setIsActing(true);
setActionResult(null);
try {
await app.updateModelContext({
content: [{
type: 'text',
text: JSON.stringify({ action, data: blogData }),
}],
});
setActionResult({ type: 'success', msg: `${action.replace('_', ' ')} request sent` });
setTimeout(() => setActionResult(null), 3000);
} catch {
setActionResult({ type: 'error', msg: '✗ Failed to send request' });
} finally {
setIsActing(false);
}
}, [app]);
// Derive unique values for filters
const uniqueStatuses = useMemo(() => {
const set = new Set<string>();
posts.forEach(p => { if (p.status) set.add(p.status); });
return Array.from(set);
}, [posts]);
const uniqueSites = useMemo(() => {
const set = new Set<string>();
posts.forEach(p => { if (p.site) set.add(p.site); });
return Array.from(set);
}, [posts]);
const uniqueAuthors = useMemo(() => {
const set = new Set<string>();
posts.forEach(p => { if (p.author) set.add(p.author); });
return Array.from(set);
}, [posts]);
// Apply client-side filters and search
const filteredPosts = useMemo(() => {
return posts.filter(p => {
// Status filter
if (statusFilter.size > 0 && p.status && !statusFilter.has(p.status)) return false;
// Site filter
if (siteFilter.size > 0 && p.site && !siteFilter.has(p.site)) return false;
// Author filter
if (authorFilter.size > 0 && p.author && !authorFilter.has(p.author)) return false;
// Search query
if (searchQuery.trim()) {
const q = searchQuery.toLowerCase();
const matches = [p.title, p.author, p.site, p.slug]
.filter(Boolean)
.some(v => v!.toLowerCase().includes(q));
if (!matches) return false;
}
return true;
});
}, [posts, statusFilter, siteFilter, authorFilter, searchQuery]);
const columns = useMemo(() => [
{ key: 'title', label: 'Title', sortable: true },
{ key: 'author', label: 'Author', sortable: true },
{ key: 'status', label: 'Status', format: 'status' as const, sortable: true },
{ key: 'site', label: 'Site', sortable: true },
{ key: 'publishedAt', label: 'Published', format: 'date' as const, sortable: true },
{ key: 'updatedAt', label: 'Updated', format: 'date' as const, sortable: true },
], []);
const rows = filteredPosts.map((p, i) => ({
id: p.id || String(i),
title: p.title || 'Untitled',
author: p.author || '—',
status: p.status || 'draft',
site: p.site || '—',
publishedAt: p.publishedAt || '—',
updatedAt: p.updatedAt || '—',
}));
const publishedCount = posts.filter(p => p.status === 'published').length;
const draftCount = posts.filter(p => p.status === 'draft').length;
return (
<>
<PageHeader
title="Blog Manager"
subtitle={`${posts.length} post${posts.length !== 1 ? 's' : ''}`}
stats={[
{ label: 'Published', value: String(publishedCount) },
{ label: 'Drafts', value: String(draftCount) },
{ label: 'Sites', value: String(sites.length || uniqueSites.length) },
]}
/>
<div className="search-bar" style={{ marginBottom: 12 }}>
<input
type="text"
className="search-input"
placeholder="Search posts..."
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
/>
</div>
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 12 }}>
{uniqueStatuses.length > 0 && (
<div className="filter-chips">
{uniqueStatuses.map(s => (
<button
key={s}
className={`chip ${statusFilter.has(s) ? 'chip-active' : ''}`}
onClick={() => {
setStatusFilter(prev => {
const next = new Set(prev);
if (next.has(s)) next.delete(s); else next.add(s);
return next;
});
}}
>
{s.charAt(0).toUpperCase() + s.slice(1)}
</button>
))}
</div>
)}
{uniqueSites.length > 1 && (
<div className="filter-chips">
{uniqueSites.map(s => (
<button
key={s}
className={`chip ${siteFilter.has(s) ? 'chip-active' : ''}`}
onClick={() => {
setSiteFilter(prev => {
const next = new Set(prev);
if (next.has(s)) next.delete(s); else next.add(s);
return next;
});
}}
>
{s}
</button>
))}
</div>
)}
{uniqueAuthors.length > 1 && (
<div className="filter-chips">
{uniqueAuthors.map(a => (
<button
key={a}
className={`chip ${authorFilter.has(a) ? 'chip-active' : ''}`}
onClick={() => {
setAuthorFilter(prev => {
const next = new Set(prev);
if (next.has(a)) next.delete(a); else next.add(a);
return next;
});
}}
>
{a}
</button>
))}
</div>
)}
</div>
<DataTable
columns={columns}
rows={rows}
pageSize={10}
emptyMessage="No blog posts found"
/>
{/* Blog post actions */}
{filteredPosts.length > 0 && (
<div style={{ marginTop: 12 }}>
{actionResult && (
<div style={{ color: actionResult.type === 'success' ? '#059669' : '#dc2626', fontSize: 13, marginBottom: 8 }}>
{actionResult.msg}
</div>
)}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
<span style={{ fontSize: 12, color: '#6b7280', lineHeight: '28px' }}>Actions:</span>
{filteredPosts.slice(0, 5).map((p, i) => (
<span key={p.id || i} style={{ display: 'inline-flex', gap: 2 }}>
{p.status === 'draft' && (
<button
className="chip"
onClick={() => handleBlogAction('publish_blog_post', { postId: p.id, title: p.title })}
disabled={isActing}
title={`Publish "${p.title}"`}
style={{ color: '#059669' }}
>
📢 Publish {(p.title || '').length > 15 ? (p.title || '').slice(0, 15) + '…' : p.title}
</button>
)}
<button
className="chip"
onClick={() => handleBlogAction('delete_blog_post', { postId: p.id, title: p.title })}
disabled={isActing}
title={`Delete "${p.title}"`}
style={{ color: '#dc2626' }}
>
🗑 {(p.title || '').length > 15 ? (p.title || '').slice(0, 15) + '…' : p.title}
</button>
</span>
))}
</div>
</div>
)}
</>
);
}

View File

@ -0,0 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /></head>
<body><div id="root"></div><script type="module" src="./main.tsx"></script></body>
</html>

View File

@ -0,0 +1,9 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App.js';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
);

View File

@ -0,0 +1,22 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { viteSingleFile } from 'vite-plugin-singlefile';
import path from 'path';
export default defineConfig({
plugins: [react(), viteSingleFile()],
root: __dirname,
build: {
outDir: path.resolve(__dirname, '../../../../dist/apps/blog-manager'),
emptyOutDir: false,
rollupOptions: { input: path.resolve(__dirname, 'index.html') },
},
resolve: {
alias: {
'@components': path.resolve(__dirname, '../../components'),
'@hooks': path.resolve(__dirname, '../../hooks'),
'@styles': path.resolve(__dirname, '../../styles'),
'@context': path.resolve(__dirname, '../../context'),
},
},
});

View File

@ -0,0 +1,91 @@
import React, { useState, useMemo } from 'react';
import { useApp } from '@modelcontextprotocol/ext-apps/react';
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
import { PageHeader } from '../../components/layout/PageHeader';
import { DataTable } from '../../components/data/DataTable';
import type { TableColumn, TableRow } from '../../types';
import '../../styles/base.css';
function extractData(result: CallToolResult): any {
const sc = (result as any).structuredContent;
if (sc) return sc;
for (const item of result.content || []) {
if (item.type === 'text') {
try { return JSON.parse(item.text); } catch {}
}
}
return null;
}
const COLUMNS: TableColumn[] = [
{ key: 'name', label: 'Name', sortable: true },
{ key: 'type', label: 'Type', sortable: true },
{ key: 'description', label: 'Description' },
{ key: 'availability', label: 'Availability', sortable: true, format: 'status' },
];
export function App() {
const [data, setData] = useState<any>((window as any).__MCP_APP_DATA__ || null);
const { isConnected, error } = useApp({
appInfo: { name: 'Calendar Resources', version: '1.0.0' },
capabilities: {},
onAppCreated: (app) => {
app.ontoolresult = async (result) => {
const d = extractData(result);
if (d) setData(d);
};
},
});
const resources: any[] = data?.resources || [];
const rows: TableRow[] = useMemo(() => {
return resources.map((r) => ({
id: r.id || r.resourceId || '',
name: r.name || r.title || 'Unnamed',
type: r.type || r.resourceType || r.category || '—',
description: r.description || r.details || '—',
availability: r.isAvailable !== undefined
? (r.isAvailable ? 'Available' : 'Unavailable')
: r.availability || r.status || 'Unknown',
}));
}, [resources]);
// Stats
const available = rows.filter((r) => String(r.availability).toLowerCase() === 'available').length;
const unavailable = rows.filter((r) => String(r.availability).toLowerCase() === 'unavailable').length;
if (error) {
return <div className="error-state"><h3>Error</h3><p>{error.message}</p></div>;
}
if (!isConnected && !data) {
return <div className="loading-state"><div className="loading-spinner" /><p>Connecting...</p></div>;
}
if (!data) {
return <div className="loading-state"><div className="loading-spinner" /><p>Waiting for resource data...</p></div>;
}
return (
<div>
<PageHeader
title="Calendar Resources"
subtitle="Rooms, equipment, and shared resources"
stats={[
{ label: 'Total', value: String(rows.length) },
{ label: 'Available', value: String(available) },
{ label: 'Unavailable', value: String(unavailable) },
]}
/>
<div style={{ marginTop: 16 }}>
<DataTable
columns={COLUMNS}
rows={rows}
emptyMessage="No resources found"
pageSize={15}
/>
</div>
</div>
);
}

View File

@ -0,0 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /></head>
<body><div id="root"></div><script type="module" src="./main.tsx"></script></body>
</html>

View File

@ -0,0 +1,9 @@
import React, { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App.js';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
);

View File

@ -0,0 +1,22 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { viteSingleFile } from 'vite-plugin-singlefile';
import path from 'path';
export default defineConfig({
plugins: [react(), viteSingleFile()],
root: __dirname,
build: {
outDir: path.resolve(__dirname, '../../../../dist/apps/calendar-resources'),
emptyOutDir: false,
rollupOptions: { input: path.resolve(__dirname, 'index.html') },
},
resolve: {
alias: {
'@components': path.resolve(__dirname, '../../components'),
'@hooks': path.resolve(__dirname, '../../hooks'),
'@styles': path.resolve(__dirname, '../../styles'),
'@context': path.resolve(__dirname, '../../context'),
},
},
});

View File

@ -0,0 +1,208 @@
import React, { useState, useMemo } from 'react';
import { useApp } from '@modelcontextprotocol/ext-apps/react';
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
import { PageHeader } from '../../components/layout/PageHeader';
import { Card } from '../../components/layout/Card';
import { CalendarView } from '../../components/viz/CalendarView';
import type { CalendarEvent } from '../../types';
import '../../styles/base.css';
import '../../styles/interactive.css';
function extractData(result: CallToolResult): any {
const sc = (result as any).structuredContent;
if (sc) return sc;
for (const item of result.content || []) {
if (item.type === 'text') {
try { return JSON.parse(item.text); } catch {}
}
}
return null;
}
const EVENT_TYPE_COLORS: Record<string, string> = {
meeting: '#4f46e5',
call: '#059669',
task: '#d97706',
deadline: '#dc2626',
event: '#7c3aed',
appointment: '#0891b2',
reminder: '#ec4899',
};
const ALL_TYPES = Object.keys(EVENT_TYPE_COLORS);
function formatDate(d?: string): string {
if (!d) return '\u2014';
try {
return new Date(d).toLocaleDateString('en-US', {
month: 'short', day: 'numeric', year: 'numeric',
});
} catch { return d; }
}
export function App() {
const [data, setData] = useState<any>((window as any).__MCP_APP_DATA__ || null);
const [activeFilters, setActiveFilters] = useState<Set<string>>(new Set());
const [selectedDate, setSelectedDate] = useState<string | null>(null);
const { isConnected, error } = useApp({
appInfo: { name: 'Calendar View', version: '1.0.0' },
capabilities: {},
onAppCreated: (app) => {
app.ontoolresult = async (result) => {
const d = extractData(result);
if (d) setData(d);
};
},
});
const calendar = data?.calendar;
const events: any[] = data?.events || [];
// Map events to CalendarEvent format with color coding
const calendarEvents: CalendarEvent[] = useMemo(() => {
return events.map((evt) => {
const eventType = (evt.type || evt.appointmentStatus || 'event').toLowerCase();
return {
title: evt.title || evt.name || evt.subject || 'Untitled',
date: evt.startTime || evt.date || evt.dateAdded || '',
time: evt.startTime
? new Date(evt.startTime).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })
: evt.time || '',
type: eventType,
color: EVENT_TYPE_COLORS[eventType] || '#4f46e5',
};
});
}, [events]);
// Filter events by active types
const filteredEvents = useMemo(() => {
if (activeFilters.size === 0) return calendarEvents;
return calendarEvents.filter((e) => activeFilters.has(e.type || 'event'));
}, [calendarEvents, activeFilters]);
// Determine present event types
const presentTypes = useMemo(() => {
const types = new Set<string>();
for (const e of calendarEvents) {
types.add(e.type || 'event');
}
return Array.from(types);
}, [calendarEvents]);
// Events for selected date
const selectedDateEvents = useMemo(() => {
if (!selectedDate) return [];
return filteredEvents.filter((e) => {
try {
const eDate = new Date(e.date).toISOString().split('T')[0];
return eDate === selectedDate;
} catch {
return false;
}
});
}, [filteredEvents, selectedDate]);
const toggleFilter = (type: string) => {
setActiveFilters((prev) => {
const next = new Set(prev);
if (next.has(type)) next.delete(type);
else next.add(type);
return next;
});
};
if (error) {
return <div className="error-state"><h3>Error</h3><p>{error.message}</p></div>;
}
if (!isConnected && !data) {
return <div className="loading-state"><div className="loading-spinner" /><p>Connecting...</p></div>;
}
if (!data) {
return <div className="loading-state"><div className="loading-spinner" /><p>Waiting for calendar data...</p></div>;
}
return (
<div>
<PageHeader
title={calendar?.name || 'Calendar'}
subtitle={calendar?.description || `${events.length} events`}
stats={[
{ label: 'Events', value: String(filteredEvents.length) },
...(data.startDate ? [{ label: 'From', value: formatDate(data.startDate) }] : []),
...(data.endDate ? [{ label: 'To', value: formatDate(data.endDate) }] : []),
]}
/>
{/* Filter chips */}
{presentTypes.length > 1 && (
<div style={{ margin: '8px 0 12px' }}>
<div className="filter-chips">
{presentTypes.map((type) => (
<button
key={type}
className={`chip ${activeFilters.size === 0 || activeFilters.has(type) ? 'chip-active' : ''}`}
onClick={() => toggleFilter(type)}
style={{
borderColor: EVENT_TYPE_COLORS[type] || '#4f46e5',
...(activeFilters.size === 0 || activeFilters.has(type)
? { background: EVENT_TYPE_COLORS[type] || '#4f46e5', color: '#fff' }
: {}),
}}
>
{type.charAt(0).toUpperCase() + type.slice(1)}
</button>
))}
</div>
</div>
)}
<Card padding="sm">
<CalendarView
events={filteredEvents}
highlightToday
onDateClick={(date) => setSelectedDate(date === selectedDate ? null : date)}
/>
</Card>
{/* Selected date detail */}
{selectedDate && (
<Card title={`Events on ${selectedDate}`} padding="sm">
{selectedDateEvents.length === 0 ? (
<div className="empty-state"><div className="empty-icon">📅</div><p>No events on this date</p></div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{selectedDateEvents.map((evt, i) => (
<div
key={i}
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '8px 12px',
background: '#f9fafb',
borderRadius: 6,
borderLeft: `3px solid ${evt.color || '#4f46e5'}`,
}}
>
<span style={{ fontWeight: 500, fontSize: 13 }}>{evt.time || '—'}</span>
<span style={{ fontSize: 13 }}>{evt.title}</span>
<span
style={{
marginLeft: 'auto',
fontSize: 11,
color: evt.color || '#6b7280',
textTransform: 'capitalize',
}}
>
{evt.type}
</span>
</div>
))}
</div>
)}
</Card>
)}
</div>
);
}

View File

@ -0,0 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /></head>
<body><div id="root"></div><script type="module" src="./main.tsx"></script></body>
</html>

View File

@ -0,0 +1,9 @@
import React, { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App.js';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
);

View File

@ -0,0 +1,22 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { viteSingleFile } from 'vite-plugin-singlefile';
import path from 'path';
export default defineConfig({
plugins: [react(), viteSingleFile()],
root: __dirname,
build: {
outDir: path.resolve(__dirname, '../../../../dist/apps/calendar-view'),
emptyOutDir: false,
rollupOptions: { input: path.resolve(__dirname, 'index.html') },
},
resolve: {
alias: {
'@components': path.resolve(__dirname, '../../components'),
'@hooks': path.resolve(__dirname, '../../hooks'),
'@styles': path.resolve(__dirname, '../../styles'),
'@context': path.resolve(__dirname, '../../context'),
},
},
});

View File

@ -0,0 +1,141 @@
import React, { useState } from 'react';
import { useApp } from '@modelcontextprotocol/ext-apps/react';
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
import { DetailHeader } from '../../components/data/DetailHeader';
import { KeyValueList } from '../../components/data/KeyValueList';
import { Card } from '../../components/layout/Card';
import { AudioPlayer } from '../../components/data/AudioPlayer';
import { TranscriptView } from '../../components/comms/TranscriptView';
import '../../styles/base.css';
import '../../styles/interactive.css';
function extractData(result: CallToolResult): any {
const sc = (result as any).structuredContent;
if (sc) return sc;
for (const item of result.content || []) {
if (item.type === 'text') {
try { return JSON.parse(item.text); } catch {}
}
}
return null;
}
function formatDuration(seconds: number | string | undefined): string {
if (!seconds) return '0:00';
const s = typeof seconds === 'string' ? parseInt(seconds, 10) : seconds;
if (isNaN(s)) return '0:00';
const m = Math.floor(s / 60);
const sec = s % 60;
return `${m}:${sec.toString().padStart(2, '0')}`;
}
function formatDate(d: string | undefined): string {
if (!d) return '-';
try {
return new Date(d).toLocaleDateString('en-US', {
weekday: 'short', month: 'short', day: 'numeric', year: 'numeric',
hour: 'numeric', minute: '2-digit',
});
} catch { return d; }
}
const statusVariantMap: Record<string, string> = {
completed: 'complete',
missed: 'error',
voicemail: 'pending',
busy: 'paused',
'no-answer': 'draft',
failed: 'error',
active: 'active',
};
export function App() {
const [data, setData] = useState<any>((window as any).__MCP_APP_DATA__ || null);
const { isConnected, error } = useApp({
appInfo: { name: 'Call Detail', version: '1.0.0' },
capabilities: {},
onAppCreated: (app) => {
app.ontoolresult = async (result) => {
const parsed = extractData(result);
if (parsed) setData(parsed);
};
},
});
if (error) return <div className="error-state"><h3>Error</h3><p>{error.message}</p></div>;
if (!isConnected) return <div className="loading-state"><div className="loading-spinner" /><p>Connecting...</p></div>;
if (!data) return <div className="loading-state"><div className="loading-spinner" /><p>Waiting for data...</p></div>;
const call = data?.call || data || {};
const contactName = call.contactName || call.contact?.name || 'Unknown Contact';
const status = (call.status || 'completed').toLowerCase();
const direction = (call.direction || 'inbound').toLowerCase();
const variant = statusVariantMap[status] || 'draft';
const metadataItems = [
{ label: 'Direction', value: direction.charAt(0).toUpperCase() + direction.slice(1), bold: true },
{ label: 'From', value: call.from || '-' },
{ label: 'To', value: call.to || '-' },
{ label: 'Duration', value: formatDuration(call.duration), bold: true },
{ label: 'Date', value: formatDate(call.date || call.createdAt || call.startedAt) },
{ label: 'Status', value: status.charAt(0).toUpperCase() + status.slice(1) },
];
if (call.source) metadataItems.push({ label: 'Source', value: call.source, bold: false });
if (call.assignedTo) metadataItems.push({ label: 'Assigned To', value: call.assignedTo, bold: false });
const hasRecording = !!(call.recordingUrl || call.recording);
const transcript = call.transcript || call.transcription || [];
const transcriptEntries = Array.isArray(transcript) ? transcript : [];
return (
<div>
<DetailHeader
title={contactName}
subtitle={call.phone || call.from || call.to || ''}
entityId={call.id ? `Call #${call.id}` : undefined}
status={status.charAt(0).toUpperCase() + status.slice(1)}
statusVariant={variant as any}
/>
<div style={{ display: 'grid', gap: 16, marginTop: 16 }}>
<Card title="Call Details">
<KeyValueList items={metadataItems} />
</Card>
{hasRecording && (
<Card title="Recording">
<AudioPlayer
title={`Call with ${contactName}`}
duration={formatDuration(call.duration)}
type="recording"
/>
</Card>
)}
{transcriptEntries.length > 0 ? (
<Card title="Transcript">
<TranscriptView
entries={transcriptEntries.map((e: any) => ({
speaker: e.speaker || e.speakerName || 'Unknown',
speakerRole: e.speakerRole || e.role || 'customer',
text: e.text || e.content || '',
timestamp: e.timestamp || e.time || '',
}))}
title={`Call Transcript`}
duration={formatDuration(call.duration)}
/>
</Card>
) : (
<Card title="Transcript">
<div className="empty-state">
<div className="empty-icon">📝</div>
<p>No transcript available for this call</p>
</div>
</Card>
)}
</div>
</div>
);
}

Some files were not shown because too many files have changed in this diff Show More