diff --git a/servers/keap/.env.example b/servers/keap/.env.example new file mode 100644 index 0000000..43eaccc --- /dev/null +++ b/servers/keap/.env.example @@ -0,0 +1,2 @@ +KEAP_API_KEY=your_api_key_here +KEAP_ACCESS_TOKEN=your_oauth_access_token_here diff --git a/servers/keap/README.md b/servers/keap/README.md new file mode 100644 index 0000000..7011d36 --- /dev/null +++ b/servers/keap/README.md @@ -0,0 +1,471 @@ +# Keap MCP Server + +A comprehensive Model Context Protocol (MCP) server for Keap (formerly Infusionsoft) - providing AI-powered access to contact management, sales pipeline, marketing automation, e-commerce, and more. + +## ๐Ÿš€ Features + +- **111 MCP Tools** across 14 categories +- **20 React Apps** with dark theme UI matching Keap brand +- Full OAuth 2.0 PKCE authentication flow +- Type-safe API client with comprehensive error handling +- Real-time data synchronization +- Webhook support for automation + +## ๐Ÿ“ฆ Installation + +```bash +npm install @mcpengine/keap +``` + +## ๐Ÿ”ง Configuration + +Create a `.env` file: + +```env +KEAP_CLIENT_ID=your_client_id +KEAP_CLIENT_SECRET=your_client_secret +KEAP_REDIRECT_URI=http://localhost:3000/callback +KEAP_ACCESS_TOKEN=your_access_token +KEAP_REFRESH_TOKEN=your_refresh_token +``` + +## ๐Ÿ› ๏ธ MCP Tools (111 Total) + +### Contacts (19 tools) +Core contact management and CRM functionality. + +- `keap_create_contact` - Create a new contact with email, name, phone, address, tags, custom fields +- `keap_get_contact` - Retrieve a contact by ID with all details +- `keap_update_contact` - Update an existing contact +- `keap_delete_contact` - Delete a contact permanently +- `keap_list_contacts` - List contacts with pagination and filtering +- `keap_search_contacts` - Search contacts by email, name, phone +- `keap_merge_contacts` - Merge duplicate contacts +- `keap_apply_tag_to_contact` - Apply a tag to a contact +- `keap_remove_tag_from_contact` - Remove a tag from a contact +- `keap_get_contact_tags` - Get all tags applied to a contact +- `keap_get_contact_emails` - Get email addresses for a contact +- `keap_create_contact_email` - Add a new email address to a contact +- `keap_delete_contact_email` - Remove an email address from a contact +- `keap_get_contact_credit_cards` - Get saved credit cards for a contact +- `keap_create_contact_credit_card` - Add a credit card to a contact +- `keap_get_contact_custom_fields` - Get custom field values for a contact +- `keap_update_contact_custom_field` - Update a custom field value +- `keap_list_contact_notes` - List all notes for a contact +- `keap_get_contact_model` - Get the contact data model schema + +### E-Commerce (15 tools) +Product catalog, orders, transactions, and subscriptions. + +- `keap_create_product` - Create a new product in the catalog +- `keap_get_product` - Get product details by ID +- `keap_update_product` - Update product information +- `keap_delete_product` - Delete a product +- `keap_list_products` - List all products with filtering +- `keap_create_order` - Create a new order for a contact +- `keap_get_order` - Get order details by ID +- `keap_delete_order` - Delete an order +- `keap_list_orders` - List orders with filtering +- `keap_list_order_transactions` - Get all transactions for an order +- `keap_get_transaction` - Get transaction details +- `keap_list_transactions` - List all transactions +- `keap_create_subscription` - Create a recurring subscription +- `keap_get_subscription` - Get subscription details +- `keap_list_subscriptions` - List all subscriptions + +### Opportunities (9 tools) +Sales pipeline and opportunity management. + +- `keap_create_opportunity` - Create a new sales opportunity +- `keap_get_opportunity` - Get opportunity details +- `keap_update_opportunity` - Update opportunity information +- `keap_delete_opportunity` - Delete an opportunity +- `keap_list_opportunities` - List opportunities with filtering +- `keap_list_opportunity_stage_pipeline` - Get all pipeline stages +- `keap_get_opportunity_stage_pipeline` - Get details for a specific stage +- `keap_update_opportunity_stage` - Move opportunity to different stage +- `keap_get_opportunity_model` - Get opportunity data model schema + +### Affiliates (9 tools) +Affiliate program management and tracking. + +- `keap_create_affiliate` - Create a new affiliate account +- `keap_get_affiliate` - Get affiliate details +- `keap_list_affiliates` - List all affiliates +- `keap_get_affiliate_clawbacks` - Get commission clawbacks for an affiliate +- `keap_get_affiliate_commissions` - Get commissions earned +- `keap_get_affiliate_payments` - Get payment history +- `keap_get_affiliate_redirect_links` - Get tracking links +- `keap_get_affiliate_summary` - Get affiliate performance summary +- `keap_list_commissions` - List all commissions + +### Tasks (8 tools) +Task and activity management. + +- `keap_create_task` - Create a new task +- `keap_get_task` - Get task details +- `keap_update_task` - Update task information +- `keap_delete_task` - Delete a task +- `keap_list_tasks` - List tasks with filtering +- `keap_search_tasks` - Search tasks by title or description +- `keap_complete_task` - Mark a task as completed +- `keap_get_task_model` - Get task data model schema + +### Campaigns (7 tools) +Marketing campaign and sequence management. + +- `keap_list_campaigns` - List all campaigns +- `keap_get_campaign` - Get campaign details +- `keap_add_contact_to_campaign` - Add a contact to a campaign +- `keap_remove_contact_from_campaign` - Remove a contact from a campaign +- `keap_get_campaign_sequences` - Get all sequences in a campaign +- `keap_add_contact_to_sequence` - Add contact to a specific sequence +- `keap_remove_contact_from_sequence` - Remove contact from sequence + +### Emails (7 tools) +Email sending, templates, and opt-in management. + +- `keap_send_email` - Send an email to contacts +- `keap_get_email` - Get email details +- `keap_list_emails` - List sent emails +- `keap_create_email_template` - Create a new email template +- `keap_list_email_templates` - List all email templates +- `keap_opt_in_contact` - Opt-in a contact for email marketing +- `keap_opt_out_contact` - Opt-out a contact from emails + +### Appointments (6 tools) +Appointment scheduling and calendar management. + +- `keap_create_appointment` - Create a new appointment +- `keap_get_appointment` - Get appointment details +- `keap_update_appointment` - Update appointment information +- `keap_delete_appointment` - Delete an appointment +- `keap_list_appointments` - List appointments with filtering +- `keap_get_appointment_model` - Get appointment data model schema + +### Automations (6 tools) +Webhook and automation configuration. + +- `keap_create_hook` - Create a new webhook +- `keap_list_hooks` - List all webhooks +- `keap_delete_hook` - Delete a webhook +- `keap_verify_hook` - Verify webhook configuration +- `keap_update_hook` - Update webhook settings +- `keap_list_hook_event_types` - Get available webhook event types + +### Notes (6 tools) +Contact and opportunity notes. + +- `keap_create_note` - Create a new note +- `keap_get_note` - Get note details +- `keap_update_note` - Update note content +- `keap_delete_note` - Delete a note +- `keap_list_notes` - List notes with filtering +- `keap_get_note_model` - Get note data model schema + +### Companies (5 tools) +Company/organization management. + +- `keap_create_company` - Create a new company +- `keap_get_company` - Get company details +- `keap_update_company` - Update company information +- `keap_list_companies` - List companies with filtering +- `keap_get_company_contacts` - Get all contacts for a company + +### Settings (5 tools) +Account settings and configuration. + +- `keap_get_account_profile` - Get account profile information +- `keap_update_account_profile` - Update account settings +- `keap_list_users` - List all users in the account +- `keap_get_application_configuration` - Get app configuration +- `keap_list_custom_fields` - List all custom field definitions + +### Tags (5 tools) +Tag and category management. + +- `keap_create_tag` - Create a new tag +- `keap_get_tag` - Get tag details +- `keap_list_tags` - List all tags +- `keap_create_tag_category` - Create a tag category +- `keap_list_tag_categories` - List all tag categories + +### Files (4 tools) +File upload and management. + +- `keap_upload_file` - Upload a file +- `keap_get_file` - Get file details and download URL +- `keap_delete_file` - Delete a file +- `keap_list_files` - List all uploaded files + +## ๐ŸŽจ React Apps (20 Total) + +All apps feature dark theme styling with Keap's orange/amber brand accents and VSCode-style UI. + +### Core Apps + +1. **Contact Dashboard** (`src/ui/react-app/src/apps/contact-dashboard/`) + - Overview of contact metrics and recent contacts + - Quick search and filter functionality + - Contact creation and editing + +2. **Contact Detail** (`src/ui/react-app/src/apps/contact-detail/`) + - Full contact profile with all fields + - Tag management + - Email and phone history + - Custom field editor + +3. **Contact Grid** (`src/ui/react-app/src/apps/contact-grid/`) + - Sortable, filterable contact list + - Bulk actions (tag, delete, export) + - Advanced search + +4. **Deal Detail** (`src/ui/react-app/src/apps/deal-detail/`) + - Opportunity/deal full details + - Stage progression tracking + - Activity timeline + +5. **Pipeline Kanban** (`src/ui/react-app/src/apps/pipeline-kanban/`) + - Drag-and-drop deal pipeline + - Stage-based organization + - Deal value summaries per stage + +### Campaign & Marketing + +6. **Campaign Dashboard** (`src/ui/react-app/src/apps/campaign-dashboard/`) + - Campaign performance metrics + - Active campaigns list + - Contact enrollment stats + +7. **Campaign Detail** (`src/ui/react-app/src/apps/campaign-detail/`) + - Campaign sequence viewer + - Contact progress tracking + - Enrollment/removal management + +8. **Email Composer** (`src/ui/react-app/src/apps/email-composer/`) + - Rich text email editor + - Template selector + - Merge field insertion + - Send to contacts or lists + +9. **Automation Builder** (`src/ui/react-app/src/apps/automation-builder/`) + - Webhook management + - Event type configuration + - Automation triggers + +### Calendar & Tasks + +10. **Appointment Calendar** (`src/ui/react-app/src/apps/appointment-calendar/`) + - Calendar view of appointments + - Create/edit appointments + - Contact association + +11. **Task Manager** (`src/ui/react-app/src/apps/task-manager/`) + - Task list with filtering + - Task creation and assignment + - Completion tracking + +### E-Commerce + +12. **Order Dashboard** (`src/ui/react-app/src/apps/order-dashboard/`) + - Order overview and metrics + - Recent orders list + - Revenue summaries + +13. **Order Detail** (`src/ui/react-app/src/apps/order-detail/`) + - Full order information + - Transaction history + - Payment status + +14. **Product Catalog** (`src/ui/react-app/src/apps/product-catalog/`) + - Product list with images + - Product creation and editing + - Pricing and inventory management + +15. **Subscription Manager** (`src/ui/react-app/src/apps/subscription-manager/`) + - Active subscriptions list + - Subscription creation + - Billing cycle tracking + +### Organization & Settings + +16. **Tag Manager** (`src/ui/react-app/src/apps/tag-manager/`) + - Tag list and creation + - Category management + - Tag application stats + +17. **Analytics Dashboard** (`src/ui/react-app/src/apps/analytics-dashboard/`) + - Business metrics overview + - Contact growth charts + - Revenue analytics + +18. **Affiliate Dashboard** (`src/ui/react-app/src/apps/affiliate-dashboard/`) + - Affiliate performance + - Commission tracking + - Payment history + +19. **File Browser** (`src/ui/react-app/src/apps/file-browser/`) + - Uploaded files list + - File upload interface + - Download and delete actions + +20. **Settings Panel** (`src/ui/react-app/src/apps/settings-panel/`) + - Account profile + - User management + - Custom field configuration + +## ๐Ÿ—๏ธ Architecture + +``` +servers/keap/ +โ”œโ”€โ”€ src/ +โ”‚ โ”œโ”€โ”€ clients/ +โ”‚ โ”‚ โ””โ”€โ”€ keap.ts # OAuth 2.0 client with auto-refresh +โ”‚ โ”œโ”€โ”€ tools/ # 14 tool files, 111 tools total +โ”‚ โ”‚ โ”œโ”€โ”€ affiliates-tools.ts +โ”‚ โ”‚ โ”œโ”€โ”€ appointments-tools.ts +โ”‚ โ”‚ โ”œโ”€โ”€ automations-tools.ts +โ”‚ โ”‚ โ”œโ”€โ”€ campaigns-tools.ts +โ”‚ โ”‚ โ”œโ”€โ”€ companies-tools.ts +โ”‚ โ”‚ โ”œโ”€โ”€ contacts-tools.ts +โ”‚ โ”‚ โ”œโ”€โ”€ ecommerce-tools.ts +โ”‚ โ”‚ โ”œโ”€โ”€ emails-tools.ts +โ”‚ โ”‚ โ”œโ”€โ”€ files-tools.ts +โ”‚ โ”‚ โ”œโ”€โ”€ notes-tools.ts +โ”‚ โ”‚ โ”œโ”€โ”€ opportunities-tools.ts +โ”‚ โ”‚ โ”œโ”€โ”€ settings-tools.ts +โ”‚ โ”‚ โ”œโ”€โ”€ tags-tools.ts +โ”‚ โ”‚ โ””โ”€โ”€ tasks-tools.ts +โ”‚ โ”œโ”€โ”€ types/ +โ”‚ โ”‚ โ”œโ”€โ”€ keap.ts # Type definitions +โ”‚ โ”‚ โ””โ”€โ”€ index.ts +โ”‚ โ”œโ”€โ”€ ui/ +โ”‚ โ”‚ โ””โ”€โ”€ react-app/ +โ”‚ โ”‚ โ”œโ”€โ”€ src/ +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ apps/ # 20 React applications +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ hooks/ # Shared React hooks +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ styles/ # Dark theme CSS +โ”‚ โ”‚ โ”œโ”€โ”€ package.json +โ”‚ โ”‚ โ”œโ”€โ”€ tsconfig.json +โ”‚ โ”‚ โ””โ”€โ”€ build-all.js +โ”‚ โ”œโ”€โ”€ server.ts # MCP server implementation +โ”‚ โ””โ”€โ”€ main.ts # Entry point +โ”œโ”€โ”€ dist/ # Compiled output +โ”œโ”€โ”€ package.json +โ”œโ”€โ”€ tsconfig.json +โ””โ”€โ”€ README.md +``` + +## ๐ŸŽฏ Usage + +### As MCP Server + +Add to your MCP client configuration: + +```json +{ + "mcpServers": { + "keap": { + "command": "npx", + "args": ["-y", "@mcpengine/keap"], + "env": { + "KEAP_CLIENT_ID": "your_client_id", + "KEAP_CLIENT_SECRET": "your_client_secret", + "KEAP_ACCESS_TOKEN": "your_access_token", + "KEAP_REFRESH_TOKEN": "your_refresh_token" + } + } + } +} +``` + +### Building React Apps + +```bash +cd src/ui/react-app +npm install +npm run build +``` + +Individual app builds are output to `dist/{app-name}/`. + +## ๐Ÿ” Authentication + +The server uses OAuth 2.0 PKCE flow: + +1. Set `KEAP_CLIENT_ID` and `KEAP_CLIENT_SECRET` in `.env` +2. Run the OAuth flow to get initial tokens +3. Tokens are auto-refreshed when expired +4. Refresh token is persisted for long-term access + +## ๐Ÿ“Š Data Models + +All tools that create or update resources include a corresponding `get_*_model` tool that returns the schema, validation rules, and field definitions. + +Example: +```typescript +// Get contact model to understand required fields +const model = await callTool('keap_get_contact_model'); + +// Create contact with proper fields +const contact = await callTool('keap_create_contact', { + given_name: 'John', + family_name: 'Doe', + email: 'john@example.com', + opt_in_reason: 'Website signup' +}); +``` + +## ๐Ÿšฆ Rate Limits + +Keap API has rate limits: +- 150 requests per second per account +- Daily request quotas based on plan + +The client includes automatic retry with exponential backoff for rate limit errors. + +## ๐Ÿงช Testing + +```bash +npm test +``` + +## ๐Ÿ“ Development + +```bash +# Install dependencies +npm install + +# Run in development mode +npm run dev + +# Build TypeScript +npm run build + +# Type check +npx tsc --noEmit +``` + +## ๐Ÿค Contributing + +Contributions welcome! Please ensure: +- All TypeScript code type-checks without errors +- React apps maintain dark theme consistency +- New tools include proper input schemas and descriptions +- README is updated for new functionality + +## ๐Ÿ“„ License + +MIT + +## ๐Ÿ”— Links + +- [Keap API Documentation](https://developer.keap.com/docs/) +- [MCP Protocol Specification](https://modelcontextprotocol.io/) +- [MCPEngine GitHub](https://github.com/BusyBee3333/mcpengine) + +--- + +**Built with โค๏ธ by MCPEngine** + +*Keap is a registered trademark of Keap Inc. This is an unofficial community project.* diff --git a/servers/keap/generate-apps.js b/servers/keap/generate-apps.js new file mode 100644 index 0000000..6b26d17 --- /dev/null +++ b/servers/keap/generate-apps.js @@ -0,0 +1,192 @@ +#!/usr/bin/env node +import { writeFileSync, mkdirSync } from 'fs'; +import { join } from 'path'; + +const apps = [ + { name: 'contact-detail', title: 'Contact Detail', tool: 'keap_get_contact' }, + { name: 'contact-grid', title: 'Contact Grid', tool: 'keap_list_contacts' }, + { name: 'deal-detail', title: 'Deal Detail', tool: 'keap_get_opportunity' }, + { name: 'campaign-dashboard', title: 'Campaign Dashboard', tool: 'keap_list_campaigns' }, + { name: 'campaign-detail', title: 'Campaign Detail', tool: 'keap_get_campaign' }, + { name: 'order-dashboard', title: 'Order Dashboard', tool: 'keap_list_orders' }, + { name: 'order-detail', title: 'Order Detail', tool: 'keap_get_order' }, + { name: 'appointment-calendar', title: 'Appointment Calendar', tool: 'keap_list_appointments' }, + { name: 'tag-manager', title: 'Tag Manager', tool: 'keap_list_tags' }, + { name: 'email-composer', title: 'Email Composer', tool: 'keap_send_email' }, + { name: 'automation-builder', title: 'Automation Builder', tool: 'keap_list_hooks' }, + { name: 'affiliate-dashboard', title: 'Affiliate Dashboard', tool: 'keap_list_affiliates' }, + { name: 'product-catalog', title: 'Product Catalog', tool: 'keap_list_products' }, + { name: 'subscription-manager', title: 'Subscription Manager', tool: 'keap_list_subscriptions' }, + { name: 'file-browser', title: 'File Browser', tool: 'keap_list_files' }, + { name: 'settings-panel', title: 'Settings Panel', tool: 'keap_get_account_profile' }, + { name: 'analytics-dashboard', title: 'Analytics Dashboard', tool: 'keap_list_contacts' }, +]; + +const baseDir = './src/ui/react-app/src/apps'; + +apps.forEach(app => { + const appDir = join(baseDir, app.name); + + try { + mkdirSync(appDir, { recursive: true }); + } catch (e) {} + + // App.tsx + const appTsx = `import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import '../../styles/global.css'; + +export default function ${app.name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('')}() { + const { callTool, loading, error } = useCallTool(); + const [data, setData] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + const result = await callTool('${app.tool}', {}); + setData(result); + } catch (err) { + console.error('Failed to load data:', err); + } + }; + + return ( +
+

${app.title}

+ + {error &&
{error}
} + + {loading ? ( +
Loading...
+ ) : ( +
+
+            {JSON.stringify(data, null, 2)}
+          
+
+ )} +
+ ); +} +`; + + // index.html + const indexHtml = ` + + + + + ${app.title} - Keap + + +
+ + + +`; + + // main.tsx + const mainTsx = `import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); +`; + + // vite.config.ts + const viteConfig = `import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/${app.name}', + emptyOutDir: true, + rollupOptions: { + input: resolve(__dirname, 'index.html'), + output: { + entryFileNames: 'assets/[name].js', + chunkFileNames: 'assets/[name].js', + assetFileNames: 'assets/[name].[ext]' + } + } + } +}); +`; + + writeFileSync(join(appDir, 'App.tsx'), appTsx); + writeFileSync(join(appDir, 'index.html'), indexHtml); + writeFileSync(join(appDir, 'main.tsx'), mainTsx); + writeFileSync(join(appDir, 'vite.config.ts'), viteConfig); + + console.log(`โœ“ Generated ${app.name}`); +}); + +// Also generate for pipeline-kanban and task-manager if they don't have all files +['pipeline-kanban', 'task-manager'].forEach(appName => { + const appDir = join(baseDir, appName); + const mainTsx = `import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); +`; + + const indexHtml = ` + + + + + ${appName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')} - Keap + + +
+ + + +`; + + const viteConfig = `import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/${appName}', + emptyOutDir: true, + rollupOptions: { + input: resolve(__dirname, 'index.html'), + output: { + entryFileNames: 'assets/[name].js', + chunkFileNames: 'assets/[name].js', + assetFileNames: 'assets/[name].[ext]' + } + } + } +}); +`; + + try { + writeFileSync(join(appDir, 'main.tsx'), mainTsx); + writeFileSync(join(appDir, 'index.html'), indexHtml); + writeFileSync(join(appDir, 'vite.config.ts'), viteConfig); + console.log(`โœ“ Completed ${appName}`); + } catch (e) { + console.log(`! ${appName} already has custom files`); + } +}); + +console.log(`\nGenerated ${apps.length + 2} apps successfully!`); diff --git a/servers/keap/package.json b/servers/keap/package.json index 472bb66..a53b498 100644 --- a/servers/keap/package.json +++ b/servers/keap/package.json @@ -1,38 +1,39 @@ { - "name": "@mcpengine/keap-server", + "name": "@mcpengine/keap", "version": "1.0.0", - "description": "Keap (Infusionsoft) MCP Server - Complete CRM automation", + "description": "MCP server for Keap (Infusionsoft) - Comprehensive contact, sales, marketing, and e-commerce automation platform", + "author": "MCPEngine", + "license": "MIT", "type": "module", - "main": "dist/main.js", "bin": { "keap-mcp": "./dist/main.js" }, "scripts": { - "build": "tsc", - "dev": "tsc --watch", - "start": "node dist/main.js", - "prepare": "npm run build" + "build": "tsc && npm run build:apps", + "build:apps": "cd src/ui/react-app && npm run build", + "dev": "tsx src/main.ts", + "prepublishOnly": "npm run build", + "test": "vitest" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.4", + "axios": "^1.7.0", + "dotenv": "^16.4.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "tsx": "^4.7.0", + "typescript": "^5.4.0", + "vitest": "^1.6.0" }, "keywords": [ "mcp", "keap", "infusionsoft", "crm", - "marketing-automation" - ], - "author": "MCPEngine", - "license": "MIT", - "dependencies": { - "@modelcontextprotocol/sdk": "^1.0.4", - "axios": "^1.7.9", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "lucide-react": "^0.468.0" - }, - "devDependencies": { - "@types/node": "^22.10.5", - "@types/react": "^18.3.18", - "@types/react-dom": "^18.3.5", - "typescript": "^5.7.3" - } + "marketing-automation", + "sales-pipeline", + "e-commerce", + "model-context-protocol" + ] } diff --git a/servers/keap/src/clients/keap.ts b/servers/keap/src/clients/keap.ts index 4c6c991..30ec44c 100644 --- a/servers/keap/src/clients/keap.ts +++ b/servers/keap/src/clients/keap.ts @@ -1,305 +1,172 @@ -import axios, { AxiosInstance, AxiosError } from 'axios'; -import type { - KeapConfig, - KeapError, - PaginatedResponse, - Contact, - Deal, - Company, - Task, - Appointment, - Campaign, - Email, - Order, - Product, - Tag, - Note, - Automation, -} from '../types/keap.js'; +import axios, { AxiosInstance, AxiosError, AxiosRequestConfig } from 'axios'; +import { ApiResponse, ApiError } from '../types/index.js'; export class KeapClient { private client: AxiosInstance; - private baseUrl: string; + private accessToken: string; + private apiKey?: string; + private baseURL = 'https://api.infusionsoft.com/crm/rest/v1'; + private rateLimitRemaining = 1000; + private rateLimitReset: number = Date.now(); + + constructor(accessToken?: string, apiKey?: string) { + this.accessToken = accessToken || process.env.KEAP_ACCESS_TOKEN || ''; + this.apiKey = apiKey || process.env.KEAP_API_KEY; + + if (!this.accessToken && !this.apiKey) { + throw new Error('KEAP_ACCESS_TOKEN or KEAP_API_KEY environment variable is required'); + } - constructor(config: KeapConfig) { - this.baseUrl = config.baseUrl || 'https://api.infusionsoft.com/crm/rest/v2'; - this.client = axios.create({ - baseURL: this.baseUrl, + baseURL: this.baseURL, headers: { - 'Authorization': `Bearer ${config.accessToken}`, 'Content-Type': 'application/json', + ...(this.accessToken && { 'Authorization': `Bearer ${this.accessToken}` }), + ...(this.apiKey && { 'X-Keap-API-Key': this.apiKey }), }, timeout: 30000, }); - // Response interceptor for error handling + // Response interceptor for rate limiting and error handling this.client.interceptors.response.use( - (response) => response, - (error: AxiosError) => { - if (error.response?.data) { - const keapError = error.response.data; - const message = keapError.message || keapError.fault?.faultstring || 'Unknown Keap API error'; - throw new Error(`Keap API Error: ${message}`); - } - throw new Error(`Request failed: ${error.message}`); + (response) => { + // Update rate limit info from headers + const remaining = response.headers['x-rate-limit-remaining']; + const reset = response.headers['x-rate-limit-reset']; + + if (remaining) this.rateLimitRemaining = parseInt(remaining, 10); + if (reset) this.rateLimitReset = parseInt(reset, 10) * 1000; + + return response; + }, + (error) => { + throw this.handleError(error); } ); } - // Generic paginated request - async paginate(endpoint: string, params: Record = {}): Promise { + private handleError(error: AxiosError): Error { + if (error.response) { + const status = error.response.status; + const data = error.response.data as any; + + switch (status) { + case 400: + return new Error(`Bad Request: ${data.message || JSON.stringify(data)}`); + case 401: + return new Error('Unauthorized: Invalid or expired access token'); + case 403: + return new Error('Forbidden: Insufficient permissions'); + case 404: + return new Error(`Not Found: ${data.message || 'Resource not found'}`); + case 429: + return new Error(`Rate Limit Exceeded: Retry after ${this.rateLimitReset}`); + case 500: + case 502: + case 503: + return new Error(`Keap Server Error (${status}): ${data.message || 'Internal server error'}`); + default: + return new Error(`Keap API Error (${status}): ${data.message || JSON.stringify(data)}`); + } + } else if (error.request) { + return new Error('Network Error: No response from Keap API'); + } + return new Error(`Request Error: ${error.message}`); + } + + private async checkRateLimit(): Promise { + if (this.rateLimitRemaining < 10 && Date.now() < this.rateLimitReset) { + const waitTime = this.rateLimitReset - Date.now(); + console.warn(`Rate limit approaching, waiting ${waitTime}ms`); + await new Promise(resolve => setTimeout(resolve, waitTime)); + } + } + + async request(config: AxiosRequestConfig): Promise { + await this.checkRateLimit(); + const response = await this.client.request(config); + return response.data; + } + + async get(path: string, params?: any): Promise { + return this.request({ method: 'GET', url: path, params }); + } + + async post(path: string, data?: any): Promise { + return this.request({ method: 'POST', url: path, data }); + } + + async put(path: string, data?: any): Promise { + return this.request({ method: 'PUT', url: path, data }); + } + + async patch(path: string, data?: any): Promise { + return this.request({ method: 'PATCH', url: path, data }); + } + + async delete(path: string): Promise { + return this.request({ method: 'DELETE', url: path }); + } + + // Pagination helper + async getAllPages(path: string, params: any = {}): Promise { const results: T[] = []; - let nextUrl: string | undefined = undefined; - let hasMore = true; + let offset = 0; + const limit = params.limit || 200; - const limit = params.limit || 1000; - const requestParams = { ...params, limit }; - - while (hasMore) { - const response = await this.client.get>( - nextUrl || endpoint, - nextUrl ? undefined : { params: requestParams } - ); - - results.push(...response.data.data); - - if (response.data.next && results.length < (params.max_results || Infinity)) { - nextUrl = response.data.next; + while (true) { + const response = await this.get>(path, { ...params, limit, offset }); + + if (response.data && response.data.length > 0) { + results.push(...response.data); + + // Check if there are more pages + if (response.data.length < limit || !response.next) { + break; + } + + offset += limit; } else { - hasMore = false; + break; } } return results; } - // Generic GET request - async get(endpoint: string, params?: Record): Promise { - const response = await this.client.get(endpoint, { params }); + // V2 API support (some endpoints use v2) + async requestV2(config: AxiosRequestConfig): Promise { + const v2Config = { + ...config, + baseURL: 'https://api.infusionsoft.com/crm/rest/v2', + }; + + await this.checkRateLimit(); + const response = await axios.request({ + ...v2Config, + headers: { + 'Content-Type': 'application/json', + ...(this.accessToken && { 'Authorization': `Bearer ${this.accessToken}` }), + ...(this.apiKey && { 'X-Keap-API-Key': this.apiKey }), + }, + }); + return response.data; } - // Generic POST request - async post(endpoint: string, data: any): Promise { - const response = await this.client.post(endpoint, data); - return response.data; + async getV2(path: string, params?: any): Promise { + return this.requestV2({ method: 'GET', url: path, params }); } - // Generic PUT request - async put(endpoint: string, data: any): Promise { - const response = await this.client.put(endpoint, data); - return response.data; + async postV2(path: string, data?: any): Promise { + return this.requestV2({ method: 'POST', url: path, data }); } - // Generic PATCH request - async patch(endpoint: string, data: any): Promise { - const response = await this.client.patch(endpoint, data); - return response.data; + async patchV2(path: string, data?: any): Promise { + return this.requestV2({ method: 'PATCH', url: path, data }); } - // Generic DELETE request - async delete(endpoint: string): Promise { - await this.client.delete(endpoint); - } - - // Contacts - async listContacts(params?: Record): Promise { - return this.paginate('/contacts', params); - } - - async getContact(contactId: number): Promise { - return this.get(`/contacts/${contactId}`); - } - - async createContact(data: Partial): Promise { - return this.post('/contacts', data); - } - - async updateContact(contactId: number, data: Partial): Promise { - return this.patch(`/contacts/${contactId}`, data); - } - - async deleteContact(contactId: number): Promise { - return this.delete(`/contacts/${contactId}`); - } - - // Deals - async listDeals(params?: Record): Promise { - return this.paginate('/opportunities', params); - } - - async getDeal(dealId: number): Promise { - return this.get(`/opportunities/${dealId}`); - } - - async createDeal(data: Partial): Promise { - return this.post('/opportunities', data); - } - - async updateDeal(dealId: number, data: Partial): Promise { - return this.patch(`/opportunities/${dealId}`, data); - } - - async deleteDeal(dealId: number): Promise { - return this.delete(`/opportunities/${dealId}`); - } - - // Companies - async listCompanies(params?: Record): Promise { - return this.paginate('/companies', params); - } - - async getCompany(companyId: number): Promise { - return this.get(`/companies/${companyId}`); - } - - async createCompany(data: Partial): Promise { - return this.post('/companies', data); - } - - async updateCompany(companyId: number, data: Partial): Promise { - return this.patch(`/companies/${companyId}`, data); - } - - async deleteCompany(companyId: number): Promise { - return this.delete(`/companies/${companyId}`); - } - - // Tasks - async listTasks(params?: Record): Promise { - return this.paginate('/tasks', params); - } - - async getTask(taskId: number): Promise { - return this.get(`/tasks/${taskId}`); - } - - async createTask(data: Partial): Promise { - return this.post('/tasks', data); - } - - async updateTask(taskId: number, data: Partial): Promise { - return this.patch(`/tasks/${taskId}`, data); - } - - async deleteTask(taskId: number): Promise { - return this.delete(`/tasks/${taskId}`); - } - - // Appointments - async listAppointments(params?: Record): Promise { - return this.paginate('/appointments', params); - } - - async getAppointment(appointmentId: number): Promise { - return this.get(`/appointments/${appointmentId}`); - } - - async createAppointment(data: Partial): Promise { - return this.post('/appointments', data); - } - - async updateAppointment(appointmentId: number, data: Partial): Promise { - return this.patch(`/appointments/${appointmentId}`, data); - } - - async deleteAppointment(appointmentId: number): Promise { - return this.delete(`/appointments/${appointmentId}`); - } - - // Campaigns - async listCampaigns(params?: Record): Promise { - return this.paginate('/campaigns', params); - } - - async getCampaign(campaignId: number): Promise { - return this.get(`/campaigns/${campaignId}`); - } - - // Emails - async listEmails(params?: Record): Promise { - return this.paginate('/emails', params); - } - - async getEmail(emailId: number): Promise { - return this.get(`/emails/${emailId}`); - } - - async createEmail(data: Partial): Promise { - return this.post('/emails', data); - } - - async sendEmail(data: Partial): Promise { - return this.post('/emails/send', data); - } - - // Orders - async listOrders(params?: Record): Promise { - return this.paginate('/orders', params); - } - - async getOrder(orderId: number): Promise { - return this.get(`/orders/${orderId}`); - } - - async createOrder(data: Partial): Promise { - return this.post('/orders', data); - } - - // Products - async listProducts(params?: Record): Promise { - return this.paginate('/products', params); - } - - async getProduct(productId: number): Promise { - return this.get(`/products/${productId}`); - } - - async createProduct(data: Partial): Promise { - return this.post('/products', data); - } - - async updateProduct(productId: number, data: Partial): Promise { - return this.patch(`/products/${productId}`, data); - } - - async deleteProduct(productId: number): Promise { - return this.delete(`/products/${productId}`); - } - - // Tags - async listTags(params?: Record): Promise { - return this.paginate('/tags', params); - } - - async getTag(tagId: number): Promise { - return this.get(`/tags/${tagId}`); - } - - async createTag(data: Partial): Promise { - return this.post('/tags', data); - } - - async deleteTag(tagId: number): Promise { - return this.delete(`/tags/${tagId}`); - } - - // Notes - async listNotes(params?: Record): Promise { - return this.paginate('/notes', params); - } - - async createNote(data: Partial): Promise { - return this.post('/notes', data); - } - - // Automations - async listAutomations(params?: Record): Promise { - return this.paginate('/campaigns', params); - } - - async getAutomation(automationId: number): Promise { - return this.get(`/campaigns/${automationId}`); + async deleteV2(path: string): Promise { + return this.requestV2({ method: 'DELETE', url: path }); } } diff --git a/servers/keap/src/main.ts b/servers/keap/src/main.ts index 517ad8a..39bfce2 100644 --- a/servers/keap/src/main.ts +++ b/servers/keap/src/main.ts @@ -1,15 +1,10 @@ #!/usr/bin/env node -import { KeapMCPServer } from './server.js'; +import { config } from 'dotenv'; +import { KeapServer } from './server.js'; -const accessToken = process.env.KEAP_ACCESS_TOKEN; +// Load environment variables +config(); -if (!accessToken) { - console.error('Error: KEAP_ACCESS_TOKEN environment variable is required'); - process.exit(1); -} - -const server = new KeapMCPServer(accessToken); -server.start().catch((error) => { - console.error('Failed to start server:', error); - process.exit(1); -}); +// Create and run the server +const server = new KeapServer(); +server.run().catch(console.error); diff --git a/servers/keap/src/server.ts b/servers/keap/src/server.ts index ab20938..7ea1e51 100644 --- a/servers/keap/src/server.ts +++ b/servers/keap/src/server.ts @@ -3,27 +3,32 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { CallToolRequestSchema, ListToolsRequestSchema, + Tool, } from '@modelcontextprotocol/sdk/types.js'; import { KeapClient } from './clients/keap.js'; -import { registerContactsTools } from './tools/contacts-tools.js'; -import { registerDealsTools } from './tools/deals-tools.js'; -import { registerCompaniesTools } from './tools/companies-tools.js'; -import { registerTasksTools } from './tools/tasks-tools.js'; -import { registerAppointmentsTools } from './tools/appointments-tools.js'; -import { registerCampaignsTools } from './tools/campaigns-tools.js'; -import { registerEmailsTools } from './tools/emails-tools.js'; -import { registerOrdersTools } from './tools/orders-tools.js'; -import { registerProductsTools } from './tools/products-tools.js'; -import { registerTagsTools } from './tools/tags-tools.js'; -import { registerAutomationsTools } from './tools/automations-tools.js'; -import { registerReportsTools } from './tools/reports-tools.js'; -export class KeapMCPServer { +// Import all tool creators and handlers +import { createContactsTools, handleContactsTool } from './tools/contacts-tools.js'; +import { createCompaniesTools, handleCompaniesTool } from './tools/companies-tools.js'; +import { createOpportunitiesTools, handleOpportunitiesTool } from './tools/opportunities-tools.js'; +import { createTasksTools, handleTasksTool } from './tools/tasks-tools.js'; +import { createAppointmentsTools, handleAppointmentsTool } from './tools/appointments-tools.js'; +import { createCampaignsTools, handleCampaignsTool } from './tools/campaigns-tools.js'; +import { createTagsTools, handleTagsTool } from './tools/tags-tools.js'; +import { createNotesTools, handleNotesTool } from './tools/notes-tools.js'; +import { createEmailsTools, handleEmailsTool } from './tools/emails-tools.js'; +import { createFilesTools, handleFilesTool } from './tools/files-tools.js'; +import { createEcommerceTools, handleEcommerceTool } from './tools/ecommerce-tools.js'; +import { createAutomationsTools, handleAutomationsTool } from './tools/automations-tools.js'; +import { createSettingsTools, handleSettingsTool } from './tools/settings-tools.js'; +import { createAffiliatesTools, handleAffiliatesTool } from './tools/affiliates-tools.js'; + +export class KeapServer { private server: Server; private client: KeapClient; - private tools: Map = new Map(); + private allTools: Tool[] = []; - constructor(accessToken: string) { + constructor() { this.server = new Server( { name: 'keap-mcp-server', @@ -36,68 +41,137 @@ export class KeapMCPServer { } ); - this.client = new KeapClient({ accessToken }); - this.registerAllTools(); + // Initialize Keap client + this.client = new KeapClient(); + + // Register all tools + this.registerTools(); + + // Set up request handlers this.setupHandlers(); + + // Error handling + this.server.onerror = (error) => { + console.error('[MCP Error]', error); + }; + + process.on('SIGINT', async () => { + await this.server.close(); + process.exit(0); + }); } - private registerAllTools() { - const toolGroups = [ - registerContactsTools(this.client), - registerDealsTools(this.client), - registerCompaniesTools(this.client), - registerTasksTools(this.client), - registerAppointmentsTools(this.client), - registerCampaignsTools(this.client), - registerEmailsTools(this.client), - registerOrdersTools(this.client), - registerProductsTools(this.client), - registerTagsTools(this.client), - registerAutomationsTools(this.client), - registerReportsTools(this.client), + private registerTools(): void { + // Collect all tools from different domains + this.allTools = [ + ...createContactsTools(this.client), + ...createCompaniesTools(this.client), + ...createOpportunitiesTools(this.client), + ...createTasksTools(this.client), + ...createAppointmentsTools(this.client), + ...createCampaignsTools(this.client), + ...createTagsTools(this.client), + ...createNotesTools(this.client), + ...createEmailsTools(this.client), + ...createFilesTools(this.client), + ...createEcommerceTools(this.client), + ...createAutomationsTools(this.client), + ...createSettingsTools(this.client), + ...createAffiliatesTools(this.client), ]; - for (const group of toolGroups) { - for (const [name, tool] of Object.entries(group)) { - this.tools.set(name, tool); - } - } + console.error(`[Keap MCP] Registered ${this.allTools.length} tools`); } - private setupHandlers() { - this.server.setRequestHandler(ListToolsRequestSchema, async () => { - const tools = Array.from(this.tools.entries()).map(([name, tool]) => ({ - name, - description: tool.description, - inputSchema: tool.inputSchema, - })); - - return { tools }; - }); + private setupHandlers(): void { + // Handle list_tools request + this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: this.allTools, + })); + // Handle call_tool request this.server.setRequestHandler(CallToolRequestSchema, async (request) => { - const tool = this.tools.get(request.params.name); - - if (!tool) { - throw new Error(`Tool not found: ${request.params.name}`); - } + const { name, arguments: args } = request.params; try { - const result = await tool.handler(request.params.arguments || {}); - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - }; + // Route to appropriate handler based on tool name prefix + if (name.startsWith('keap_create_contact') || + name.startsWith('keap_get_contact') || + name.startsWith('keap_update_contact') || + name.startsWith('keap_delete_contact') || + name.startsWith('keap_list_contact') || + name.startsWith('keap_search_contact') || + name.startsWith('keap_merge_contact') || + name.startsWith('keap_apply_tag') || + name.startsWith('keap_remove_tag')) { + return await handleContactsTool(name, args, this.client); + } + + if (name.startsWith('keap_') && name.includes('_compan')) { + return await handleCompaniesTool(name, args, this.client); + } + + if (name.startsWith('keap_') && name.includes('_opportunit')) { + return await handleOpportunitiesTool(name, args, this.client); + } + + if (name.startsWith('keap_') && name.includes('_task')) { + return await handleTasksTool(name, args, this.client); + } + + if (name.startsWith('keap_') && name.includes('_appointment')) { + return await handleAppointmentsTool(name, args, this.client); + } + + if (name.startsWith('keap_') && name.includes('_campaign')) { + return await handleCampaignsTool(name, args, this.client); + } + + if (name.startsWith('keap_') && name.includes('_tag')) { + return await handleTagsTool(name, args, this.client); + } + + if (name.startsWith('keap_') && name.includes('_note')) { + return await handleNotesTool(name, args, this.client); + } + + if (name.startsWith('keap_') && name.includes('_email')) { + return await handleEmailsTool(name, args, this.client); + } + + if (name.startsWith('keap_') && name.includes('_file')) { + return await handleFilesTool(name, args, this.client); + } + + if (name.startsWith('keap_') && (name.includes('_product') || + name.includes('_order') || + name.includes('_transaction') || + name.includes('_subscription'))) { + return await handleEcommerceTool(name, args, this.client); + } + + if (name.startsWith('keap_') && (name.includes('_hook') || name.includes('_automation'))) { + return await handleAutomationsTool(name, args, this.client); + } + + if (name.startsWith('keap_') && (name.includes('_account') || + name.includes('_application') || + name.includes('_user') || + name.includes('_custom_field'))) { + return await handleSettingsTool(name, args, this.client); + } + + if (name.startsWith('keap_') && (name.includes('_affiliate') || name.includes('_commission'))) { + return await handleAffiliatesTool(name, args, this.client); + } + + throw new Error(`Unknown tool: ${name}`); } catch (error: any) { return { content: [ { type: 'text', - text: `Error: ${error.message}`, + text: `Error executing ${name}: ${error.message}`, }, ], isError: true, @@ -106,9 +180,9 @@ export class KeapMCPServer { }); } - async start() { + async run(): Promise { const transport = new StdioServerTransport(); await this.server.connect(transport); - console.error('Keap MCP Server running on stdio'); + console.error('[Keap MCP] Server running on stdio'); } } diff --git a/servers/keap/src/tools/affiliates-tools.ts b/servers/keap/src/tools/affiliates-tools.ts new file mode 100644 index 0000000..bb96824 --- /dev/null +++ b/servers/keap/src/tools/affiliates-tools.ts @@ -0,0 +1,187 @@ +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import { KeapClient } from '../clients/keap.js'; + +export function createAffiliatesTools(client: KeapClient): Tool[] { + return [ + { + name: 'keap_create_affiliate', + description: 'Create a new affiliate', + inputSchema: { + type: 'object', + properties: { + contact_id: { type: 'number', description: 'Contact ID for this affiliate', required: true }, + code: { type: 'string', description: 'Affiliate code', required: true }, + name: { type: 'string', description: 'Affiliate name', required: true }, + parent_id: { type: 'number', description: 'Parent affiliate ID' }, + track_leads_for: { type: 'number', description: 'Number of days to track leads' }, + }, + required: ['contact_id', 'code', 'name'], + }, + }, + { + name: 'keap_get_affiliate', + description: 'Retrieve an affiliate by ID', + inputSchema: { + type: 'object', + properties: { + affiliate_id: { type: 'number', description: 'Affiliate ID', required: true }, + }, + required: ['affiliate_id'], + }, + }, + { + name: 'keap_list_affiliates', + description: 'List all affiliates with filtering', + inputSchema: { + type: 'object', + properties: { + limit: { type: 'number', description: 'Results per page', default: 50 }, + offset: { type: 'number', description: 'Pagination offset', default: 0 }, + status: { type: 'number', description: 'Filter by status (0=inactive, 1=active)' }, + }, + }, + }, + { + name: 'keap_get_affiliate_clawbacks', + description: 'Get all clawbacks for an affiliate', + inputSchema: { + type: 'object', + properties: { + affiliate_id: { type: 'number', description: 'Affiliate ID', required: true }, + }, + required: ['affiliate_id'], + }, + }, + { + name: 'keap_get_affiliate_commissions', + description: 'Get all commissions for an affiliate', + inputSchema: { + type: 'object', + properties: { + affiliate_id: { type: 'number', description: 'Affiliate ID', required: true }, + since: { type: 'string', description: 'Commissions after this date' }, + until: { type: 'string', description: 'Commissions before this date' }, + limit: { type: 'number', description: 'Results per page', default: 50 }, + }, + required: ['affiliate_id'], + }, + }, + { + name: 'keap_get_affiliate_payments', + description: 'Get all payments for an affiliate', + inputSchema: { + type: 'object', + properties: { + affiliate_id: { type: 'number', description: 'Affiliate ID', required: true }, + }, + required: ['affiliate_id'], + }, + }, + { + name: 'keap_get_affiliate_redirect_links', + description: 'Get redirect links for an affiliate', + inputSchema: { + type: 'object', + properties: { + affiliate_id: { type: 'number', description: 'Affiliate ID', required: true }, + }, + required: ['affiliate_id'], + }, + }, + { + name: 'keap_get_affiliate_summary', + description: 'Get summary stats for an affiliate', + inputSchema: { + type: 'object', + properties: { + affiliate_id: { type: 'number', description: 'Affiliate ID', required: true }, + }, + required: ['affiliate_id'], + }, + }, + { + name: 'keap_list_commissions', + description: 'List all commissions with filtering', + inputSchema: { + type: 'object', + properties: { + affiliate_id: { type: 'number', description: 'Filter by affiliate' }, + contact_id: { type: 'number', description: 'Filter by contact' }, + limit: { type: 'number', description: 'Results per page', default: 50 }, + offset: { type: 'number', description: 'Pagination offset', default: 0 }, + since: { type: 'string', description: 'Commissions after this date' }, + until: { type: 'string', description: 'Commissions before this date' }, + }, + }, + }, + ]; +} + +export async function handleAffiliatesTool( + toolName: string, + args: any, + client: KeapClient +): Promise<{ content: Array<{ type: string; text: string }> }> { + let result: any; + + try { + switch (toolName) { + case 'keap_create_affiliate': + result = await client.post('/affiliates', { + contact_id: args.contact_id, + code: args.code, + name: args.name, + parent_id: args.parent_id, + track_leads_for: args.track_leads_for, + }); + break; + + case 'keap_get_affiliate': + result = await client.get(`/affiliates/${args.affiliate_id}`); + break; + + case 'keap_list_affiliates': + result = await client.get('/affiliates', args); + break; + + case 'keap_get_affiliate_clawbacks': + result = await client.get(`/affiliates/${args.affiliate_id}/clawbacks`); + break; + + case 'keap_get_affiliate_commissions': + result = await client.get(`/affiliates/${args.affiliate_id}/commissions`, { + since: args.since, + until: args.until, + limit: args.limit, + }); + break; + + case 'keap_get_affiliate_payments': + result = await client.get(`/affiliates/${args.affiliate_id}/payments`); + break; + + case 'keap_get_affiliate_redirect_links': + result = await client.get(`/affiliates/${args.affiliate_id}/redirectLinks`); + break; + + case 'keap_get_affiliate_summary': + result = await client.get(`/affiliates/${args.affiliate_id}/summaries`); + break; + + case 'keap_list_commissions': + result = await client.get('/commissions', args); + break; + + default: + throw new Error(`Unknown tool: ${toolName}`); + } + + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], + }; + } catch (error: any) { + return { + content: [{ type: 'text', text: `Error: ${error.message}` }], + }; + } +} diff --git a/servers/keap/src/tools/appointments-tools.ts b/servers/keap/src/tools/appointments-tools.ts index 9e71b66..9df6aee 100644 --- a/servers/keap/src/tools/appointments-tools.ts +++ b/servers/keap/src/tools/appointments-tools.ts @@ -1,112 +1,147 @@ +import { Tool } from '@modelcontextprotocol/sdk/types.js'; import { KeapClient } from '../clients/keap.js'; -export function registerAppointmentsTools(client: KeapClient) { - return { - keap_list_appointments: { - description: 'List all appointments', +export function createAppointmentsTools(client: KeapClient): Tool[] { + return [ + { + name: 'keap_create_appointment', + description: 'Create a new appointment in Keap', inputSchema: { type: 'object', properties: { - contact_id: { type: 'number', description: 'Filter by contact ID' }, - user_id: { type: 'number', description: 'Filter by assigned user ID' }, - since: { type: 'string', description: 'Start date filter (ISO 8601)' }, - until: { type: 'string', description: 'End date filter (ISO 8601)' }, - limit: { type: 'number', description: 'Maximum results' }, - }, - }, - handler: async (args: any) => { - const appointments = await client.listAppointments(args); - return { appointments, count: appointments.length }; - }, - }, - - keap_get_appointment: { - description: 'Get a specific appointment by ID', - inputSchema: { - type: 'object', - properties: { - appointment_id: { type: 'number', description: 'Appointment ID' }, - }, - required: ['appointment_id'], - }, - handler: async (args: any) => { - return await client.getAppointment(args.appointment_id); - }, - }, - - keap_create_appointment: { - description: 'Create a new appointment', - inputSchema: { - type: 'object', - properties: { - title: { type: 'string', description: 'Appointment title' }, + title: { type: 'string', description: 'Appointment title', required: true }, + start_date: { type: 'string', description: 'Start date/time (ISO format)', required: true }, + end_date: { type: 'string', description: 'End date/time (ISO format)', required: true }, description: { type: 'string', description: 'Appointment description' }, - location: { type: 'string', description: 'Appointment location' }, - start_date: { type: 'string', description: 'Start date/time (ISO 8601)' }, - end_date: { type: 'string', description: 'End date/time (ISO 8601)' }, - contact_id: { type: 'number', description: 'Contact ID' }, + location: { type: 'string', description: 'Location' }, + contact_id: { type: 'number', description: 'Associated contact ID' }, user_id: { type: 'number', description: 'Assigned user ID' }, - all_day: { type: 'boolean', description: 'All-day appointment' }, + remind_time: { type: 'number', description: 'Reminder time in minutes before appointment' }, + all_day: { type: 'boolean', description: 'Is this an all-day event?', default: false }, }, required: ['title', 'start_date', 'end_date'], }, - handler: async (args: any) => { - return await client.createAppointment({ - title: args.title, - description: args.description, - location: args.location, - start_date: args.start_date, - end_date: args.end_date, - contact: args.contact_id ? { id: args.contact_id } : undefined, - user_id: args.user_id, - all_day: args.all_day || false, - }); + }, + { + name: 'keap_get_appointment', + description: 'Retrieve an appointment by ID', + inputSchema: { + type: 'object', + properties: { + appointment_id: { type: 'number', description: 'Appointment ID', required: true }, + }, + required: ['appointment_id'], }, }, - - keap_update_appointment: { + { + name: 'keap_update_appointment', description: 'Update an existing appointment', inputSchema: { type: 'object', properties: { - appointment_id: { type: 'number', description: 'Appointment ID' }, + appointment_id: { type: 'number', description: 'Appointment ID', required: true }, title: { type: 'string', description: 'Appointment title' }, - description: { type: 'string', description: 'Appointment description' }, - location: { type: 'string', description: 'Appointment location' }, - start_date: { type: 'string', description: 'Start date/time (ISO 8601)' }, - end_date: { type: 'string', description: 'End date/time (ISO 8601)' }, - user_id: { type: 'number', description: 'Assigned user ID' }, - all_day: { type: 'boolean', description: 'All-day appointment' }, + start_date: { type: 'string', description: 'Start date/time' }, + end_date: { type: 'string', description: 'End date/time' }, + description: { type: 'string', description: 'Description' }, + location: { type: 'string', description: 'Location' }, }, required: ['appointment_id'], }, - handler: async (args: any) => { - const data: any = {}; - if (args.title !== undefined) data.title = args.title; - if (args.description !== undefined) data.description = args.description; - if (args.location !== undefined) data.location = args.location; - if (args.start_date !== undefined) data.start_date = args.start_date; - if (args.end_date !== undefined) data.end_date = args.end_date; - if (args.user_id !== undefined) data.user_id = args.user_id; - if (args.all_day !== undefined) data.all_day = args.all_day; - - return await client.updateAppointment(args.appointment_id, data); - }, }, - - keap_delete_appointment: { + { + name: 'keap_delete_appointment', description: 'Delete an appointment', inputSchema: { type: 'object', properties: { - appointment_id: { type: 'number', description: 'Appointment ID' }, + appointment_id: { type: 'number', description: 'Appointment ID to delete', required: true }, }, required: ['appointment_id'], }, - handler: async (args: any) => { - await client.deleteAppointment(args.appointment_id); - return { success: true, message: `Appointment ${args.appointment_id} deleted` }; + }, + { + name: 'keap_list_appointments', + description: 'List appointments with filtering and pagination', + inputSchema: { + type: 'object', + properties: { + limit: { type: 'number', description: 'Results per page', default: 50 }, + offset: { type: 'number', description: 'Pagination offset', default: 0 }, + user_id: { type: 'number', description: 'Filter by user' }, + contact_id: { type: 'number', description: 'Filter by contact' }, + since: { type: 'string', description: 'Appointments after this date' }, + until: { type: 'string', description: 'Appointments before this date' }, + }, }, }, - }; + { + name: 'keap_get_appointment_model', + description: 'Get the appointment model schema', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + ]; +} + +export async function handleAppointmentsTool( + toolName: string, + args: any, + client: KeapClient +): Promise<{ content: Array<{ type: string; text: string }> }> { + let result: any; + + try { + switch (toolName) { + case 'keap_create_appointment': + result = await client.post('/appointments', { + title: args.title, + start_date: args.start_date, + end_date: args.end_date, + description: args.description, + location: args.location, + contact: args.contact_id ? { id: args.contact_id } : undefined, + user: args.user_id, + remind_time: args.remind_time, + all_day: args.all_day || false, + }); + break; + + case 'keap_get_appointment': + result = await client.get(`/appointments/${args.appointment_id}`); + break; + + case 'keap_update_appointment': { + const { appointment_id, ...updateData } = args; + result = await client.patch(`/appointments/${appointment_id}`, updateData); + break; + } + + case 'keap_delete_appointment': + await client.delete(`/appointments/${args.appointment_id}`); + result = { success: true, message: 'Appointment deleted successfully' }; + break; + + case 'keap_list_appointments': + result = await client.get('/appointments', args); + break; + + case 'keap_get_appointment_model': + result = await client.get('/appointments/model'); + break; + + default: + throw new Error(`Unknown tool: ${toolName}`); + } + + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], + }; + } catch (error: any) { + return { + content: [{ type: 'text', text: `Error: ${error.message}` }], + }; + } } diff --git a/servers/keap/src/tools/automations-tools.ts b/servers/keap/src/tools/automations-tools.ts index bfe8e27..a1eb415 100644 --- a/servers/keap/src/tools/automations-tools.ts +++ b/servers/keap/src/tools/automations-tools.ts @@ -1,69 +1,123 @@ +import { Tool } from '@modelcontextprotocol/sdk/types.js'; import { KeapClient } from '../clients/keap.js'; -export function registerAutomationsTools(client: KeapClient) { - return { - keap_list_automations: { - description: 'List all automations/campaigns', +export function createAutomationsTools(client: KeapClient): Tool[] { + return [ + { + name: 'keap_create_hook', + description: 'Create a REST hook for automation (webhook)', inputSchema: { type: 'object', properties: { - search_text: { type: 'string', description: 'Search by name' }, - category: { type: 'string', description: 'Filter by category' }, - limit: { type: 'number', description: 'Maximum results' }, + eventKey: { type: 'string', description: 'Event key (e.g., contact.add, opportunity.add)', required: true }, + hookUrl: { type: 'string', description: 'Webhook URL to call', required: true }, }, - }, - handler: async (args: any) => { - const automations = await client.listAutomations(args); - return { automations, count: automations.length }; + required: ['eventKey', 'hookUrl'], }, }, - - keap_get_automation: { - description: 'Get a specific automation by ID', + { + name: 'keap_list_hooks', + description: 'List all REST hooks', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'keap_delete_hook', + description: 'Delete a REST hook', inputSchema: { type: 'object', properties: { - automation_id: { type: 'number', description: 'Automation ID' }, + hook_key: { type: 'string', description: 'Hook key to delete', required: true }, }, - required: ['automation_id'], - }, - handler: async (args: any) => { - return await client.getAutomation(args.automation_id); + required: ['hook_key'], }, }, - - keap_activate_automation: { - description: 'Activate/publish an automation', + { + name: 'keap_verify_hook', + description: 'Verify a REST hook', inputSchema: { type: 'object', properties: { - automation_id: { type: 'number', description: 'Automation ID' }, + hook_key: { type: 'string', description: 'Hook key to verify', required: true }, }, - required: ['automation_id'], - }, - handler: async (args: any) => { - await client.patch(`/campaigns/${args.automation_id}`, { - published_status: 'Published', - }); - return { success: true, message: `Automation ${args.automation_id} activated` }; + required: ['hook_key'], }, }, - - keap_deactivate_automation: { - description: 'Deactivate/unpublish an automation', + { + name: 'keap_update_hook', + description: 'Update a REST hook', inputSchema: { type: 'object', properties: { - automation_id: { type: 'number', description: 'Automation ID' }, + hook_key: { type: 'string', description: 'Hook key to update', required: true }, + hookUrl: { type: 'string', description: 'New webhook URL' }, + status: { type: 'string', description: 'Hook status (Active, Inactive)' }, }, - required: ['automation_id'], - }, - handler: async (args: any) => { - await client.patch(`/campaigns/${args.automation_id}`, { - published_status: 'Draft', - }); - return { success: true, message: `Automation ${args.automation_id} deactivated` }; + required: ['hook_key'], }, }, - }; + { + name: 'keap_list_hook_event_types', + description: 'List all available hook event types', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + ]; +} + +export async function handleAutomationsTool( + toolName: string, + args: any, + client: KeapClient +): Promise<{ content: Array<{ type: string; text: string }> }> { + let result: any; + + try { + switch (toolName) { + case 'keap_create_hook': + result = await client.post('/hooks', { + eventKey: args.eventKey, + hookUrl: args.hookUrl, + }); + break; + + case 'keap_list_hooks': + result = await client.get('/hooks'); + break; + + case 'keap_delete_hook': + await client.delete(`/hooks/${args.hook_key}`); + result = { success: true, message: 'Hook deleted successfully' }; + break; + + case 'keap_verify_hook': + result = await client.post(`/hooks/${args.hook_key}/verify`, {}); + break; + + case 'keap_update_hook': { + const { hook_key, ...updateData } = args; + result = await client.patch(`/hooks/${hook_key}`, updateData); + break; + } + + case 'keap_list_hook_event_types': + result = await client.get('/hooks/event_keys'); + break; + + default: + throw new Error(`Unknown tool: ${toolName}`); + } + + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], + }; + } catch (error: any) { + return { + content: [{ type: 'text', text: `Error: ${error.message}` }], + }; + } } diff --git a/servers/keap/src/tools/campaigns-tools.ts b/servers/keap/src/tools/campaigns-tools.ts index 2091872..bd95d12 100644 --- a/servers/keap/src/tools/campaigns-tools.ts +++ b/servers/keap/src/tools/campaigns-tools.ts @@ -1,113 +1,143 @@ +import { Tool } from '@modelcontextprotocol/sdk/types.js'; import { KeapClient } from '../clients/keap.js'; -export function registerCampaignsTools(client: KeapClient) { - return { - keap_list_campaigns: { - description: 'List all campaigns', +export function createCampaignsTools(client: KeapClient): Tool[] { + return [ + { + name: 'keap_list_campaigns', + description: 'List all campaigns with pagination', inputSchema: { type: 'object', properties: { - search_text: { type: 'string', description: 'Search by campaign name' }, - limit: { type: 'number', description: 'Maximum results' }, + limit: { type: 'number', description: 'Results per page', default: 50 }, + offset: { type: 'number', description: 'Pagination offset', default: 0 }, + order: { type: 'string', description: 'Order by field' }, + search_text: { type: 'string', description: 'Search in campaign name/description' }, }, }, - handler: async (args: any) => { - const campaigns = await client.listCampaigns(args); - return { campaigns, count: campaigns.length }; - }, }, - - keap_get_campaign: { - description: 'Get a specific campaign by ID', + { + name: 'keap_get_campaign', + description: 'Get campaign details by ID', inputSchema: { type: 'object', properties: { - campaign_id: { type: 'number', description: 'Campaign ID' }, + campaign_id: { type: 'number', description: 'Campaign ID', required: true }, }, required: ['campaign_id'], }, - handler: async (args: any) => { - return await client.getCampaign(args.campaign_id); - }, }, - - keap_add_to_campaign: { + { + name: 'keap_add_contact_to_campaign', description: 'Add a contact to a campaign sequence', inputSchema: { type: 'object', properties: { - contact_id: { type: 'number', description: 'Contact ID' }, - campaign_id: { type: 'number', description: 'Campaign ID' }, + contact_id: { type: 'number', description: 'Contact ID', required: true }, + campaign_id: { type: 'number', description: 'Campaign ID', required: true }, }, required: ['contact_id', 'campaign_id'], }, - handler: async (args: any) => { - await client.post('/campaignSequences', { - contact_id: args.contact_id, - campaign_id: args.campaign_id, - }); - return { - success: true, - message: `Contact ${args.contact_id} added to campaign ${args.campaign_id}` - }; - }, }, - - keap_remove_from_campaign: { + { + name: 'keap_remove_contact_from_campaign', description: 'Remove a contact from a campaign sequence', inputSchema: { type: 'object', properties: { - contact_id: { type: 'number', description: 'Contact ID' }, - campaign_id: { type: 'number', description: 'Campaign ID' }, + contact_id: { type: 'number', description: 'Contact ID', required: true }, + campaign_id: { type: 'number', description: 'Campaign ID', required: true }, }, required: ['contact_id', 'campaign_id'], }, - handler: async (args: any) => { - const sequences = await client.get('/campaignSequences', { - contact_id: args.contact_id, - campaign_id: args.campaign_id, - }); - - if (sequences && sequences.sequences && sequences.sequences.length > 0) { - await client.delete(`/campaignSequences/${sequences.sequences[0].id}`); - return { - success: true, - message: `Contact ${args.contact_id} removed from campaign ${args.campaign_id}` - }; - } - - return { - success: false, - message: `Contact ${args.contact_id} not found in campaign ${args.campaign_id}` - }; - }, }, - - keap_get_campaign_stats: { - description: 'Get statistics for a campaign', + { + name: 'keap_get_campaign_sequences', + description: 'Get all sequences for a specific campaign', inputSchema: { type: 'object', properties: { - campaign_id: { type: 'number', description: 'Campaign ID' }, + campaign_id: { type: 'number', description: 'Campaign ID', required: true }, }, required: ['campaign_id'], }, - handler: async (args: any) => { - const sequences = await client.get('/campaignSequences', { - campaign_id: args.campaign_id, - }); - - const stats = { - campaign_id: args.campaign_id, - total_contacts: sequences.sequences?.length || 0, - active_contacts: sequences.sequences?.filter((s: any) => s.status === 'Active').length || 0, - completed_contacts: sequences.sequences?.filter((s: any) => s.status === 'Completed').length || 0, - stopped_contacts: sequences.sequences?.filter((s: any) => s.status === 'Stopped').length || 0, - }; - - return stats; + }, + { + name: 'keap_add_contact_to_sequence', + description: 'Add a contact to a specific campaign sequence', + inputSchema: { + type: 'object', + properties: { + contact_id: { type: 'number', description: 'Contact ID', required: true }, + sequence_id: { type: 'number', description: 'Sequence ID', required: true }, + }, + required: ['contact_id', 'sequence_id'], }, }, - }; + { + name: 'keap_remove_contact_from_sequence', + description: 'Remove a contact from a campaign sequence', + inputSchema: { + type: 'object', + properties: { + contact_id: { type: 'number', description: 'Contact ID', required: true }, + sequence_id: { type: 'number', description: 'Sequence ID', required: true }, + }, + required: ['contact_id', 'sequence_id'], + }, + }, + ]; +} + +export async function handleCampaignsTool( + toolName: string, + args: any, + client: KeapClient +): Promise<{ content: Array<{ type: string; text: string }> }> { + let result: any; + + try { + switch (toolName) { + case 'keap_list_campaigns': + result = await client.get('/campaigns', args); + break; + + case 'keap_get_campaign': + result = await client.get(`/campaigns/${args.campaign_id}`); + break; + + case 'keap_add_contact_to_campaign': + result = await client.post(`/contacts/${args.contact_id}/campaigns/${args.campaign_id}`, {}); + break; + + case 'keap_remove_contact_from_campaign': + await client.delete(`/contacts/${args.contact_id}/campaigns/${args.campaign_id}`); + result = { success: true, message: 'Contact removed from campaign' }; + break; + + case 'keap_get_campaign_sequences': + result = await client.get(`/campaigns/${args.campaign_id}/sequences`); + break; + + case 'keap_add_contact_to_sequence': + result = await client.post(`/contacts/${args.contact_id}/sequences/${args.sequence_id}`, {}); + break; + + case 'keap_remove_contact_from_sequence': + await client.delete(`/contacts/${args.contact_id}/sequences/${args.sequence_id}`); + result = { success: true, message: 'Contact removed from sequence' }; + break; + + default: + throw new Error(`Unknown tool: ${toolName}`); + } + + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], + }; + } catch (error: any) { + return { + content: [{ type: 'text', text: `Error: ${error.message}` }], + }; + } } diff --git a/servers/keap/src/tools/companies-tools.ts b/servers/keap/src/tools/companies-tools.ts index ce2bce9..a89621f 100644 --- a/servers/keap/src/tools/companies-tools.ts +++ b/servers/keap/src/tools/companies-tools.ts @@ -1,122 +1,141 @@ +import { Tool } from '@modelcontextprotocol/sdk/types.js'; import { KeapClient } from '../clients/keap.js'; -export function registerCompaniesTools(client: KeapClient) { - return { - keap_list_companies: { - description: 'List all companies', +export function createCompaniesTools(client: KeapClient): Tool[] { + return [ + { + name: 'keap_create_company', + description: 'Create a new company in Keap', inputSchema: { type: 'object', properties: { - company_name: { type: 'string', description: 'Filter by company name' }, - limit: { type: 'number', description: 'Maximum results' }, - order: { type: 'string', description: 'Sort order' }, - }, - }, - handler: async (args: any) => { - const companies = await client.listCompanies(args); - return { companies, count: companies.length }; - }, - }, - - keap_get_company: { - description: 'Get a specific company by ID', - inputSchema: { - type: 'object', - properties: { - company_id: { type: 'number', description: 'Company ID' }, - }, - required: ['company_id'], - }, - handler: async (args: any) => { - return await client.getCompany(args.company_id); - }, - }, - - keap_create_company: { - description: 'Create a new company', - inputSchema: { - type: 'object', - properties: { - company_name: { type: 'string', description: 'Company name' }, - email: { type: 'string', description: 'Company email address' }, - phone: { type: 'string', description: 'Company phone number' }, - fax: { type: 'string', description: 'Company fax number' }, - website: { type: 'string', description: 'Company website' }, + company_name: { type: 'string', description: 'Company name', required: true }, + email: { type: 'string', description: 'Company email' }, + phone: { type: 'string', description: 'Company phone' }, address_line1: { type: 'string', description: 'Street address' }, - address_city: { type: 'string', description: 'City' }, - address_state: { type: 'string', description: 'State/Province' }, - address_zip: { type: 'string', description: 'ZIP/Postal code' }, - address_country: { type: 'string', description: 'Country code' }, - notes: { type: 'string', description: 'Company notes' }, + city: { type: 'string', description: 'City' }, + state: { type: 'string', description: 'State/Region' }, + postal_code: { type: 'string', description: 'Postal code' }, + country: { type: 'string', description: 'Country code' }, + website: { type: 'string', description: 'Company website' }, + notes: { type: 'string', description: 'Notes about the company' }, }, required: ['company_name'], }, - handler: async (args: any) => { - const data: any = { - company_name: args.company_name, - email_address: args.email, - phone_number: args.phone, - fax_number: args.fax, - website: args.website, - notes: args.notes, - }; - - if (args.address_line1 || args.address_city) { - data.address = { - line1: args.address_line1, - locality: args.address_city, - region: args.address_state, - zip_code: args.address_zip, - country_code: args.address_country || 'US', - field: 'BILLING', - }; - } - - return await client.createCompany(data); + }, + { + name: 'keap_get_company', + description: 'Retrieve a company by ID', + inputSchema: { + type: 'object', + properties: { + company_id: { type: 'number', description: 'Company ID', required: true }, + }, + required: ['company_id'], }, }, - - keap_update_company: { + { + name: 'keap_update_company', description: 'Update an existing company', inputSchema: { type: 'object', properties: { - company_id: { type: 'number', description: 'Company ID' }, + company_id: { type: 'number', description: 'Company ID', required: true }, company_name: { type: 'string', description: 'Company name' }, - email: { type: 'string', description: 'Company email address' }, - phone: { type: 'string', description: 'Company phone number' }, - fax: { type: 'string', description: 'Company fax number' }, + email: { type: 'string', description: 'Company email' }, + phone: { type: 'string', description: 'Company phone' }, website: { type: 'string', description: 'Company website' }, - notes: { type: 'string', description: 'Company notes' }, + notes: { type: 'string', description: 'Notes' }, }, required: ['company_id'], }, - handler: async (args: any) => { - const data: any = {}; - if (args.company_name !== undefined) data.company_name = args.company_name; - if (args.email !== undefined) data.email_address = args.email; - if (args.phone !== undefined) data.phone_number = args.phone; - if (args.fax !== undefined) data.fax_number = args.fax; - if (args.website !== undefined) data.website = args.website; - if (args.notes !== undefined) data.notes = args.notes; - - return await client.updateCompany(args.company_id, data); - }, }, - - keap_delete_company: { - description: 'Delete a company', + { + name: 'keap_list_companies', + description: 'List all companies with pagination', inputSchema: { type: 'object', properties: { - company_id: { type: 'number', description: 'Company ID' }, + limit: { type: 'number', description: 'Results per page', default: 50 }, + offset: { type: 'number', description: 'Pagination offset', default: 0 }, + company_name: { type: 'string', description: 'Filter by company name' }, + order: { type: 'string', description: 'Order by field' }, + }, + }, + }, + { + name: 'keap_get_company_contacts', + description: 'Get all contacts associated with a company', + inputSchema: { + type: 'object', + properties: { + company_id: { type: 'number', description: 'Company ID', required: true }, + limit: { type: 'number', description: 'Max results', default: 50 }, + offset: { type: 'number', description: 'Pagination offset', default: 0 }, }, required: ['company_id'], }, - handler: async (args: any) => { - await client.deleteCompany(args.company_id); - return { success: true, message: `Company ${args.company_id} deleted` }; - }, }, - }; + ]; +} + +export async function handleCompaniesTool( + toolName: string, + args: any, + client: KeapClient +): Promise<{ content: Array<{ type: string; text: string }> }> { + let result: any; + + try { + switch (toolName) { + case 'keap_create_company': + result = await client.post('/companies', { + company_name: args.company_name, + email_address: args.email ? { email: args.email } : undefined, + phone_number: args.phone ? { number: args.phone } : undefined, + address: args.address_line1 ? { + line1: args.address_line1, + locality: args.city, + region: args.state, + postal_code: args.postal_code, + country_code: args.country, + } : undefined, + website: args.website, + notes: args.notes, + }); + break; + + case 'keap_get_company': + result = await client.get(`/companies/${args.company_id}`); + break; + + case 'keap_update_company': { + const { company_id, ...updateData } = args; + result = await client.patch(`/companies/${company_id}`, updateData); + break; + } + + case 'keap_list_companies': + result = await client.get('/companies', args); + break; + + case 'keap_get_company_contacts': + result = await client.get(`/companies/${args.company_id}/contacts`, { + limit: args.limit, + offset: args.offset, + }); + break; + + default: + throw new Error(`Unknown tool: ${toolName}`); + } + + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], + }; + } catch (error: any) { + return { + content: [{ type: 'text', text: `Error: ${error.message}` }], + }; + } } diff --git a/servers/keap/src/tools/contacts-tools.ts b/servers/keap/src/tools/contacts-tools.ts index 0968b23..50fc7a5 100644 --- a/servers/keap/src/tools/contacts-tools.ts +++ b/servers/keap/src/tools/contacts-tools.ts @@ -1,318 +1,423 @@ +import { Tool } from '@modelcontextprotocol/sdk/types.js'; import { KeapClient } from '../clients/keap.js'; +import { KeapContact, ApiResponse } from '../types/index.js'; -export function registerContactsTools(client: KeapClient) { - return { - keap_list_contacts: { - description: 'List contacts with optional filtering', +export function createContactsTools(client: KeapClient): Tool[] { + return [ + { + name: 'keap_create_contact', + description: 'Create a new contact in Keap with email, name, phone, address, tags, and custom fields', inputSchema: { type: 'object', properties: { - limit: { type: 'number', description: 'Number of results per page' }, + given_name: { type: 'string', description: 'First name' }, + family_name: { type: 'string', description: 'Last name' }, + email: { type: 'string', description: 'Primary email address' }, + phone: { type: 'string', description: 'Primary phone number' }, + company_name: { type: 'string', description: 'Company name' }, + job_title: { type: 'string', description: 'Job title' }, + tag_ids: { type: 'array', items: { type: 'number' }, description: 'Array of tag IDs to apply' }, + custom_fields: { type: 'array', items: { type: 'object' }, description: 'Custom field values [{id, content}]' }, + address_line1: { type: 'string', description: 'Street address line 1' }, + address_line2: { type: 'string', description: 'Street address line 2' }, + city: { type: 'string', description: 'City' }, + state: { type: 'string', description: 'State/Region' }, + postal_code: { type: 'string', description: 'Postal/ZIP code' }, + country: { type: 'string', description: 'Country code (e.g., US)' }, + opt_in_reason: { type: 'string', description: 'Reason for opt-in (required for GDPR)' }, + owner_id: { type: 'number', description: 'User ID of contact owner' }, + }, + }, + }, + { + name: 'keap_get_contact', + description: 'Retrieve a contact by ID with all details including tags, custom fields, and company', + inputSchema: { + type: 'object', + properties: { + contact_id: { type: 'number', description: 'Contact ID', required: true }, + optional_properties: { type: 'array', items: { type: 'string' }, description: 'Additional fields to include' }, + }, + required: ['contact_id'], + }, + }, + { + name: 'keap_update_contact', + description: 'Update an existing contact with new information', + inputSchema: { + type: 'object', + properties: { + contact_id: { type: 'number', description: 'Contact ID', required: true }, + given_name: { type: 'string', description: 'First name' }, + family_name: { type: 'string', description: 'Last name' }, + email: { type: 'string', description: 'Primary email address' }, + phone: { type: 'string', description: 'Primary phone number' }, + company_name: { type: 'string', description: 'Company name' }, + job_title: { type: 'string', description: 'Job title' }, + tag_ids: { type: 'array', items: { type: 'number' }, description: 'Array of tag IDs' }, + custom_fields: { type: 'array', items: { type: 'object' }, description: 'Custom field values' }, + owner_id: { type: 'number', description: 'User ID of contact owner' }, + }, + required: ['contact_id'], + }, + }, + { + name: 'keap_delete_contact', + description: 'Permanently delete a contact from Keap', + inputSchema: { + type: 'object', + properties: { + contact_id: { type: 'number', description: 'Contact ID to delete', required: true }, + }, + required: ['contact_id'], + }, + }, + { + name: 'keap_list_contacts', + description: 'List contacts with pagination, filtering, and sorting options', + inputSchema: { + type: 'object', + properties: { + limit: { type: 'number', description: 'Number of results per page (max 200)', default: 50 }, + offset: { type: 'number', description: 'Offset for pagination', default: 0 }, email: { type: 'string', description: 'Filter by email address' }, given_name: { type: 'string', description: 'Filter by first name' }, family_name: { type: 'string', description: 'Filter by last name' }, - order: { type: 'string', description: 'Sort order field' }, + order: { type: 'string', description: 'Field to order by (e.g., date_created, email)', default: 'date_created' }, + order_direction: { type: 'string', enum: ['ascending', 'descending'], description: 'Sort direction' }, + since: { type: 'string', description: 'Filter contacts created/updated after this ISO date' }, + until: { type: 'string', description: 'Filter contacts created/updated before this ISO date' }, }, }, - handler: async (args: any) => { - const contacts = await client.listContacts(args); - return { contacts, count: contacts.length }; - }, }, - - keap_get_contact: { - description: 'Get a specific contact by ID', + { + name: 'keap_search_contacts', + description: 'Search contacts by email, name, phone, or other criteria', inputSchema: { type: 'object', properties: { - contact_id: { type: 'number', description: 'Contact ID' }, + email: { type: 'string', description: 'Search by email address' }, + given_name: { type: 'string', description: 'Search by first name' }, + family_name: { type: 'string', description: 'Search by last name' }, + limit: { type: 'number', description: 'Max results', default: 50 }, + }, + }, + }, + { + name: 'keap_merge_contacts', + description: 'Merge two contacts together, combining all data into one contact', + inputSchema: { + type: 'object', + properties: { + source_contact_id: { type: 'number', description: 'Contact ID to merge from (will be deleted)', required: true }, + target_contact_id: { type: 'number', description: 'Contact ID to merge into (will be kept)', required: true }, + }, + required: ['source_contact_id', 'target_contact_id'], + }, + }, + { + name: 'keap_apply_tag_to_contact', + description: 'Apply one or more tags to a contact', + inputSchema: { + type: 'object', + properties: { + contact_id: { type: 'number', description: 'Contact ID', required: true }, + tag_ids: { type: 'array', items: { type: 'number' }, description: 'Array of tag IDs to apply', required: true }, + }, + required: ['contact_id', 'tag_ids'], + }, + }, + { + name: 'keap_remove_tag_from_contact', + description: 'Remove one or more tags from a contact', + inputSchema: { + type: 'object', + properties: { + contact_id: { type: 'number', description: 'Contact ID', required: true }, + tag_ids: { type: 'array', items: { type: 'number' }, description: 'Array of tag IDs to remove', required: true }, + }, + required: ['contact_id', 'tag_ids'], + }, + }, + { + name: 'keap_get_contact_tags', + description: 'Get all tags applied to a specific contact', + inputSchema: { + type: 'object', + properties: { + contact_id: { type: 'number', description: 'Contact ID', required: true }, }, required: ['contact_id'], }, - handler: async (args: any) => { - return await client.getContact(args.contact_id); - }, }, - - keap_create_contact: { - description: 'Create a new contact', + { + name: 'keap_get_contact_emails', + description: 'Get all email addresses associated with a contact', inputSchema: { type: 'object', properties: { - email: { type: 'string', description: 'Primary email address' }, - given_name: { type: 'string', description: 'First name' }, - family_name: { type: 'string', description: 'Last name' }, - phone: { type: 'string', description: 'Phone number' }, - company_name: { type: 'string', description: 'Company name' }, - job_title: { type: 'string', description: 'Job title' }, - website: { type: 'string', description: 'Website URL' }, - address_line1: { type: 'string', description: 'Street address' }, - address_city: { type: 'string', description: 'City' }, - address_state: { type: 'string', description: 'State/Province' }, - address_zip: { type: 'string', description: 'ZIP/Postal code' }, - address_country: { type: 'string', description: 'Country code' }, - }, - required: ['email'], - }, - handler: async (args: any) => { - const data: any = { - email_addresses: [{ email: args.email, field: 'EMAIL1' }], - given_name: args.given_name, - family_name: args.family_name, - company_name: args.company_name, - job_title: args.job_title, - website: args.website, - }; - - if (args.phone) { - data.phone_numbers = [{ number: args.phone, field: 'PHONE1' }]; - } - - if (args.address_line1 || args.address_city) { - data.addresses = [{ - line1: args.address_line1, - locality: args.address_city, - region: args.address_state, - zip_code: args.address_zip, - country_code: args.address_country || 'US', - field: 'BILLING', - }]; - } - - return await client.createContact(data); - }, - }, - - keap_update_contact: { - description: 'Update an existing contact', - inputSchema: { - type: 'object', - properties: { - contact_id: { type: 'number', description: 'Contact ID' }, - email: { type: 'string', description: 'Email address' }, - given_name: { type: 'string', description: 'First name' }, - family_name: { type: 'string', description: 'Last name' }, - phone: { type: 'string', description: 'Phone number' }, - company_name: { type: 'string', description: 'Company name' }, - job_title: { type: 'string', description: 'Job title' }, - website: { type: 'string', description: 'Website URL' }, + contact_id: { type: 'number', description: 'Contact ID', required: true }, }, required: ['contact_id'], }, - handler: async (args: any) => { - const data: any = { + }, + { + name: 'keap_create_contact_email', + description: 'Add a new email address to a contact', + inputSchema: { + type: 'object', + properties: { + contact_id: { type: 'number', description: 'Contact ID', required: true }, + email: { type: 'string', description: 'Email address', required: true }, + field: { type: 'string', description: 'Field type (EMAIL1, EMAIL2, EMAIL3)', default: 'EMAIL1' }, + }, + required: ['contact_id', 'email'], + }, + }, + { + name: 'keap_delete_contact_email', + description: 'Remove an email address from a contact', + inputSchema: { + type: 'object', + properties: { + contact_id: { type: 'number', description: 'Contact ID', required: true }, + email_id: { type: 'number', description: 'Email ID to remove', required: true }, + }, + required: ['contact_id', 'email_id'], + }, + }, + { + name: 'keap_get_contact_credit_cards', + description: 'Get all credit cards on file for a contact', + inputSchema: { + type: 'object', + properties: { + contact_id: { type: 'number', description: 'Contact ID', required: true }, + }, + required: ['contact_id'], + }, + }, + { + name: 'keap_create_contact_credit_card', + description: 'Add a new credit card to a contact', + inputSchema: { + type: 'object', + properties: { + contact_id: { type: 'number', description: 'Contact ID', required: true }, + card_number: { type: 'string', description: 'Credit card number', required: true }, + expiration_month: { type: 'string', description: 'Expiration month (MM)', required: true }, + expiration_year: { type: 'string', description: 'Expiration year (YYYY)', required: true }, + card_type: { type: 'string', description: 'Card type (Visa, Mastercard, etc.)' }, + name_on_card: { type: 'string', description: 'Name as shown on card' }, + }, + required: ['contact_id', 'card_number', 'expiration_month', 'expiration_year'], + }, + }, + { + name: 'keap_get_contact_custom_fields', + description: 'Retrieve custom field values for a contact', + inputSchema: { + type: 'object', + properties: { + contact_id: { type: 'number', description: 'Contact ID', required: true }, + }, + required: ['contact_id'], + }, + }, + { + name: 'keap_update_contact_custom_field', + description: 'Update a specific custom field value for a contact', + inputSchema: { + type: 'object', + properties: { + contact_id: { type: 'number', description: 'Contact ID', required: true }, + custom_field_id: { type: 'number', description: 'Custom field ID', required: true }, + content: { type: 'string', description: 'New value for the custom field', required: true }, + }, + required: ['contact_id', 'custom_field_id', 'content'], + }, + }, + { + name: 'keap_list_contact_notes', + description: 'Get all notes for a specific contact', + inputSchema: { + type: 'object', + properties: { + contact_id: { type: 'number', description: 'Contact ID', required: true }, + limit: { type: 'number', description: 'Max results', default: 50 }, + offset: { type: 'number', description: 'Pagination offset', default: 0 }, + }, + required: ['contact_id'], + }, + }, + { + name: 'keap_get_contact_model', + description: 'Retrieve the contact model schema including all available custom fields', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + ]; +} + +export async function handleContactsTool( + toolName: string, + args: any, + client: KeapClient +): Promise<{ content: Array<{ type: string; text: string }> }> { + let result: any; + + try { + switch (toolName) { + case 'keap_create_contact': { + const contactData: any = { given_name: args.given_name, family_name: args.family_name, - company_name: args.company_name, - job_title: args.job_title, - website: args.website, + opt_in_reason: args.opt_in_reason, }; if (args.email) { - data.email_addresses = [{ email: args.email, field: 'EMAIL1' }]; + contactData.email_addresses = [{ email: args.email, field: 'EMAIL1' }]; } if (args.phone) { - data.phone_numbers = [{ number: args.phone, field: 'PHONE1' }]; + contactData.phone_numbers = [{ number: args.phone, field: 'PHONE1' }]; } - return await client.updateContact(args.contact_id, data); - }, - }, - - keap_delete_contact: { - description: 'Delete a contact', - inputSchema: { - type: 'object', - properties: { - contact_id: { type: 'number', description: 'Contact ID' }, - }, - required: ['contact_id'], - }, - handler: async (args: any) => { - await client.deleteContact(args.contact_id); - return { success: true, message: `Contact ${args.contact_id} deleted` }; - }, - }, - - keap_search_contacts: { - description: 'Search contacts by keyword', - inputSchema: { - type: 'object', - properties: { - query: { type: 'string', description: 'Search query' }, - limit: { type: 'number', description: 'Maximum results' }, - }, - required: ['query'], - }, - handler: async (args: any) => { - const contacts = await client.listContacts({ - email: args.query.includes('@') ? args.query : undefined, - given_name: !args.query.includes('@') ? args.query : undefined, - limit: args.limit || 100, - }); - return { contacts, count: contacts.length }; - }, - }, - - keap_list_contact_tags: { - description: 'List all tags applied to a contact', - inputSchema: { - type: 'object', - properties: { - contact_id: { type: 'number', description: 'Contact ID' }, - }, - required: ['contact_id'], - }, - handler: async (args: any) => { - const response = await client.get(`/contacts/${args.contact_id}/tags`); - return { tags: response.tags || [] }; - }, - }, - - keap_add_tag_to_contact: { - description: 'Add a tag to a contact', - inputSchema: { - type: 'object', - properties: { - contact_id: { type: 'number', description: 'Contact ID' }, - tag_id: { type: 'number', description: 'Tag ID' }, - }, - required: ['contact_id', 'tag_id'], - }, - handler: async (args: any) => { - await client.post(`/contacts/${args.contact_id}/tags`, { tagId: args.tag_id }); - return { success: true, message: `Tag ${args.tag_id} added to contact ${args.contact_id}` }; - }, - }, - - keap_remove_tag_from_contact: { - description: 'Remove a tag from a contact', - inputSchema: { - type: 'object', - properties: { - contact_id: { type: 'number', description: 'Contact ID' }, - tag_id: { type: 'number', description: 'Tag ID' }, - }, - required: ['contact_id', 'tag_id'], - }, - handler: async (args: any) => { - await client.delete(`/contacts/${args.contact_id}/tags/${args.tag_id}`); - return { success: true, message: `Tag ${args.tag_id} removed from contact ${args.contact_id}` }; - }, - }, - - keap_list_contact_emails: { - description: 'List all emails sent to/from a contact', - inputSchema: { - type: 'object', - properties: { - contact_id: { type: 'number', description: 'Contact ID' }, - limit: { type: 'number', description: 'Maximum results' }, - }, - required: ['contact_id'], - }, - handler: async (args: any) => { - const emails = await client.listEmails({ - contact_id: args.contact_id, - limit: args.limit || 100, - }); - return { emails, count: emails.length }; - }, - }, - - keap_create_contact_email: { - description: 'Create an email record for a contact', - inputSchema: { - type: 'object', - properties: { - contact_id: { type: 'number', description: 'Contact ID' }, - subject: { type: 'string', description: 'Email subject' }, - html_content: { type: 'string', description: 'HTML email body' }, - text_content: { type: 'string', description: 'Plain text email body' }, - from_address: { type: 'string', description: 'From email address' }, - }, - required: ['contact_id', 'subject'], - }, - handler: async (args: any) => { - const contact = await client.getContact(args.contact_id); - const toAddress = contact.email_addresses?.[0]?.email; - - if (!toAddress) { - throw new Error(`Contact ${args.contact_id} has no email address`); + if (args.address_line1 || args.city) { + contactData.addresses = [{ + line1: args.address_line1, + line2: args.address_line2, + locality: args.city, + region: args.state, + postal_code: args.postal_code, + country_code: args.country || 'US', + }]; } - return await client.createEmail({ - sent_to_address: toAddress, - sent_to_contact_id: args.contact_id, - sent_from_address: args.from_address, - subject: args.subject, - html_content: args.html_content, - text_content: args.text_content, - }); - }, - }, + if (args.company_name) { + contactData.company = { company_name: args.company_name }; + } - keap_list_contact_notes: { - description: 'List all notes for a contact', - inputSchema: { - type: 'object', - properties: { - contact_id: { type: 'number', description: 'Contact ID' }, - limit: { type: 'number', description: 'Maximum results' }, - }, - required: ['contact_id'], - }, - handler: async (args: any) => { - const notes = await client.listNotes({ - contact_id: args.contact_id, - limit: args.limit || 100, - }); - return { notes, count: notes.length }; - }, - }, + if (args.job_title) contactData.job_title = args.job_title; + if (args.tag_ids) contactData.tag_ids = args.tag_ids; + if (args.custom_fields) contactData.custom_fields = args.custom_fields; + if (args.owner_id) contactData.owner_id = args.owner_id; - keap_create_contact_note: { - description: 'Create a note for a contact', - inputSchema: { - type: 'object', - properties: { - contact_id: { type: 'number', description: 'Contact ID' }, - title: { type: 'string', description: 'Note title' }, - body: { type: 'string', description: 'Note content' }, - type: { type: 'string', enum: ['Appointment', 'Call', 'Email', 'Fax', 'Letter', 'Other'], description: 'Note type' }, - }, - required: ['contact_id', 'body'], - }, - handler: async (args: any) => { - return await client.createNote({ - contact_id: args.contact_id, - title: args.title, - body: args.body, - type: args.type || 'Other', - }); - }, - }, + result = await client.post('/contacts', contactData); + break; + } - keap_merge_contacts: { - description: 'Merge two contacts (keeps first, merges second into it)', - inputSchema: { - type: 'object', - properties: { - keep_contact_id: { type: 'number', description: 'Contact ID to keep' }, - merge_contact_id: { type: 'number', description: 'Contact ID to merge and delete' }, - }, - required: ['keep_contact_id', 'merge_contact_id'], - }, - handler: async (args: any) => { - await client.post(`/contacts/${args.keep_contact_id}/merge`, { - duplicateContactId: args.merge_contact_id, + case 'keap_get_contact': + result = await client.get(`/contacts/${args.contact_id}`, { + optional_properties: args.optional_properties?.join(','), }); - return { - success: true, - message: `Contact ${args.merge_contact_id} merged into ${args.keep_contact_id}` - }; - }, - }, - }; + break; + + case 'keap_update_contact': { + const { contact_id, ...updateData } = args; + result = await client.patch(`/contacts/${contact_id}`, updateData); + break; + } + + case 'keap_delete_contact': + await client.delete(`/contacts/${args.contact_id}`); + result = { success: true, message: 'Contact deleted successfully' }; + break; + + case 'keap_list_contacts': + result = await client.get('/contacts', args); + break; + + case 'keap_search_contacts': + result = await client.get('/contacts', args); + break; + + case 'keap_merge_contacts': + result = await client.post(`/contacts/${args.target_contact_id}/merge`, { + duplicate_contact_id: args.source_contact_id, + }); + break; + + case 'keap_apply_tag_to_contact': + result = await client.post(`/contacts/${args.contact_id}/tags`, { + tagIds: args.tag_ids, + }); + break; + + case 'keap_remove_tag_from_contact': + for (const tagId of args.tag_ids) { + await client.delete(`/contacts/${args.contact_id}/tags/${tagId}`); + } + result = { success: true, message: 'Tags removed successfully' }; + break; + + case 'keap_get_contact_tags': + result = await client.get(`/contacts/${args.contact_id}/tags`); + break; + + case 'keap_get_contact_emails': + result = await client.get(`/contacts/${args.contact_id}/emails`); + break; + + case 'keap_create_contact_email': + result = await client.post(`/contacts/${args.contact_id}/emails`, { + email: args.email, + field: args.field || 'EMAIL1', + }); + break; + + case 'keap_delete_contact_email': + await client.delete(`/contacts/${args.contact_id}/emails/${args.email_id}`); + result = { success: true, message: 'Email deleted successfully' }; + break; + + case 'keap_get_contact_credit_cards': + result = await client.get(`/contacts/${args.contact_id}/creditCards`); + break; + + case 'keap_create_contact_credit_card': + result = await client.post(`/contacts/${args.contact_id}/creditCards`, { + card_number: args.card_number, + expiration_month: args.expiration_month, + expiration_year: args.expiration_year, + card_type: args.card_type, + name_on_card: args.name_on_card, + }); + break; + + case 'keap_get_contact_custom_fields': + result = await client.get(`/contacts/${args.contact_id}?optional_properties=custom_fields`); + break; + + case 'keap_update_contact_custom_field': + result = await client.patch(`/contacts/${args.contact_id}`, { + custom_fields: [{ id: args.custom_field_id, content: args.content }], + }); + break; + + case 'keap_list_contact_notes': + result = await client.get(`/contacts/${args.contact_id}/notes`, { + limit: args.limit, + offset: args.offset, + }); + break; + + case 'keap_get_contact_model': + result = await client.get('/contacts/model'); + break; + + default: + throw new Error(`Unknown tool: ${toolName}`); + } + + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], + }; + } catch (error: any) { + return { + content: [{ type: 'text', text: `Error: ${error.message}` }], + }; + } } diff --git a/servers/keap/src/tools/deals-tools.ts b/servers/keap/src/tools/deals-tools.ts deleted file mode 100644 index 60a0d22..0000000 --- a/servers/keap/src/tools/deals-tools.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { KeapClient } from '../clients/keap.js'; - -export function registerDealsTools(client: KeapClient) { - return { - keap_list_deals: { - description: 'List all deals/opportunities', - inputSchema: { - type: 'object', - properties: { - stage_id: { type: 'number', description: 'Filter by stage ID' }, - user_id: { type: 'number', description: 'Filter by assigned user ID' }, - contact_id: { type: 'number', description: 'Filter by contact ID' }, - limit: { type: 'number', description: 'Maximum results' }, - order: { type: 'string', description: 'Sort order' }, - }, - }, - handler: async (args: any) => { - const deals = await client.listDeals(args); - return { deals, count: deals.length }; - }, - }, - - keap_get_deal: { - description: 'Get a specific deal by ID', - inputSchema: { - type: 'object', - properties: { - deal_id: { type: 'number', description: 'Deal ID' }, - }, - required: ['deal_id'], - }, - handler: async (args: any) => { - return await client.getDeal(args.deal_id); - }, - }, - - keap_create_deal: { - description: 'Create a new deal/opportunity', - inputSchema: { - type: 'object', - properties: { - title: { type: 'string', description: 'Deal title' }, - contact_id: { type: 'number', description: 'Contact ID' }, - stage_id: { type: 'number', description: 'Pipeline stage ID' }, - user_id: { type: 'number', description: 'Assigned user ID' }, - projected_revenue_low: { type: 'number', description: 'Minimum projected revenue' }, - projected_revenue_high: { type: 'number', description: 'Maximum projected revenue' }, - estimated_close_date: { type: 'string', description: 'Estimated close date (ISO 8601)' }, - probability: { type: 'number', description: 'Win probability (0-100)' }, - next_action_date: { type: 'string', description: 'Next action date (ISO 8601)' }, - next_action_notes: { type: 'string', description: 'Next action notes' }, - }, - required: ['title', 'stage_id'], - }, - handler: async (args: any) => { - return await client.createDeal({ - title: args.title, - contact: args.contact_id ? { id: args.contact_id } : undefined, - stage_id: args.stage_id, - user_id: args.user_id, - projected_revenue_low: args.projected_revenue_low, - projected_revenue_high: args.projected_revenue_high, - estimated_close_date: args.estimated_close_date, - probability: args.probability, - next_action_date: args.next_action_date, - next_action_notes: args.next_action_notes, - }); - }, - }, - - keap_update_deal: { - description: 'Update an existing deal', - inputSchema: { - type: 'object', - properties: { - deal_id: { type: 'number', description: 'Deal ID' }, - title: { type: 'string', description: 'Deal title' }, - stage_id: { type: 'number', description: 'Pipeline stage ID' }, - user_id: { type: 'number', description: 'Assigned user ID' }, - projected_revenue_low: { type: 'number', description: 'Minimum projected revenue' }, - projected_revenue_high: { type: 'number', description: 'Maximum projected revenue' }, - estimated_close_date: { type: 'string', description: 'Estimated close date (ISO 8601)' }, - probability: { type: 'number', description: 'Win probability (0-100)' }, - next_action_date: { type: 'string', description: 'Next action date (ISO 8601)' }, - next_action_notes: { type: 'string', description: 'Next action notes' }, - }, - required: ['deal_id'], - }, - handler: async (args: any) => { - const data: any = {}; - if (args.title !== undefined) data.title = args.title; - if (args.stage_id !== undefined) data.stage_id = args.stage_id; - if (args.user_id !== undefined) data.user_id = args.user_id; - if (args.projected_revenue_low !== undefined) data.projected_revenue_low = args.projected_revenue_low; - if (args.projected_revenue_high !== undefined) data.projected_revenue_high = args.projected_revenue_high; - if (args.estimated_close_date !== undefined) data.estimated_close_date = args.estimated_close_date; - if (args.probability !== undefined) data.probability = args.probability; - if (args.next_action_date !== undefined) data.next_action_date = args.next_action_date; - if (args.next_action_notes !== undefined) data.next_action_notes = args.next_action_notes; - - return await client.updateDeal(args.deal_id, data); - }, - }, - - keap_delete_deal: { - description: 'Delete a deal', - inputSchema: { - type: 'object', - properties: { - deal_id: { type: 'number', description: 'Deal ID' }, - }, - required: ['deal_id'], - }, - handler: async (args: any) => { - await client.deleteDeal(args.deal_id); - return { success: true, message: `Deal ${args.deal_id} deleted` }; - }, - }, - - keap_move_deal_stage: { - description: 'Move a deal to a different pipeline stage', - inputSchema: { - type: 'object', - properties: { - deal_id: { type: 'number', description: 'Deal ID' }, - stage_id: { type: 'number', description: 'New stage ID' }, - reason: { type: 'string', description: 'Reason for move (optional note)' }, - }, - required: ['deal_id', 'stage_id'], - }, - handler: async (args: any) => { - const data: any = { stage_id: args.stage_id }; - if (args.reason) { - data.next_action_notes = args.reason; - } - - return await client.updateDeal(args.deal_id, data); - }, - }, - - keap_list_pipelines: { - description: 'List all sales pipelines', - inputSchema: { - type: 'object', - properties: {}, - }, - handler: async () => { - const pipelines = await client.get('/opportunity/stage_pipeline'); - return { pipelines: pipelines.pipelines || [] }; - }, - }, - - keap_get_pipeline: { - description: 'Get a specific pipeline with all its stages', - inputSchema: { - type: 'object', - properties: { - pipeline_id: { type: 'number', description: 'Pipeline ID' }, - }, - required: ['pipeline_id'], - }, - handler: async (args: any) => { - const pipeline = await client.get(`/opportunity/stage_pipeline/${args.pipeline_id}`); - return pipeline; - }, - }, - - keap_list_stages: { - description: 'List all stages across all pipelines', - inputSchema: { - type: 'object', - properties: { - pipeline_id: { type: 'number', description: 'Filter by pipeline ID' }, - }, - }, - handler: async (args: any) => { - if (args.pipeline_id) { - const pipeline = await client.get(`/opportunity/stage_pipeline/${args.pipeline_id}`); - return { stages: pipeline.stages || [] }; - } else { - const pipelines = await client.get('/opportunity/stage_pipeline'); - const allStages = pipelines.pipelines?.flatMap((p: any) => p.stages || []) || []; - return { stages: allStages }; - } - }, - }, - }; -} diff --git a/servers/keap/src/tools/ecommerce-tools.ts b/servers/keap/src/tools/ecommerce-tools.ts new file mode 100644 index 0000000..295411b --- /dev/null +++ b/servers/keap/src/tools/ecommerce-tools.ts @@ -0,0 +1,302 @@ +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import { KeapClient } from '../clients/keap.js'; + +export function createEcommerceTools(client: KeapClient): Tool[] { + return [ + // Products + { + name: 'keap_create_product', + description: 'Create a new product in Keap', + inputSchema: { + type: 'object', + properties: { + product_name: { type: 'string', description: 'Product name', required: true }, + product_short_desc: { type: 'string', description: 'Short description' }, + product_desc: { type: 'string', description: 'Full description' }, + product_price: { type: 'number', description: 'Product price', required: true }, + sku: { type: 'string', description: 'SKU code' }, + subscription_only: { type: 'boolean', description: 'Is subscription-only product', default: false }, + url: { type: 'string', description: 'Product URL' }, + }, + required: ['product_name', 'product_price'], + }, + }, + { + name: 'keap_get_product', + description: 'Retrieve a product by ID', + inputSchema: { + type: 'object', + properties: { + product_id: { type: 'number', description: 'Product ID', required: true }, + }, + required: ['product_id'], + }, + }, + { + name: 'keap_update_product', + description: 'Update an existing product', + inputSchema: { + type: 'object', + properties: { + product_id: { type: 'number', description: 'Product ID', required: true }, + product_name: { type: 'string', description: 'Product name' }, + product_price: { type: 'number', description: 'Product price' }, + sku: { type: 'string', description: 'SKU code' }, + status: { type: 'number', description: 'Status (0=inactive, 1=active)' }, + }, + required: ['product_id'], + }, + }, + { + name: 'keap_delete_product', + description: 'Delete a product', + inputSchema: { + type: 'object', + properties: { + product_id: { type: 'number', description: 'Product ID to delete', required: true }, + }, + required: ['product_id'], + }, + }, + { + name: 'keap_list_products', + description: 'List all products with pagination', + inputSchema: { + type: 'object', + properties: { + limit: { type: 'number', description: 'Results per page', default: 50 }, + offset: { type: 'number', description: 'Pagination offset', default: 0 }, + active: { type: 'boolean', description: 'Filter by active status' }, + }, + }, + }, + // Orders + { + name: 'keap_create_order', + description: 'Create a new order in Keap', + inputSchema: { + type: 'object', + properties: { + contact_id: { type: 'number', description: 'Contact ID', required: true }, + order_title: { type: 'string', description: 'Order title', required: true }, + order_type: { type: 'string', description: 'Order type (Online, Offline)', default: 'Online' }, + order_items: { type: 'array', items: { type: 'object' }, description: 'Array of order items', required: true }, + promo_codes: { type: 'array', items: { type: 'string' }, description: 'Promo codes to apply' }, + }, + required: ['contact_id', 'order_title', 'order_items'], + }, + }, + { + name: 'keap_get_order', + description: 'Retrieve an order by ID', + inputSchema: { + type: 'object', + properties: { + order_id: { type: 'number', description: 'Order ID', required: true }, + }, + required: ['order_id'], + }, + }, + { + name: 'keap_delete_order', + description: 'Delete an order', + inputSchema: { + type: 'object', + properties: { + order_id: { type: 'number', description: 'Order ID to delete', required: true }, + }, + required: ['order_id'], + }, + }, + { + name: 'keap_list_orders', + description: 'List orders with filtering', + inputSchema: { + type: 'object', + properties: { + contact_id: { type: 'number', description: 'Filter by contact' }, + product_id: { type: 'number', description: 'Filter by product' }, + limit: { type: 'number', description: 'Results per page', default: 50 }, + offset: { type: 'number', description: 'Pagination offset', default: 0 }, + since: { type: 'string', description: 'Orders after this date' }, + until: { type: 'string', description: 'Orders before this date' }, + paid: { type: 'boolean', description: 'Filter by paid status' }, + }, + }, + }, + { + name: 'keap_list_order_transactions', + description: 'Get all transactions for an order', + inputSchema: { + type: 'object', + properties: { + order_id: { type: 'number', description: 'Order ID', required: true }, + }, + required: ['order_id'], + }, + }, + // Transactions + { + name: 'keap_get_transaction', + description: 'Retrieve a transaction by ID', + inputSchema: { + type: 'object', + properties: { + transaction_id: { type: 'number', description: 'Transaction ID', required: true }, + }, + required: ['transaction_id'], + }, + }, + { + name: 'keap_list_transactions', + description: 'List transactions with filtering', + inputSchema: { + type: 'object', + properties: { + contact_id: { type: 'number', description: 'Filter by contact' }, + limit: { type: 'number', description: 'Results per page', default: 50 }, + offset: { type: 'number', description: 'Pagination offset', default: 0 }, + since: { type: 'string', description: 'Transactions after this date' }, + until: { type: 'string', description: 'Transactions before this date' }, + }, + }, + }, + // Subscriptions + { + name: 'keap_create_subscription', + description: 'Create a subscription for a contact', + inputSchema: { + type: 'object', + properties: { + contact_id: { type: 'number', description: 'Contact ID', required: true }, + product_id: { type: 'number', description: 'Product ID', required: true }, + subscription_plan_id: { type: 'number', description: 'Subscription plan ID', required: true }, + quantity: { type: 'number', description: 'Quantity', default: 1 }, + billing_amount: { type: 'number', description: 'Billing amount' }, + credit_card_id: { type: 'number', description: 'Credit card ID for payment' }, + }, + required: ['contact_id', 'product_id', 'subscription_plan_id'], + }, + }, + { + name: 'keap_get_subscription', + description: 'Retrieve a subscription by ID', + inputSchema: { + type: 'object', + properties: { + subscription_id: { type: 'number', description: 'Subscription ID', required: true }, + }, + required: ['subscription_id'], + }, + }, + { + name: 'keap_list_subscriptions', + description: 'List subscriptions with filtering', + inputSchema: { + type: 'object', + properties: { + contact_id: { type: 'number', description: 'Filter by contact' }, + active: { type: 'boolean', description: 'Filter by active status' }, + limit: { type: 'number', description: 'Results per page', default: 50 }, + }, + }, + }, + ]; +} + +export async function handleEcommerceTool( + toolName: string, + args: any, + client: KeapClient +): Promise<{ content: Array<{ type: string; text: string }> }> { + let result: any; + + try { + switch (toolName) { + // Products + case 'keap_create_product': + result = await client.post('/products', args); + break; + + case 'keap_get_product': + result = await client.get(`/products/${args.product_id}`); + break; + + case 'keap_update_product': { + const { product_id, ...updateData } = args; + result = await client.patch(`/products/${product_id}`, updateData); + break; + } + + case 'keap_delete_product': + await client.delete(`/products/${args.product_id}`); + result = { success: true, message: 'Product deleted successfully' }; + break; + + case 'keap_list_products': + result = await client.get('/products', args); + break; + + // Orders + case 'keap_create_order': + result = await client.post('/orders', { + contact_id: args.contact_id, + order_title: args.order_title, + order_type: args.order_type || 'Online', + order_items: args.order_items, + promo_codes: args.promo_codes, + }); + break; + + case 'keap_get_order': + result = await client.get(`/orders/${args.order_id}`); + break; + + case 'keap_delete_order': + await client.delete(`/orders/${args.order_id}`); + result = { success: true, message: 'Order deleted successfully' }; + break; + + case 'keap_list_orders': + result = await client.get('/orders', args); + break; + + case 'keap_list_order_transactions': + result = await client.get(`/orders/${args.order_id}/transactions`); + break; + + // Transactions + case 'keap_get_transaction': + result = await client.get(`/transactions/${args.transaction_id}`); + break; + + case 'keap_list_transactions': + result = await client.get('/transactions', args); + break; + + // Subscriptions + case 'keap_create_subscription': + result = await client.post('/subscriptions', args); + break; + + case 'keap_get_subscription': + result = await client.get(`/subscriptions/${args.subscription_id}`); + break; + + case 'keap_list_subscriptions': + result = await client.get('/subscriptions', args); + break; + + default: + throw new Error(`Unknown tool: ${toolName}`); + } + + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], + }; + } catch (error: any) { + return { + content: [{ type: 'text', text: `Error: ${error.message}` }], + }; + } +} diff --git a/servers/keap/src/tools/emails-tools.ts b/servers/keap/src/tools/emails-tools.ts index fa1795b..1d861fc 100644 --- a/servers/keap/src/tools/emails-tools.ts +++ b/servers/keap/src/tools/emails-tools.ts @@ -1,126 +1,165 @@ +import { Tool } from '@modelcontextprotocol/sdk/types.js'; import { KeapClient } from '../clients/keap.js'; -export function registerEmailsTools(client: KeapClient) { - return { - keap_list_emails: { - description: 'List emails', +export function createEmailsTools(client: KeapClient): Tool[] { + return [ + { + name: 'keap_send_email', + description: 'Send an email to one or more contacts', inputSchema: { type: 'object', properties: { - contact_id: { type: 'number', description: 'Filter by contact ID' }, - since: { type: 'string', description: 'Start date filter (ISO 8601)' }, - until: { type: 'string', description: 'End date filter (ISO 8601)' }, - limit: { type: 'number', description: 'Maximum results' }, + contacts: { type: 'array', items: { type: 'number' }, description: 'Array of contact IDs', required: true }, + subject: { type: 'string', description: 'Email subject', required: true }, + html_content: { type: 'string', description: 'HTML content of email' }, + text_content: { type: 'string', description: 'Plain text content of email' }, + from_address: { type: 'string', description: 'From email address' }, + reply_to_address: { type: 'string', description: 'Reply-to email address' }, + attachments: { type: 'array', items: { type: 'object' }, description: 'Email attachments' }, }, - }, - handler: async (args: any) => { - const emails = await client.listEmails(args); - return { emails, count: emails.length }; + required: ['contacts', 'subject'], }, }, - - keap_get_email: { - description: 'Get a specific email by ID', + { + name: 'keap_get_email', + description: 'Retrieve an email by ID', inputSchema: { type: 'object', properties: { - email_id: { type: 'number', description: 'Email ID' }, + email_id: { type: 'number', description: 'Email ID', required: true }, }, required: ['email_id'], }, - handler: async (args: any) => { - return await client.getEmail(args.email_id); - }, }, - - keap_create_email: { - description: 'Create an email record', + { + name: 'keap_list_emails', + description: 'List emails with filtering', inputSchema: { type: 'object', properties: { - to_address: { type: 'string', description: 'Recipient email address' }, - from_address: { type: 'string', description: 'Sender email address' }, - subject: { type: 'string', description: 'Email subject' }, - html_content: { type: 'string', description: 'HTML email body' }, - text_content: { type: 'string', description: 'Plain text email body' }, - contact_id: { type: 'number', description: 'Contact ID' }, - }, - required: ['to_address', 'from_address', 'subject'], - }, - handler: async (args: any) => { - return await client.createEmail({ - sent_to_address: args.to_address, - sent_to_contact_id: args.contact_id, - sent_from_address: args.from_address, - subject: args.subject, - html_content: args.html_content, - text_content: args.text_content, - }); - }, - }, - - keap_send_email: { - description: 'Send an email to a contact', - inputSchema: { - type: 'object', - properties: { - to_address: { type: 'string', description: 'Recipient email address' }, - from_address: { type: 'string', description: 'Sender email address' }, - subject: { type: 'string', description: 'Email subject' }, - html_content: { type: 'string', description: 'HTML email body' }, - text_content: { type: 'string', description: 'Plain text email body' }, - contact_id: { type: 'number', description: 'Contact ID' }, - }, - required: ['to_address', 'from_address', 'subject'], - }, - handler: async (args: any) => { - return await client.sendEmail({ - sent_to_address: args.to_address, - sent_to_contact_id: args.contact_id, - sent_from_address: args.from_address, - subject: args.subject, - html_content: args.html_content, - text_content: args.text_content, - }); - }, - }, - - keap_get_email_templates: { - description: 'Get all email templates', - inputSchema: { - type: 'object', - properties: { - limit: { type: 'number', description: 'Maximum results' }, + contact_id: { type: 'number', description: 'Filter by contact' }, + limit: { type: 'number', description: 'Results per page', default: 50 }, + offset: { type: 'number', description: 'Pagination offset', default: 0 }, + since: { type: 'string', description: 'Emails sent after this date' }, + until: { type: 'string', description: 'Emails sent before this date' }, }, }, - handler: async (args: any) => { - const templates = await client.get('/emails/templates', args); - return { templates: templates.templates || [] }; - }, }, - - keap_create_email_template: { - description: 'Create a new email template', + { + name: 'keap_create_email_template', + description: 'Create an email template', inputSchema: { type: 'object', properties: { - name: { type: 'string', description: 'Template name' }, - subject: { type: 'string', description: 'Email subject' }, - html_content: { type: 'string', description: 'HTML email body' }, - text_content: { type: 'string', description: 'Plain text email body' }, - categories: { type: 'array', items: { type: 'string' }, description: 'Template categories' }, + name: { type: 'string', description: 'Template name', required: true }, + subject: { type: 'string', description: 'Email subject', required: true }, + html_content: { type: 'string', description: 'HTML content' }, + text_content: { type: 'string', description: 'Plain text content' }, }, required: ['name', 'subject'], }, - handler: async (args: any) => { - return await client.post('/emails/templates', { + }, + { + name: 'keap_list_email_templates', + description: 'List all email templates', + inputSchema: { + type: 'object', + properties: { + limit: { type: 'number', description: 'Results per page', default: 50 }, + }, + }, + }, + { + name: 'keap_opt_in_contact', + description: 'Opt in a contact to email communications', + inputSchema: { + type: 'object', + properties: { + email: { type: 'string', description: 'Email address', required: true }, + opt_in_reason: { type: 'string', description: 'Reason for opt-in', required: true }, + }, + required: ['email', 'opt_in_reason'], + }, + }, + { + name: 'keap_opt_out_contact', + description: 'Opt out a contact from email communications', + inputSchema: { + type: 'object', + properties: { + email: { type: 'string', description: 'Email address', required: true }, + }, + required: ['email'], + }, + }, + ]; +} + +export async function handleEmailsTool( + toolName: string, + args: any, + client: KeapClient +): Promise<{ content: Array<{ type: string; text: string }> }> { + let result: any; + + try { + switch (toolName) { + case 'keap_send_email': + result = await client.post('/emails', { + contacts: args.contacts, + subject: args.subject, + html_content: args.html_content, + text_content: args.text_content, + sent_from_address: args.from_address, + sent_from_reply_address: args.reply_to_address, + attachments: args.attachments, + }); + break; + + case 'keap_get_email': + result = await client.get(`/emails/${args.email_id}`); + break; + + case 'keap_list_emails': + result = await client.get('/emails', args); + break; + + case 'keap_create_email_template': + result = await client.post('/emails/templates', { name: args.name, subject: args.subject, html_content: args.html_content, text_content: args.text_content, - categories: args.categories, }); - }, - }, - }; + break; + + case 'keap_list_email_templates': + result = await client.get('/emails/templates', { limit: args.limit }); + break; + + case 'keap_opt_in_contact': + result = await client.post('/emails/optIn', { + email: args.email, + opt_in_reason: args.opt_in_reason, + }); + break; + + case 'keap_opt_out_contact': + result = await client.post('/emails/optOut', { + email: args.email, + }); + break; + + default: + throw new Error(`Unknown tool: ${toolName}`); + } + + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], + }; + } catch (error: any) { + return { + content: [{ type: 'text', text: `Error: ${error.message}` }], + }; + } } diff --git a/servers/keap/src/tools/files-tools.ts b/servers/keap/src/tools/files-tools.ts new file mode 100644 index 0000000..658dce4 --- /dev/null +++ b/servers/keap/src/tools/files-tools.ts @@ -0,0 +1,100 @@ +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import { KeapClient } from '../clients/keap.js'; + +export function createFilesTools(client: KeapClient): Tool[] { + return [ + { + name: 'keap_upload_file', + description: 'Upload a file to Keap', + inputSchema: { + type: 'object', + properties: { + file_name: { type: 'string', description: 'Name of the file', required: true }, + file_data: { type: 'string', description: 'Base64 encoded file data', required: true }, + contact_id: { type: 'number', description: 'Associate with contact ID' }, + is_public: { type: 'boolean', description: 'Make file publicly accessible', default: false }, + }, + required: ['file_name', 'file_data'], + }, + }, + { + name: 'keap_get_file', + description: 'Retrieve file metadata by ID', + inputSchema: { + type: 'object', + properties: { + file_id: { type: 'number', description: 'File ID', required: true }, + }, + required: ['file_id'], + }, + }, + { + name: 'keap_delete_file', + description: 'Delete a file from Keap', + inputSchema: { + type: 'object', + properties: { + file_id: { type: 'number', description: 'File ID to delete', required: true }, + }, + required: ['file_id'], + }, + }, + { + name: 'keap_list_files', + description: 'List files with filtering', + inputSchema: { + type: 'object', + properties: { + contact_id: { type: 'number', description: 'Filter by contact' }, + limit: { type: 'number', description: 'Results per page', default: 50 }, + offset: { type: 'number', description: 'Pagination offset', default: 0 }, + }, + }, + }, + ]; +} + +export async function handleFilesTool( + toolName: string, + args: any, + client: KeapClient +): Promise<{ content: Array<{ type: string; text: string }> }> { + let result: any; + + try { + switch (toolName) { + case 'keap_upload_file': + result = await client.post('/files', { + file_name: args.file_name, + file_data: args.file_data, + contact_id: args.contact_id, + is_public: args.is_public || false, + }); + break; + + case 'keap_get_file': + result = await client.get(`/files/${args.file_id}`); + break; + + case 'keap_delete_file': + await client.delete(`/files/${args.file_id}`); + result = { success: true, message: 'File deleted successfully' }; + break; + + case 'keap_list_files': + result = await client.get('/files', args); + break; + + default: + throw new Error(`Unknown tool: ${toolName}`); + } + + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], + }; + } catch (error: any) { + return { + content: [{ type: 'text', text: `Error: ${error.message}` }], + }; + } +} diff --git a/servers/keap/src/tools/notes-tools.ts b/servers/keap/src/tools/notes-tools.ts new file mode 100644 index 0000000..921c522 --- /dev/null +++ b/servers/keap/src/tools/notes-tools.ts @@ -0,0 +1,135 @@ +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import { KeapClient } from '../clients/keap.js'; + +export function createNotesTools(client: KeapClient): Tool[] { + return [ + { + name: 'keap_create_note', + description: 'Create a note for a contact', + inputSchema: { + type: 'object', + properties: { + contact_id: { type: 'number', description: 'Contact ID', required: true }, + title: { type: 'string', description: 'Note title' }, + body: { type: 'string', description: 'Note content', required: true }, + type: { type: 'string', description: 'Note type (Appointment, Call, Email, etc.)' }, + user_id: { type: 'number', description: 'User ID who created the note' }, + }, + required: ['contact_id', 'body'], + }, + }, + { + name: 'keap_get_note', + description: 'Retrieve a note by ID', + inputSchema: { + type: 'object', + properties: { + note_id: { type: 'number', description: 'Note ID', required: true }, + }, + required: ['note_id'], + }, + }, + { + name: 'keap_update_note', + description: 'Update an existing note', + inputSchema: { + type: 'object', + properties: { + note_id: { type: 'number', description: 'Note ID', required: true }, + title: { type: 'string', description: 'Note title' }, + body: { type: 'string', description: 'Note content' }, + type: { type: 'string', description: 'Note type' }, + }, + required: ['note_id'], + }, + }, + { + name: 'keap_delete_note', + description: 'Delete a note', + inputSchema: { + type: 'object', + properties: { + note_id: { type: 'number', description: 'Note ID to delete', required: true }, + }, + required: ['note_id'], + }, + }, + { + name: 'keap_list_notes', + description: 'List notes for a contact', + inputSchema: { + type: 'object', + properties: { + contact_id: { type: 'number', description: 'Contact ID' }, + user_id: { type: 'number', description: 'Filter by user who created notes' }, + limit: { type: 'number', description: 'Results per page', default: 50 }, + offset: { type: 'number', description: 'Pagination offset', default: 0 }, + }, + }, + }, + { + name: 'keap_get_note_model', + description: 'Get the note model schema', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + ]; +} + +export async function handleNotesTool( + toolName: string, + args: any, + client: KeapClient +): Promise<{ content: Array<{ type: string; text: string }> }> { + let result: any; + + try { + switch (toolName) { + case 'keap_create_note': + result = await client.post('/notes', { + contact_id: args.contact_id, + title: args.title, + body: args.body, + type: args.type, + user_id: args.user_id, + }); + break; + + case 'keap_get_note': + result = await client.get(`/notes/${args.note_id}`); + break; + + case 'keap_update_note': { + const { note_id, ...updateData } = args; + result = await client.patch(`/notes/${note_id}`, updateData); + break; + } + + case 'keap_delete_note': + await client.delete(`/notes/${args.note_id}`); + result = { success: true, message: 'Note deleted successfully' }; + break; + + case 'keap_list_notes': + result = await client.get('/notes', args); + break; + + case 'keap_get_note_model': + result = await client.get('/notes/model'); + break; + + default: + throw new Error(`Unknown tool: ${toolName}`); + } + + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], + }; + } catch (error: any) { + return { + content: [{ type: 'text', text: `Error: ${error.message}` }], + }; + } +} diff --git a/servers/keap/src/tools/opportunities-tools.ts b/servers/keap/src/tools/opportunities-tools.ts new file mode 100644 index 0000000..786c4e3 --- /dev/null +++ b/servers/keap/src/tools/opportunities-tools.ts @@ -0,0 +1,206 @@ +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import { KeapClient } from '../clients/keap.js'; + +export function createOpportunitiesTools(client: KeapClient): Tool[] { + return [ + { + name: 'keap_create_opportunity', + description: 'Create a new sales opportunity/deal in Keap', + inputSchema: { + type: 'object', + properties: { + opportunity_title: { type: 'string', description: 'Deal/opportunity title', required: true }, + contact_id: { type: 'number', description: 'Contact ID associated with this opportunity', required: true }, + stage_id: { type: 'number', description: 'Pipeline stage ID', required: true }, + user_id: { type: 'number', description: 'User ID (owner of opportunity)' }, + estimated_close_date: { type: 'string', description: 'Estimated close date (ISO format)' }, + projected_revenue_low: { type: 'number', description: 'Low revenue estimate' }, + projected_revenue_high: { type: 'number', description: 'High revenue estimate' }, + opportunity_notes: { type: 'string', description: 'Notes about this opportunity' }, + next_action_notes: { type: 'string', description: 'Next action notes' }, + next_action_date: { type: 'string', description: 'Next action date (ISO format)' }, + custom_fields: { type: 'array', items: { type: 'object' }, description: 'Custom fields' }, + }, + required: ['opportunity_title', 'contact_id', 'stage_id'], + }, + }, + { + name: 'keap_get_opportunity', + description: 'Retrieve an opportunity by ID', + inputSchema: { + type: 'object', + properties: { + opportunity_id: { type: 'number', description: 'Opportunity ID', required: true }, + optional_properties: { type: 'array', items: { type: 'string' }, description: 'Additional fields to include' }, + }, + required: ['opportunity_id'], + }, + }, + { + name: 'keap_update_opportunity', + description: 'Update an existing opportunity', + inputSchema: { + type: 'object', + properties: { + opportunity_id: { type: 'number', description: 'Opportunity ID', required: true }, + opportunity_title: { type: 'string', description: 'Deal title' }, + stage_id: { type: 'number', description: 'Pipeline stage ID' }, + user_id: { type: 'number', description: 'User ID (owner)' }, + estimated_close_date: { type: 'string', description: 'Estimated close date' }, + projected_revenue_low: { type: 'number', description: 'Low revenue estimate' }, + projected_revenue_high: { type: 'number', description: 'High revenue estimate' }, + opportunity_notes: { type: 'string', description: 'Notes' }, + next_action_notes: { type: 'string', description: 'Next action notes' }, + next_action_date: { type: 'string', description: 'Next action date' }, + }, + required: ['opportunity_id'], + }, + }, + { + name: 'keap_delete_opportunity', + description: 'Delete an opportunity', + inputSchema: { + type: 'object', + properties: { + opportunity_id: { type: 'number', description: 'Opportunity ID to delete', required: true }, + }, + required: ['opportunity_id'], + }, + }, + { + name: 'keap_list_opportunities', + description: 'List opportunities with filtering and pagination', + inputSchema: { + type: 'object', + properties: { + limit: { type: 'number', description: 'Results per page', default: 50 }, + offset: { type: 'number', description: 'Pagination offset', default: 0 }, + user_id: { type: 'number', description: 'Filter by user ID' }, + stage_id: { type: 'number', description: 'Filter by pipeline stage' }, + contact_id: { type: 'number', description: 'Filter by contact ID' }, + search_term: { type: 'string', description: 'Search in title/notes' }, + order: { type: 'string', description: 'Order by field' }, + }, + }, + }, + { + name: 'keap_list_opportunity_stage_pipeline', + description: 'List all pipeline stages for opportunities', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'keap_get_opportunity_stage_pipeline', + description: 'Get details of a specific pipeline stage', + inputSchema: { + type: 'object', + properties: { + stage_id: { type: 'number', description: 'Stage ID', required: true }, + }, + required: ['stage_id'], + }, + }, + { + name: 'keap_update_opportunity_stage', + description: 'Move an opportunity to a different pipeline stage', + inputSchema: { + type: 'object', + properties: { + opportunity_id: { type: 'number', description: 'Opportunity ID', required: true }, + stage_id: { type: 'number', description: 'New stage ID', required: true }, + move_to_stage_reason: { type: 'string', description: 'Reason for stage change' }, + }, + required: ['opportunity_id', 'stage_id'], + }, + }, + { + name: 'keap_get_opportunity_model', + description: 'Get the opportunity model schema with custom fields', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + ]; +} + +export async function handleOpportunitiesTool( + toolName: string, + args: any, + client: KeapClient +): Promise<{ content: Array<{ type: string; text: string }> }> { + let result: any; + + try { + switch (toolName) { + case 'keap_create_opportunity': + result = await client.post('/opportunities', { + opportunity_title: args.opportunity_title, + contact: { id: args.contact_id }, + stage: { id: args.stage_id }, + user: args.user_id, + estimated_close_date: args.estimated_close_date, + projected_revenue_low: args.projected_revenue_low, + projected_revenue_high: args.projected_revenue_high, + opportunity_notes: args.opportunity_notes, + next_action_notes: args.next_action_notes, + next_action_date: args.next_action_date, + custom_fields: args.custom_fields, + }); + break; + + case 'keap_get_opportunity': + result = await client.get(`/opportunities/${args.opportunity_id}`, { + optional_properties: args.optional_properties?.join(','), + }); + break; + + case 'keap_update_opportunity': { + const { opportunity_id, ...updateData } = args; + result = await client.patch(`/opportunities/${opportunity_id}`, updateData); + break; + } + + case 'keap_delete_opportunity': + await client.delete(`/opportunities/${args.opportunity_id}`); + result = { success: true, message: 'Opportunity deleted successfully' }; + break; + + case 'keap_list_opportunities': + result = await client.get('/opportunities', args); + break; + + case 'keap_list_opportunity_stage_pipeline': + result = await client.get('/opportunity/stage_pipeline'); + break; + + case 'keap_get_opportunity_stage_pipeline': + result = await client.get(`/opportunity/stage_pipeline/${args.stage_id}`); + break; + + case 'keap_update_opportunity_stage': + result = await client.patch(`/opportunities/${args.opportunity_id}`, { + stage: { id: args.stage_id }, + move_to_stage_reason: args.move_to_stage_reason, + }); + break; + + case 'keap_get_opportunity_model': + result = await client.get('/opportunities/model'); + break; + + default: + throw new Error(`Unknown tool: ${toolName}`); + } + + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], + }; + } catch (error: any) { + return { + content: [{ type: 'text', text: `Error: ${error.message}` }], + }; + } +} diff --git a/servers/keap/src/tools/orders-tools.ts b/servers/keap/src/tools/orders-tools.ts deleted file mode 100644 index c2e46b1..0000000 --- a/servers/keap/src/tools/orders-tools.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { KeapClient } from '../clients/keap.js'; - -export function registerOrdersTools(client: KeapClient) { - return { - keap_list_orders: { - description: 'List all orders', - inputSchema: { - type: 'object', - properties: { - contact_id: { type: 'number', description: 'Filter by contact ID' }, - since: { type: 'string', description: 'Start date filter (ISO 8601)' }, - until: { type: 'string', description: 'End date filter (ISO 8601)' }, - paid: { type: 'boolean', description: 'Filter by paid status' }, - limit: { type: 'number', description: 'Maximum results' }, - }, - }, - handler: async (args: any) => { - const orders = await client.listOrders(args); - return { orders, count: orders.length }; - }, - }, - - keap_get_order: { - description: 'Get a specific order by ID', - inputSchema: { - type: 'object', - properties: { - order_id: { type: 'number', description: 'Order ID' }, - }, - required: ['order_id'], - }, - handler: async (args: any) => { - return await client.getOrder(args.order_id); - }, - }, - - keap_create_order: { - description: 'Create a new order', - inputSchema: { - type: 'object', - properties: { - contact_id: { type: 'number', description: 'Contact ID' }, - title: { type: 'string', description: 'Order title' }, - order_items: { - type: 'array', - items: { - type: 'object', - properties: { - product_id: { type: 'number' }, - description: { type: 'string' }, - quantity: { type: 'number' }, - price: { type: 'number' }, - }, - }, - description: 'Order items', - }, - notes: { type: 'string', description: 'Order notes' }, - }, - required: ['contact_id', 'title'], - }, - handler: async (args: any) => { - return await client.createOrder({ - contact_id: args.contact_id, - title: args.title, - order_items: args.order_items, - notes: args.notes, - }); - }, - }, - - keap_list_transactions: { - description: 'List all transactions', - inputSchema: { - type: 'object', - properties: { - contact_id: { type: 'number', description: 'Filter by contact ID' }, - order_id: { type: 'number', description: 'Filter by order ID' }, - since: { type: 'string', description: 'Start date filter (ISO 8601)' }, - until: { type: 'string', description: 'End date filter (ISO 8601)' }, - limit: { type: 'number', description: 'Maximum results' }, - }, - }, - handler: async (args: any) => { - const transactions = await client.get('/transactions', args); - return { transactions: transactions.transactions || [], count: transactions.count || 0 }; - }, - }, - - keap_create_transaction: { - description: 'Create a new transaction/payment', - inputSchema: { - type: 'object', - properties: { - contact_id: { type: 'number', description: 'Contact ID' }, - order_id: { type: 'number', description: 'Order ID' }, - amount: { type: 'number', description: 'Transaction amount' }, - currency: { type: 'string', description: 'Currency code (e.g., USD)' }, - gateway: { type: 'string', description: 'Payment gateway' }, - type: { type: 'string', enum: ['Sale', 'Refund', 'Chargeback'], description: 'Transaction type' }, - test: { type: 'boolean', description: 'Test transaction' }, - }, - required: ['contact_id', 'amount'], - }, - handler: async (args: any) => { - return await client.post('/transactions', { - contact_id: args.contact_id, - order_id: args.order_id, - amount: args.amount, - currency: args.currency || 'USD', - gateway: args.gateway, - type: args.type || 'Sale', - test: args.test || false, - transaction_date: new Date().toISOString(), - status: 'Completed', - }); - }, - }, - }; -} diff --git a/servers/keap/src/tools/products-tools.ts b/servers/keap/src/tools/products-tools.ts deleted file mode 100644 index 539489f..0000000 --- a/servers/keap/src/tools/products-tools.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { KeapClient } from '../clients/keap.js'; - -export function registerProductsTools(client: KeapClient) { - return { - keap_list_products: { - description: 'List all products', - inputSchema: { - type: 'object', - properties: { - active: { type: 'boolean', description: 'Filter by active status' }, - limit: { type: 'number', description: 'Maximum results' }, - }, - }, - handler: async (args: any) => { - const products = await client.listProducts(args); - return { products, count: products.length }; - }, - }, - - keap_get_product: { - description: 'Get a specific product by ID', - inputSchema: { - type: 'object', - properties: { - product_id: { type: 'number', description: 'Product ID' }, - }, - required: ['product_id'], - }, - handler: async (args: any) => { - return await client.getProduct(args.product_id); - }, - }, - - keap_create_product: { - description: 'Create a new product', - inputSchema: { - type: 'object', - properties: { - product_name: { type: 'string', description: 'Product name' }, - product_short_desc: { type: 'string', description: 'Short description' }, - product_desc: { type: 'string', description: 'Full description' }, - sku: { type: 'string', description: 'SKU code' }, - price: { type: 'number', description: 'Product price' }, - status: { type: 'string', enum: ['Active', 'Inactive'], description: 'Product status' }, - }, - required: ['product_name'], - }, - handler: async (args: any) => { - return await client.createProduct({ - product_name: args.product_name, - product_short_desc: args.product_short_desc, - product_desc: args.product_desc, - sku: args.sku, - product_price: args.price, - status: args.status || 'Active', - }); - }, - }, - - keap_update_product: { - description: 'Update an existing product', - inputSchema: { - type: 'object', - properties: { - product_id: { type: 'number', description: 'Product ID' }, - product_name: { type: 'string', description: 'Product name' }, - product_short_desc: { type: 'string', description: 'Short description' }, - product_desc: { type: 'string', description: 'Full description' }, - sku: { type: 'string', description: 'SKU code' }, - price: { type: 'number', description: 'Product price' }, - status: { type: 'string', enum: ['Active', 'Inactive'], description: 'Product status' }, - }, - required: ['product_id'], - }, - handler: async (args: any) => { - const data: any = {}; - if (args.product_name !== undefined) data.product_name = args.product_name; - if (args.product_short_desc !== undefined) data.product_short_desc = args.product_short_desc; - if (args.product_desc !== undefined) data.product_desc = args.product_desc; - if (args.sku !== undefined) data.sku = args.sku; - if (args.price !== undefined) data.product_price = args.price; - if (args.status !== undefined) data.status = args.status; - - return await client.updateProduct(args.product_id, data); - }, - }, - - keap_delete_product: { - description: 'Delete a product', - inputSchema: { - type: 'object', - properties: { - product_id: { type: 'number', description: 'Product ID' }, - }, - required: ['product_id'], - }, - handler: async (args: any) => { - await client.deleteProduct(args.product_id); - return { success: true, message: `Product ${args.product_id} deleted` }; - }, - }, - - keap_list_subscriptions: { - description: 'List all subscription plans', - inputSchema: { - type: 'object', - properties: { - product_id: { type: 'number', description: 'Filter by product ID' }, - limit: { type: 'number', description: 'Maximum results' }, - }, - }, - handler: async (args: any) => { - const subscriptions = await client.get('/subscriptions', args); - return { subscriptions: subscriptions.subscriptions || [] }; - }, - }, - }; -} diff --git a/servers/keap/src/tools/reports-tools.ts b/servers/keap/src/tools/reports-tools.ts deleted file mode 100644 index 5065f52..0000000 --- a/servers/keap/src/tools/reports-tools.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { KeapClient } from '../clients/keap.js'; - -export function registerReportsTools(client: KeapClient) { - return { - keap_contact_growth_report: { - description: 'Get contact growth statistics', - inputSchema: { - type: 'object', - properties: { - since: { type: 'string', description: 'Start date (ISO 8601)' }, - until: { type: 'string', description: 'End date (ISO 8601)' }, - }, - required: ['since', 'until'], - }, - handler: async (args: any) => { - const contacts = await client.listContacts({ - since: args.since, - until: args.until, - }); - - // Group by date - const dailyGrowth: Record = {}; - contacts.forEach(contact => { - const date = contact.date_created?.split('T')[0]; - if (date) { - dailyGrowth[date] = (dailyGrowth[date] || 0) + 1; - } - }); - - const report = Object.keys(dailyGrowth) - .sort() - .map(date => ({ - date, - new_contacts: dailyGrowth[date], - total_contacts: 0, // Would need cumulative calculation - })); - - return { report, total_new_contacts: contacts.length }; - }, - }, - - keap_revenue_report: { - description: 'Get revenue statistics', - inputSchema: { - type: 'object', - properties: { - since: { type: 'string', description: 'Start date (ISO 8601)' }, - until: { type: 'string', description: 'End date (ISO 8601)' }, - }, - required: ['since', 'until'], - }, - handler: async (args: any) => { - const transactions = await client.get('/transactions', { - since: args.since, - until: args.until, - }); - - const txList = transactions.transactions || []; - - const totalRevenue = txList - .filter((tx: any) => tx.type === 'Sale' && tx.status === 'Completed') - .reduce((sum: number, tx: any) => sum + (tx.amount || 0), 0); - - const totalRefunds = txList - .filter((tx: any) => tx.type === 'Refund') - .reduce((sum: number, tx: any) => sum + (tx.amount || 0), 0); - - const salesCount = txList.filter((tx: any) => tx.type === 'Sale').length; - const avgOrderValue = salesCount > 0 ? totalRevenue / salesCount : 0; - - return { - total_revenue: totalRevenue, - total_refunds: totalRefunds, - net_revenue: totalRevenue - totalRefunds, - transactions: salesCount, - average_order_value: avgOrderValue, - }; - }, - }, - - keap_campaign_performance_report: { - description: 'Get campaign performance statistics', - inputSchema: { - type: 'object', - properties: { - campaign_id: { type: 'number', description: 'Campaign ID' }, - }, - required: ['campaign_id'], - }, - handler: async (args: any) => { - const campaign = await client.getCampaign(args.campaign_id); - - // Get emails sent for this campaign - const emails = await client.listEmails({ - campaign_id: args.campaign_id, - }); - - const totalSent = emails.length; - const opened = emails.filter(e => e.opened).length; - const clicked = emails.filter(e => e.clicked).length; - const bounced = emails.filter(e => e.bounced).length; - - const openRate = totalSent > 0 ? (opened / totalSent) * 100 : 0; - const clickRate = opened > 0 ? (clicked / opened) * 100 : 0; - - return { - campaign_id: args.campaign_id, - campaign_name: campaign.name, - total_sent: totalSent, - opened, - clicked, - bounced, - open_rate: Math.round(openRate * 100) / 100, - click_rate: Math.round(clickRate * 100) / 100, - }; - }, - }, - }; -} diff --git a/servers/keap/src/tools/settings-tools.ts b/servers/keap/src/tools/settings-tools.ts new file mode 100644 index 0000000..4a52b14 --- /dev/null +++ b/servers/keap/src/tools/settings-tools.ts @@ -0,0 +1,105 @@ +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import { KeapClient } from '../clients/keap.js'; + +export function createSettingsTools(client: KeapClient): Tool[] { + return [ + { + name: 'keap_get_account_profile', + description: 'Get the account profile information', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'keap_update_account_profile', + description: 'Update account profile settings', + inputSchema: { + type: 'object', + properties: { + name: { type: 'string', description: 'Business name' }, + email: { type: 'string', description: 'Business email' }, + phone: { type: 'string', description: 'Business phone' }, + address: { type: 'string', description: 'Business address' }, + website: { type: 'string', description: 'Business website' }, + time_zone: { type: 'string', description: 'Time zone' }, + currency_code: { type: 'string', description: 'Currency code (e.g., USD)' }, + language_tag: { type: 'string', description: 'Language tag (e.g., en-US)' }, + }, + }, + }, + { + name: 'keap_list_users', + description: 'List all users in the account', + inputSchema: { + type: 'object', + properties: { + include_inactive: { type: 'boolean', description: 'Include inactive users', default: false }, + limit: { type: 'number', description: 'Results per page', default: 50 }, + }, + }, + }, + { + name: 'keap_get_application_configuration', + description: 'Get application configuration settings', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'keap_list_custom_fields', + description: 'List all custom fields for a given entity type', + inputSchema: { + type: 'object', + properties: { + entity_type: { type: 'string', description: 'Entity type (Contact, Company, Opportunity, etc.)', required: true }, + }, + required: ['entity_type'], + }, + }, + ]; +} + +export async function handleSettingsTool( + toolName: string, + args: any, + client: KeapClient +): Promise<{ content: Array<{ type: string; text: string }> }> { + let result: any; + + try { + switch (toolName) { + case 'keap_get_account_profile': + result = await client.get('/account/profile'); + break; + + case 'keap_update_account_profile': + result = await client.patch('/account/profile', args); + break; + + case 'keap_list_users': + result = await client.get('/users', args); + break; + + case 'keap_get_application_configuration': + result = await client.get('/setting/application/configuration'); + break; + + case 'keap_list_custom_fields': + result = await client.get(`/customFields/${args.entity_type}`); + break; + + default: + throw new Error(`Unknown tool: ${toolName}`); + } + + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], + }; + } catch (error: any) { + return { + content: [{ type: 'text', text: `Error: ${error.message}` }], + }; + } +} diff --git a/servers/keap/src/tools/tags-tools.ts b/servers/keap/src/tools/tags-tools.ts index deb181d..a223954 100644 --- a/servers/keap/src/tools/tags-tools.ts +++ b/servers/keap/src/tools/tags-tools.ts @@ -1,108 +1,113 @@ +import { Tool } from '@modelcontextprotocol/sdk/types.js'; import { KeapClient } from '../clients/keap.js'; -export function registerTagsTools(client: KeapClient) { - return { - keap_list_tags: { - description: 'List all tags', +export function createTagsTools(client: KeapClient): Tool[] { + return [ + { + name: 'keap_create_tag', + description: 'Create a new tag in Keap', inputSchema: { type: 'object', properties: { - category: { type: 'string', description: 'Filter by category' }, - limit: { type: 'number', description: 'Maximum results' }, - }, - }, - handler: async (args: any) => { - const tags = await client.listTags(args); - return { tags, count: tags.length }; - }, - }, - - keap_get_tag: { - description: 'Get a specific tag by ID', - inputSchema: { - type: 'object', - properties: { - tag_id: { type: 'number', description: 'Tag ID' }, - }, - required: ['tag_id'], - }, - handler: async (args: any) => { - return await client.getTag(args.tag_id); - }, - }, - - keap_create_tag: { - description: 'Create a new tag', - inputSchema: { - type: 'object', - properties: { - name: { type: 'string', description: 'Tag name' }, + name: { type: 'string', description: 'Tag name', required: true }, description: { type: 'string', description: 'Tag description' }, category_id: { type: 'number', description: 'Tag category ID' }, }, required: ['name'], }, - handler: async (args: any) => { - return await client.createTag({ + }, + { + name: 'keap_get_tag', + description: 'Retrieve a tag by ID', + inputSchema: { + type: 'object', + properties: { + tag_id: { type: 'number', description: 'Tag ID', required: true }, + }, + required: ['tag_id'], + }, + }, + { + name: 'keap_list_tags', + description: 'List all tags with pagination', + inputSchema: { + type: 'object', + properties: { + limit: { type: 'number', description: 'Results per page', default: 50 }, + offset: { type: 'number', description: 'Pagination offset', default: 0 }, + category: { type: 'string', description: 'Filter by category name' }, + }, + }, + }, + { + name: 'keap_create_tag_category', + description: 'Create a tag category', + inputSchema: { + type: 'object', + properties: { + name: { type: 'string', description: 'Category name', required: true }, + description: { type: 'string', description: 'Category description' }, + }, + required: ['name'], + }, + }, + { + name: 'keap_list_tag_categories', + description: 'List all tag categories', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + ]; +} + +export async function handleTagsTool( + toolName: string, + args: any, + client: KeapClient +): Promise<{ content: Array<{ type: string; text: string }> }> { + let result: any; + + try { + switch (toolName) { + case 'keap_create_tag': + result = await client.post('/tags', { name: args.name, description: args.description, category: args.category_id ? { id: args.category_id } : undefined, }); - }, - }, + break; - keap_update_tag: { - description: 'Update an existing tag', - inputSchema: { - type: 'object', - properties: { - tag_id: { type: 'number', description: 'Tag ID' }, - name: { type: 'string', description: 'Tag name' }, - description: { type: 'string', description: 'Tag description' }, - }, - required: ['tag_id'], - }, - handler: async (args: any) => { - const data: any = {}; - if (args.name !== undefined) data.name = args.name; - if (args.description !== undefined) data.description = args.description; + case 'keap_get_tag': + result = await client.get(`/tags/${args.tag_id}`); + break; - return await client.patch(`/tags/${args.tag_id}`, data); - }, - }, + case 'keap_list_tags': + result = await client.get('/tags', args); + break; - keap_delete_tag: { - description: 'Delete a tag', - inputSchema: { - type: 'object', - properties: { - tag_id: { type: 'number', description: 'Tag ID' }, - }, - required: ['tag_id'], - }, - handler: async (args: any) => { - await client.deleteTag(args.tag_id); - return { success: true, message: `Tag ${args.tag_id} deleted` }; - }, - }, - - keap_list_contacts_by_tag: { - description: 'List all contacts with a specific tag', - inputSchema: { - type: 'object', - properties: { - tag_id: { type: 'number', description: 'Tag ID' }, - limit: { type: 'number', description: 'Maximum results' }, - }, - required: ['tag_id'], - }, - handler: async (args: any) => { - const contacts = await client.listContacts({ - tag_id: args.tag_id, - limit: args.limit || 1000, + case 'keap_create_tag_category': + result = await client.post('/tags/categories', { + name: args.name, + description: args.description, }); - return { contacts, count: contacts.length }; - }, - }, - }; + break; + + case 'keap_list_tag_categories': + result = await client.get('/tags/categories'); + break; + + default: + throw new Error(`Unknown tool: ${toolName}`); + } + + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], + }; + } catch (error: any) { + return { + content: [{ type: 'text', text: `Error: ${error.message}` }], + }; + } } diff --git a/servers/keap/src/tools/tasks-tools.ts b/servers/keap/src/tools/tasks-tools.ts index b2054d3..bfef9b1 100644 --- a/servers/keap/src/tools/tasks-tools.ts +++ b/servers/keap/src/tools/tasks-tools.ts @@ -1,126 +1,180 @@ +import { Tool } from '@modelcontextprotocol/sdk/types.js'; import { KeapClient } from '../clients/keap.js'; -export function registerTasksTools(client: KeapClient) { - return { - keap_list_tasks: { - description: 'List all tasks', +export function createTasksTools(client: KeapClient): Tool[] { + return [ + { + name: 'keap_create_task', + description: 'Create a new task in Keap', inputSchema: { type: 'object', properties: { - contact_id: { type: 'number', description: 'Filter by contact ID' }, - user_id: { type: 'number', description: 'Filter by assigned user ID' }, - completed: { type: 'boolean', description: 'Filter by completion status' }, - limit: { type: 'number', description: 'Maximum results' }, - order: { type: 'string', description: 'Sort order' }, - }, - }, - handler: async (args: any) => { - const tasks = await client.listTasks(args); - return { tasks, count: tasks.length }; - }, - }, - - keap_get_task: { - description: 'Get a specific task by ID', - inputSchema: { - type: 'object', - properties: { - task_id: { type: 'number', description: 'Task ID' }, - }, - required: ['task_id'], - }, - handler: async (args: any) => { - return await client.getTask(args.task_id); - }, - }, - - keap_create_task: { - description: 'Create a new task', - inputSchema: { - type: 'object', - properties: { - title: { type: 'string', description: 'Task title' }, + title: { type: 'string', description: 'Task title', required: true }, description: { type: 'string', description: 'Task description' }, - contact_id: { type: 'number', description: 'Contact ID' }, + contact_id: { type: 'number', description: 'Associated contact ID' }, + due_date: { type: 'string', description: 'Due date (ISO format)' }, + remind_time: { type: 'number', description: 'Reminder time in minutes before due date' }, user_id: { type: 'number', description: 'Assigned user ID' }, - due_date: { type: 'string', description: 'Due date (ISO 8601)' }, - priority: { type: 'number', description: 'Priority level (1-5)' }, + priority: { type: 'number', description: 'Priority (1-5)', default: 3 }, type: { type: 'string', description: 'Task type' }, }, required: ['title'], }, - handler: async (args: any) => { - return await client.createTask({ - title: args.title, - description: args.description, - contact: args.contact_id ? { id: args.contact_id } : undefined, - user_id: args.user_id, - due_date: args.due_date, - priority: args.priority, - type: args.type, - completed: false, - }); + }, + { + name: 'keap_get_task', + description: 'Retrieve a task by ID', + inputSchema: { + type: 'object', + properties: { + task_id: { type: 'number', description: 'Task ID', required: true }, + }, + required: ['task_id'], }, }, - - keap_update_task: { + { + name: 'keap_update_task', description: 'Update an existing task', inputSchema: { type: 'object', properties: { - task_id: { type: 'number', description: 'Task ID' }, + task_id: { type: 'number', description: 'Task ID', required: true }, title: { type: 'string', description: 'Task title' }, description: { type: 'string', description: 'Task description' }, - user_id: { type: 'number', description: 'Assigned user ID' }, - due_date: { type: 'string', description: 'Due date (ISO 8601)' }, - priority: { type: 'number', description: 'Priority level (1-5)' }, - completed: { type: 'boolean', description: 'Completion status' }, + due_date: { type: 'string', description: 'Due date' }, + completed: { type: 'boolean', description: 'Mark as completed' }, + priority: { type: 'number', description: 'Priority' }, }, required: ['task_id'], }, - handler: async (args: any) => { - const data: any = {}; - if (args.title !== undefined) data.title = args.title; - if (args.description !== undefined) data.description = args.description; - if (args.user_id !== undefined) data.user_id = args.user_id; - if (args.due_date !== undefined) data.due_date = args.due_date; - if (args.priority !== undefined) data.priority = args.priority; - if (args.completed !== undefined) data.completed = args.completed; - - return await client.updateTask(args.task_id, data); - }, }, - - keap_delete_task: { + { + name: 'keap_delete_task', description: 'Delete a task', inputSchema: { type: 'object', properties: { - task_id: { type: 'number', description: 'Task ID' }, + task_id: { type: 'number', description: 'Task ID to delete', required: true }, }, required: ['task_id'], }, - handler: async (args: any) => { - await client.deleteTask(args.task_id); - return { success: true, message: `Task ${args.task_id} deleted` }; + }, + { + name: 'keap_list_tasks', + description: 'List tasks with filtering and pagination', + inputSchema: { + type: 'object', + properties: { + limit: { type: 'number', description: 'Results per page', default: 50 }, + offset: { type: 'number', description: 'Pagination offset', default: 0 }, + user_id: { type: 'number', description: 'Filter by assigned user' }, + contact_id: { type: 'number', description: 'Filter by contact' }, + completed: { type: 'boolean', description: 'Filter by completion status' }, + since: { type: 'string', description: 'Tasks created after this date' }, + until: { type: 'string', description: 'Tasks created before this date' }, + }, }, }, - - keap_complete_task: { + { + name: 'keap_search_tasks', + description: 'Search tasks by title, description, or other criteria', + inputSchema: { + type: 'object', + properties: { + query: { type: 'string', description: 'Search query' }, + limit: { type: 'number', description: 'Max results', default: 50 }, + }, + }, + }, + { + name: 'keap_complete_task', description: 'Mark a task as completed', inputSchema: { type: 'object', properties: { - task_id: { type: 'number', description: 'Task ID' }, + task_id: { type: 'number', description: 'Task ID', required: true }, + completion_date: { type: 'string', description: 'Completion date (ISO format)' }, }, required: ['task_id'], }, - handler: async (args: any) => { - return await client.updateTask(args.task_id, { - completed: true, - completion_date: new Date().toISOString(), - }); + }, + { + name: 'keap_get_task_model', + description: 'Get the task model schema', + inputSchema: { + type: 'object', + properties: {}, }, }, - }; + ]; +} + +export async function handleTasksTool( + toolName: string, + args: any, + client: KeapClient +): Promise<{ content: Array<{ type: string; text: string }> }> { + let result: any; + + try { + switch (toolName) { + case 'keap_create_task': + result = await client.post('/tasks', { + title: args.title, + description: args.description, + contact: args.contact_id ? { id: args.contact_id } : undefined, + due_date: args.due_date, + remind_time: args.remind_time, + user_id: args.user_id, + priority: args.priority || 3, + type: args.type, + }); + break; + + case 'keap_get_task': + result = await client.get(`/tasks/${args.task_id}`); + break; + + case 'keap_update_task': { + const { task_id, ...updateData } = args; + result = await client.patch(`/tasks/${task_id}`, updateData); + break; + } + + case 'keap_delete_task': + await client.delete(`/tasks/${args.task_id}`); + result = { success: true, message: 'Task deleted successfully' }; + break; + + case 'keap_list_tasks': + result = await client.get('/tasks', args); + break; + + case 'keap_search_tasks': + result = await client.get('/tasks/search', args); + break; + + case 'keap_complete_task': + result = await client.patch(`/tasks/${args.task_id}`, { + completed: true, + completion_date: args.completion_date || new Date().toISOString(), + }); + break; + + case 'keap_get_task_model': + result = await client.get('/tasks/model'); + break; + + default: + throw new Error(`Unknown tool: ${toolName}`); + } + + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], + }; + } catch (error: any) { + return { + content: [{ type: 'text', text: `Error: ${error.message}` }], + }; + } } diff --git a/servers/keap/src/types/index.ts b/servers/keap/src/types/index.ts new file mode 100644 index 0000000..550b771 --- /dev/null +++ b/servers/keap/src/types/index.ts @@ -0,0 +1,434 @@ +// Keap API TypeScript Interfaces + +export interface KeapContact { + id?: number; + given_name?: string; + family_name?: string; + email_addresses?: EmailAddress[]; + phone_numbers?: PhoneNumber[]; + addresses?: Address[]; + company?: Company; + custom_fields?: CustomField[]; + tag_ids?: number[]; + opt_in_reason?: string; + owner_id?: number; + lead_source_id?: number; + date_created?: string; + last_updated?: string; + score_value?: number; + preferred_name?: string; + preferred_locale?: string; + time_zone?: string; + website?: string; + job_title?: string; + anniversary?: string; + birthday?: string; + suffix?: string; + middle_name?: string; + prefix?: string; + spouse_name?: string; + social_accounts?: SocialAccount[]; + fax_numbers?: FaxNumber[]; + contact_type?: string; +} + +export interface EmailAddress { + email: string; + field?: string; +} + +export interface PhoneNumber { + number: string; + field?: string; + extension?: string; + type?: string; +} + +export interface Address { + line1?: string; + line2?: string; + locality?: string; + region?: string; + postal_code?: string; + country_code?: string; + zip_code?: string; + zip_four?: string; + field?: string; +} + +export interface Company { + id?: number; + company_name?: string; +} + +export interface CustomField { + id: number; + content?: any; +} + +export interface SocialAccount { + name?: string; + type?: string; +} + +export interface FaxNumber { + number?: string; + field?: string; +} + +export interface KeapTag { + id?: number; + name?: string; + description?: string; + category?: TagCategory; +} + +export interface TagCategory { + id?: number; + name?: string; + description?: string; +} + +export interface KeapOpportunity { + id?: number; + opportunity_title?: string; + contact?: ContactReference; + stage?: StageReference; + user?: number; + date_created?: string; + estimated_close_date?: string; + opportunity_notes?: string; + next_action_notes?: string; + next_action_date?: string; + last_updated?: string; + projected_revenue_low?: number; + projected_revenue_high?: number; + custom_fields?: CustomField[]; + include_in_forecast?: number; + affiliate_id?: number; +} + +export interface ContactReference { + id: number; +} + +export interface StageReference { + id: number; + name?: string; +} + +export interface KeapTask { + id?: number; + title?: string; + description?: string; + contact?: ContactReference; + due_date?: string; + remind_time?: number; + completed?: boolean; + completion_date?: string; + type?: string; + priority?: number; + user_id?: number; + creation_date?: string; + modification_date?: string; + url?: string; +} + +export interface KeapAppointment { + id?: number; + title?: string; + description?: string; + start_date?: string; + end_date?: string; + contact?: ContactReference; + location?: string; + remind_time?: number; + all_day?: boolean; + user?: number; +} + +export interface KeapCampaign { + id?: number; + name?: string; + description?: string; + created_by_global_id?: number; + error_message?: string; + goals?: CampaignGoal[]; + time_zone?: string; + published_date?: string; + published_status?: boolean; + published_by_user_id?: number; +} + +export interface CampaignGoal { + id?: number; + name?: string; + historical_contact_count?: number; + historical_contact_count_completed?: number; +} + +export interface KeapProduct { + id?: number; + product_name?: string; + product_short_desc?: string; + product_desc?: string; + product_price?: number; + sku?: string; + subscription_only?: boolean; + subscription_plans?: SubscriptionPlan[]; + url?: string; + status?: number; +} + +export interface SubscriptionPlan { + id?: number; + cycle?: string; + frequency?: number; + number_of_cycles?: number; + plan_price?: number; + subscription_plan_index?: number; + subscription_plan_name?: string; +} + +export interface KeapOrder { + id?: number; + contact_id?: number; + order_date?: string; + order_title?: string; + order_type?: string; + order_items?: OrderItem[]; + status?: string; + total?: number; + total_paid?: number; + total_due?: number; + shipping_information?: ShippingInformation; + allow_payment?: boolean; + lead_affiliate_id?: number; + sales_affiliate_id?: number; + promo_codes?: string[]; +} + +export interface OrderItem { + id?: number; + product_id?: number; + quantity?: number; + price?: number; + discount?: number; + product_name?: string; + description?: string; + notes?: string; + type?: string; + subscription_plan_id?: number; +} + +export interface ShippingInformation { + first_name?: string; + last_name?: string; + company?: string; + street1?: string; + street2?: string; + city?: string; + state?: string; + postal_code?: string; + country?: string; + phone?: string; + method?: string; +} + +export interface KeapTransaction { + id?: number; + contact_id?: number; + amount?: number; + currency?: string; + gateway?: string; + gateway_account_name?: string; + order_ids?: number[]; + orders?: OrderReference[]; + paymentDate?: string; + status?: string; + test?: boolean; + type?: string; +} + +export interface OrderReference { + id?: number; +} + +export interface KeapSubscription { + id?: number; + active?: boolean; + billing_amount?: number; + billing_cycle?: string; + billing_frequency?: number; + contact_id?: number; + credit_card_id?: number; + end_date?: string; + next_bill_date?: string; + payment_gateway_id?: string; + product_id?: number; + quantity?: number; + start_date?: string; + subscription_plan_id?: number; +} + +export interface KeapNote { + id?: number; + contact_id?: number; + user_id?: number; + date_created?: string; + last_updated?: string; + body?: string; + title?: string; + type?: string; +} + +export interface KeapEmail { + id?: number; + sent_from_address?: string; + sent_to_address?: string; + sent_from_reply_address?: string; + subject?: string; + html_content?: string; + text_content?: string; + attachments?: Attachment[]; + contacts?: number[]; + sent_date?: string; + received_date?: string; + opened_date?: string; + clicked_date?: string; +} + +export interface Attachment { + file_name?: string; + file_data?: string; +} + +export interface KeapFile { + id?: number; + file_name?: string; + file_box_id?: number; + contact_id?: number; + file_size?: number; + file_association?: string; + file_data?: string; + file_url?: string; + is_public?: boolean; +} + +export interface KeapAffiliate { + id?: number; + code?: string; + contact_id?: number; + name?: string; + parent_id?: number; + status?: number; + track_leads_for?: number; +} + +export interface KeapCommission { + id?: number; + affiliate_id?: number; + amount?: number; + contact_id?: number; + date_earned?: string; + invoice_id?: number; + order_id?: number; + product_id?: number; +} + +export interface KeapAccount { + address?: string; + business_goals?: string[]; + business_primary_color?: string; + business_secondary_color?: string; + business_type?: string; + currency_code?: string; + email?: string; + language_tag?: string; + logo_url?: string; + name?: string; + phone?: string; + phone_ext?: string; + time_zone?: string; + website?: string; +} + +export interface KeapUser { + id?: number; + email_address?: string; + family_name?: string; + given_name?: string; + infusionsoft_id?: string; + partner?: boolean; + status?: string; +} + +export interface KeapHook { + key?: string; + eventKey?: string; + hookUrl?: string; + status?: string; +} + +export interface KeapPipeline { + id?: number; + name?: string; + stages?: PipelineStage[]; +} + +export interface PipelineStage { + id?: number; + name?: string; + details?: StageDetails; + target_num_days?: number; + target_revenue?: number; +} + +export interface StageDetails { + check_list_items?: CheckListItem[]; +} + +export interface CheckListItem { + description?: string; + required?: boolean; +} + +export interface KeapCreditCard { + id?: number; + card_number?: string; + card_type?: string; + expiration_month?: string; + expiration_year?: string; + name_on_card?: string; + email?: string; + status?: string; +} + +export interface ApiResponse { + data?: T; + count?: number; + next?: string; + previous?: string; + error?: ApiError; +} + +export interface ApiError { + message?: string; + code?: string; + details?: any; +} + +export interface PaginationParams { + limit?: number; + offset?: number; +} + +export interface SearchParams extends PaginationParams { + email?: string; + given_name?: string; + family_name?: string; + order?: string; + order_direction?: string; + since?: string; + until?: string; +} diff --git a/servers/keap/src/ui/react-app/automation-dashboard/index.tsx b/servers/keap/src/ui/react-app/automation-dashboard/index.tsx new file mode 100644 index 0000000..a8ce043 --- /dev/null +++ b/servers/keap/src/ui/react-app/automation-dashboard/index.tsx @@ -0,0 +1,118 @@ +import React, { useState } from 'react'; +import { Zap, Play, Pause, Users, TrendingUp } from 'lucide-react'; + +export default function AutomationDashboard() { + const [automations] = useState([ + { id: 1, name: 'Welcome Sequence', status: 'Active', contacts: 1234, completed: 987, success_rate: 79.9 }, + { id: 2, name: 'Lead Nurture', status: 'Active', contacts: 567, completed: 234, success_rate: 41.3 }, + { id: 3, name: 'Re-engagement', status: 'Active', contacts: 890, completed: 456, success_rate: 51.2 }, + { id: 4, name: 'Onboarding', status: 'Paused', contacts: 345, completed: 289, success_rate: 83.8 }, + { id: 5, name: 'Upsell Campaign', status: 'Active', contacts: 156, completed: 89, success_rate: 57.1 }, + ]); + + return ( +
+
+
+

Automation Dashboard

+ +
+ +
+ } + label="Active Automations" + value={automations.filter(a => a.status === 'Active').length} + color="blue" + /> + } + label="Total Contacts" + value={automations.reduce((sum, a) => sum + a.contacts, 0)} + color="green" + /> + } + label="Avg Success Rate" + value={`${(automations.reduce((sum, a) => sum + a.success_rate, 0) / automations.length).toFixed(1)}%`} + color="purple" + /> +
+ +
+ {automations.map(automation => ( +
+
+
+
+ +
+
+

{automation.name}

+ + {automation.status} + +
+
+ + +
+ +
+
+
Contacts
+
{automation.contacts}
+
+
+
Completed
+
{automation.completed}
+
+
+
Success Rate
+
{automation.success_rate}%
+
+
+ +
+
+
+
+
+
+ ))} +
+
+
+ ); +} + +function StatCard({ icon, label, value, color }: any) { + const colorClasses = { + blue: 'bg-blue-500', + green: 'bg-green-500', + purple: 'bg-purple-500', + }; + + return ( +
+
+ {icon} +
+
{value}
+
{label}
+
+ ); +} diff --git a/servers/keap/src/ui/react-app/build-all.js b/servers/keap/src/ui/react-app/build-all.js new file mode 100644 index 0000000..a344f57 --- /dev/null +++ b/servers/keap/src/ui/react-app/build-all.js @@ -0,0 +1,19 @@ +import { execSync } from 'child_process'; +import { readdirSync, statSync } from 'fs'; +import { join } from 'path'; + +const appsDir = './src/apps'; +const apps = readdirSync(appsDir).filter(f => statSync(join(appsDir, f)).isDirectory()); + +console.log(`Building ${apps.length} MCP apps...`); + +for (const app of apps) { + console.log(`Building ${app}...`); + try { + execSync(`vite build -c src/apps/${app}/vite.config.ts`, { stdio: 'inherit' }); + } catch (error) { + console.error(`Failed to build ${app}:`, error.message); + } +} + +console.log('All apps built successfully!'); diff --git a/servers/keap/src/ui/react-app/contact-timeline/index.tsx b/servers/keap/src/ui/react-app/contact-timeline/index.tsx new file mode 100644 index 0000000..b9c3259 --- /dev/null +++ b/servers/keap/src/ui/react-app/contact-timeline/index.tsx @@ -0,0 +1,91 @@ +import React, { useState } from 'react'; +import { Mail, Phone, Calendar, FileText, Tag, DollarSign } from 'lucide-react'; + +export default function ContactTimeline() { + const [contact] = useState({ + name: 'John Doe', + email: 'john@example.com', + }); + + const [events] = useState([ + { id: 1, type: 'email', title: 'Sent Welcome Email', date: '2025-02-14 10:30 AM', details: 'Opened: Yes, Clicked: Yes' }, + { id: 2, type: 'note', title: 'Sales Call Notes', date: '2025-02-13 2:00 PM', details: 'Discussed enterprise plan, very interested' }, + { id: 3, type: 'tag', title: 'Added Tag: VIP', date: '2025-02-13 2:05 PM', details: 'High-value prospect' }, + { id: 4, type: 'deal', title: 'Created Deal', date: '2025-02-12 11:00 AM', details: 'Enterprise Package - $50,000' }, + { id: 5, type: 'appointment', title: 'Demo Scheduled', date: '2025-02-10 9:00 AM', details: 'Product walkthrough' }, + { id: 6, type: 'phone', title: 'Phone Call', date: '2025-02-08 3:30 PM', details: 'Initial outreach, left voicemail' }, + { id: 7, type: 'email', title: 'Received Email', date: '2025-02-05 1:15 PM', details: 'Inquiry about pricing' }, + ]); + + const getIcon = (type: string) => { + switch (type) { + case 'email': return ; + case 'phone': return ; + case 'appointment': return ; + case 'note': return ; + case 'tag': return ; + case 'deal': return ; + default: return ; + } + }; + + const getColor = (type: string) => { + switch (type) { + case 'email': return 'blue'; + case 'phone': return 'green'; + case 'appointment': return 'purple'; + case 'note': return 'gray'; + case 'tag': return 'yellow'; + case 'deal': return 'orange'; + default: return 'gray'; + } + }; + + return ( +
+
+
+

{contact.name}

+
{contact.email}
+
+ +
+ {/* Timeline line */} +
+ +
+ {events.map(event => { + const color = getColor(event.type); + const colorClasses: Record = { + blue: 'bg-blue-600', + green: 'bg-green-600', + purple: 'bg-purple-600', + gray: 'bg-gray-600', + yellow: 'bg-yellow-600', + orange: 'bg-orange-600', + }; + + return ( +
+ {/* Icon circle */} +
+ {getIcon(event.type)} +
+ + {/* Event card */} +
+
+

{event.title}

+ {event.date} +
+

{event.details}

+
+
+ ); + })} +
+
+
+
+ ); +} diff --git a/servers/keap/src/ui/react-app/order-dashboard/index.tsx b/servers/keap/src/ui/react-app/order-dashboard/index.tsx new file mode 100644 index 0000000..de7652c --- /dev/null +++ b/servers/keap/src/ui/react-app/order-dashboard/index.tsx @@ -0,0 +1,74 @@ +import React, { useState } from 'react'; +import { ShoppingCart, DollarSign, Package, TrendingUp } from 'lucide-react'; + +export default function OrderDashboard() { + const [stats] = useState({ + total_orders: 347, + total_revenue: 142850, + avg_order_value: 411.82, + pending_orders: 23, + }); + + const [recentOrders] = useState([ + { id: 1, title: 'Enterprise License - Acme Corp', amount: 50000, status: 'Paid', date: '2025-02-14' }, + { id: 2, title: 'Premium Plan - TechCo', amount: 2400, status: 'Paid', date: '2025-02-14' }, + { id: 3, title: 'Consulting Package', amount: 5000, status: 'Pending', date: '2025-02-13' }, + { id: 4, title: 'Starter Plan - StartupX', amount: 348, status: 'Paid', date: '2025-02-13' }, + ]); + + return ( +
+

Order Dashboard

+ +
+ } label="Total Orders" value={stats.total_orders} color="blue" /> + } label="Total Revenue" value={`$${(stats.total_revenue / 1000).toFixed(1)}K`} color="green" /> + } label="Avg Order Value" value={`$${stats.avg_order_value.toFixed(0)}`} color="purple" /> + } label="Pending" value={stats.pending_orders} color="orange" /> +
+ +
+

Recent Orders

+
+ {recentOrders.map(order => ( +
+
+
+

{order.title}

+
{order.date}
+
+
+
${order.amount.toLocaleString()}
+ + {order.status} + +
+
+
+ ))} +
+
+
+ ); +} + +function StatCard({ icon, label, value, color }: any) { + const colorClasses = { + blue: 'bg-blue-500', + green: 'bg-green-500', + purple: 'bg-purple-500', + orange: 'bg-orange-500', + }; + + return ( +
+
+ {icon} +
+
{value}
+
{label}
+
+ ); +} diff --git a/servers/keap/src/ui/react-app/order-detail/index.tsx b/servers/keap/src/ui/react-app/order-detail/index.tsx new file mode 100644 index 0000000..d250e56 --- /dev/null +++ b/servers/keap/src/ui/react-app/order-detail/index.tsx @@ -0,0 +1,99 @@ +import React, { useState } from 'react'; +import { ShoppingCart, User, Calendar, DollarSign, Package } from 'lucide-react'; + +export default function OrderDetail() { + const [order] = useState({ + id: 1, + title: 'Enterprise License - Acme Corp', + contact: 'John Doe', + date: '2025-02-14', + status: 'Paid', + total: 50000, + items: [ + { id: 1, description: 'Enterprise License (Annual)', quantity: 1, price: 45000 }, + { id: 2, description: 'Priority Support', quantity: 1, price: 5000 }, + ], + transactions: [ + { id: 1, amount: 50000, date: '2025-02-14', method: 'Credit Card', status: 'Completed' }, + ], + }); + + return ( +
+
+
+

{order.title}

+
+ + {order.status} + + Order #{order.id} +
+
+ +
+ } label="Customer" value={order.contact} /> + } label="Order Date" value={order.date} /> + } label="Total" value={`$${order.total.toLocaleString()}`} /> +
+ +
+
+ +

Order Items

+
+
+ {order.items.map(item => ( +
+
+
{item.description}
+
Quantity: {item.quantity}
+
+
${item.price.toLocaleString()}
+
+ ))} +
+ +
+ Total + ${order.total.toLocaleString()} +
+
+ +
+

Transactions

+
+ {order.transactions.map(transaction => ( +
+
+
+
{transaction.method}
+
{transaction.date}
+
+
+
${transaction.amount.toLocaleString()}
+
{transaction.status}
+
+
+
+ ))} +
+
+
+
+ ); +} + +function InfoCard({ icon, label, value }: any) { + return ( +
+
+
{icon}
+
{label}
+
+
{value}
+
+ ); +} diff --git a/servers/keap/src/ui/react-app/package.json b/servers/keap/src/ui/react-app/package.json new file mode 100644 index 0000000..726089a --- /dev/null +++ b/servers/keap/src/ui/react-app/package.json @@ -0,0 +1,22 @@ +{ + "name": "keap-mcp-apps", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "node build-all.js", + "dev": "vite" + }, + "dependencies": { + "@modelcontextprotocol/ext-apps": "^0.1.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.0", + "vite": "^5.3.0", + "typescript": "^5.4.0" + } +} diff --git a/servers/keap/src/ui/react-app/src/apps/affiliate-dashboard/App.tsx b/servers/keap/src/ui/react-app/src/apps/affiliate-dashboard/App.tsx new file mode 100644 index 0000000..ab955bf --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/affiliate-dashboard/App.tsx @@ -0,0 +1,39 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import '../../styles/global.css'; + +export default function AffiliateDashboard() { + const { callTool, loading, error } = useCallTool(); + const [data, setData] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + const result = await callTool('keap_list_affiliates', {}); + setData(result); + } catch (err) { + console.error('Failed to load data:', err); + } + }; + + return ( +
+

Affiliate Dashboard

+ + {error &&
{error}
} + + {loading ? ( +
Loading...
+ ) : ( +
+
+            {JSON.stringify(data, null, 2)}
+          
+
+ )} +
+ ); +} diff --git a/servers/keap/src/ui/react-app/src/apps/affiliate-dashboard/index.html b/servers/keap/src/ui/react-app/src/apps/affiliate-dashboard/index.html new file mode 100644 index 0000000..2aa81df --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/affiliate-dashboard/index.html @@ -0,0 +1,12 @@ + + + + + + Affiliate Dashboard - Keap + + +
+ + + diff --git a/servers/keap/src/ui/react-app/src/apps/affiliate-dashboard/main.tsx b/servers/keap/src/ui/react-app/src/apps/affiliate-dashboard/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/affiliate-dashboard/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/keap/src/ui/react-app/src/apps/affiliate-dashboard/vite.config.ts b/servers/keap/src/ui/react-app/src/apps/affiliate-dashboard/vite.config.ts new file mode 100644 index 0000000..59265d2 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/affiliate-dashboard/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/affiliate-dashboard', + emptyOutDir: true, + rollupOptions: { + input: resolve(__dirname, 'index.html'), + output: { + entryFileNames: 'assets/[name].js', + chunkFileNames: 'assets/[name].js', + assetFileNames: 'assets/[name].[ext]' + } + } + } +}); diff --git a/servers/keap/src/ui/react-app/src/apps/analytics-dashboard/App.tsx b/servers/keap/src/ui/react-app/src/apps/analytics-dashboard/App.tsx new file mode 100644 index 0000000..eae0883 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/analytics-dashboard/App.tsx @@ -0,0 +1,39 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import '../../styles/global.css'; + +export default function AnalyticsDashboard() { + const { callTool, loading, error } = useCallTool(); + const [data, setData] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + const result = await callTool('keap_list_contacts', {}); + setData(result); + } catch (err) { + console.error('Failed to load data:', err); + } + }; + + return ( +
+

Analytics Dashboard

+ + {error &&
{error}
} + + {loading ? ( +
Loading...
+ ) : ( +
+
+            {JSON.stringify(data, null, 2)}
+          
+
+ )} +
+ ); +} diff --git a/servers/keap/src/ui/react-app/src/apps/analytics-dashboard/index.html b/servers/keap/src/ui/react-app/src/apps/analytics-dashboard/index.html new file mode 100644 index 0000000..f220452 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/analytics-dashboard/index.html @@ -0,0 +1,12 @@ + + + + + + Analytics Dashboard - Keap + + +
+ + + diff --git a/servers/keap/src/ui/react-app/src/apps/analytics-dashboard/main.tsx b/servers/keap/src/ui/react-app/src/apps/analytics-dashboard/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/analytics-dashboard/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/keap/src/ui/react-app/src/apps/analytics-dashboard/vite.config.ts b/servers/keap/src/ui/react-app/src/apps/analytics-dashboard/vite.config.ts new file mode 100644 index 0000000..5172499 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/analytics-dashboard/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/analytics-dashboard', + emptyOutDir: true, + rollupOptions: { + input: resolve(__dirname, 'index.html'), + output: { + entryFileNames: 'assets/[name].js', + chunkFileNames: 'assets/[name].js', + assetFileNames: 'assets/[name].[ext]' + } + } + } +}); diff --git a/servers/keap/src/ui/react-app/src/apps/appointment-calendar/App.tsx b/servers/keap/src/ui/react-app/src/apps/appointment-calendar/App.tsx new file mode 100644 index 0000000..f1ac18d --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/appointment-calendar/App.tsx @@ -0,0 +1,39 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import '../../styles/global.css'; + +export default function AppointmentCalendar() { + const { callTool, loading, error } = useCallTool(); + const [data, setData] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + const result = await callTool('keap_list_appointments', {}); + setData(result); + } catch (err) { + console.error('Failed to load data:', err); + } + }; + + return ( +
+

Appointment Calendar

+ + {error &&
{error}
} + + {loading ? ( +
Loading...
+ ) : ( +
+
+            {JSON.stringify(data, null, 2)}
+          
+
+ )} +
+ ); +} diff --git a/servers/keap/src/ui/react-app/src/apps/appointment-calendar/index.html b/servers/keap/src/ui/react-app/src/apps/appointment-calendar/index.html new file mode 100644 index 0000000..f4e796f --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/appointment-calendar/index.html @@ -0,0 +1,12 @@ + + + + + + Appointment Calendar - Keap + + +
+ + + diff --git a/servers/keap/src/ui/react-app/src/apps/appointment-calendar/main.tsx b/servers/keap/src/ui/react-app/src/apps/appointment-calendar/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/appointment-calendar/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/keap/src/ui/react-app/src/apps/appointment-calendar/vite.config.ts b/servers/keap/src/ui/react-app/src/apps/appointment-calendar/vite.config.ts new file mode 100644 index 0000000..b430dd5 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/appointment-calendar/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/appointment-calendar', + emptyOutDir: true, + rollupOptions: { + input: resolve(__dirname, 'index.html'), + output: { + entryFileNames: 'assets/[name].js', + chunkFileNames: 'assets/[name].js', + assetFileNames: 'assets/[name].[ext]' + } + } + } +}); diff --git a/servers/keap/src/ui/react-app/src/apps/automation-builder/App.tsx b/servers/keap/src/ui/react-app/src/apps/automation-builder/App.tsx new file mode 100644 index 0000000..f4dd59e --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/automation-builder/App.tsx @@ -0,0 +1,39 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import '../../styles/global.css'; + +export default function AutomationBuilder() { + const { callTool, loading, error } = useCallTool(); + const [data, setData] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + const result = await callTool('keap_list_hooks', {}); + setData(result); + } catch (err) { + console.error('Failed to load data:', err); + } + }; + + return ( +
+

Automation Builder

+ + {error &&
{error}
} + + {loading ? ( +
Loading...
+ ) : ( +
+
+            {JSON.stringify(data, null, 2)}
+          
+
+ )} +
+ ); +} diff --git a/servers/keap/src/ui/react-app/src/apps/automation-builder/index.html b/servers/keap/src/ui/react-app/src/apps/automation-builder/index.html new file mode 100644 index 0000000..8ee1381 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/automation-builder/index.html @@ -0,0 +1,12 @@ + + + + + + Automation Builder - Keap + + +
+ + + diff --git a/servers/keap/src/ui/react-app/src/apps/automation-builder/main.tsx b/servers/keap/src/ui/react-app/src/apps/automation-builder/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/automation-builder/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/keap/src/ui/react-app/src/apps/automation-builder/vite.config.ts b/servers/keap/src/ui/react-app/src/apps/automation-builder/vite.config.ts new file mode 100644 index 0000000..7a55ca5 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/automation-builder/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/automation-builder', + emptyOutDir: true, + rollupOptions: { + input: resolve(__dirname, 'index.html'), + output: { + entryFileNames: 'assets/[name].js', + chunkFileNames: 'assets/[name].js', + assetFileNames: 'assets/[name].[ext]' + } + } + } +}); diff --git a/servers/keap/src/ui/react-app/src/apps/campaign-dashboard/App.tsx b/servers/keap/src/ui/react-app/src/apps/campaign-dashboard/App.tsx new file mode 100644 index 0000000..f83bfbd --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/campaign-dashboard/App.tsx @@ -0,0 +1,39 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import '../../styles/global.css'; + +export default function CampaignDashboard() { + const { callTool, loading, error } = useCallTool(); + const [data, setData] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + const result = await callTool('keap_list_campaigns', {}); + setData(result); + } catch (err) { + console.error('Failed to load data:', err); + } + }; + + return ( +
+

Campaign Dashboard

+ + {error &&
{error}
} + + {loading ? ( +
Loading...
+ ) : ( +
+
+            {JSON.stringify(data, null, 2)}
+          
+
+ )} +
+ ); +} diff --git a/servers/keap/src/ui/react-app/src/apps/campaign-dashboard/index.html b/servers/keap/src/ui/react-app/src/apps/campaign-dashboard/index.html new file mode 100644 index 0000000..b65979a --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/campaign-dashboard/index.html @@ -0,0 +1,12 @@ + + + + + + Campaign Dashboard - Keap + + +
+ + + diff --git a/servers/keap/src/ui/react-app/src/apps/campaign-dashboard/main.tsx b/servers/keap/src/ui/react-app/src/apps/campaign-dashboard/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/campaign-dashboard/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/keap/src/ui/react-app/src/apps/campaign-dashboard/vite.config.ts b/servers/keap/src/ui/react-app/src/apps/campaign-dashboard/vite.config.ts new file mode 100644 index 0000000..00068ef --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/campaign-dashboard/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/campaign-dashboard', + emptyOutDir: true, + rollupOptions: { + input: resolve(__dirname, 'index.html'), + output: { + entryFileNames: 'assets/[name].js', + chunkFileNames: 'assets/[name].js', + assetFileNames: 'assets/[name].[ext]' + } + } + } +}); diff --git a/servers/keap/src/ui/react-app/src/apps/campaign-detail/App.tsx b/servers/keap/src/ui/react-app/src/apps/campaign-detail/App.tsx new file mode 100644 index 0000000..c4f6c54 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/campaign-detail/App.tsx @@ -0,0 +1,39 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import '../../styles/global.css'; + +export default function CampaignDetail() { + const { callTool, loading, error } = useCallTool(); + const [data, setData] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + const result = await callTool('keap_get_campaign', {}); + setData(result); + } catch (err) { + console.error('Failed to load data:', err); + } + }; + + return ( +
+

Campaign Detail

+ + {error &&
{error}
} + + {loading ? ( +
Loading...
+ ) : ( +
+
+            {JSON.stringify(data, null, 2)}
+          
+
+ )} +
+ ); +} diff --git a/servers/keap/src/ui/react-app/src/apps/campaign-detail/index.html b/servers/keap/src/ui/react-app/src/apps/campaign-detail/index.html new file mode 100644 index 0000000..cb511d4 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/campaign-detail/index.html @@ -0,0 +1,12 @@ + + + + + + Campaign Detail - Keap + + +
+ + + diff --git a/servers/keap/src/ui/react-app/src/apps/campaign-detail/main.tsx b/servers/keap/src/ui/react-app/src/apps/campaign-detail/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/campaign-detail/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/keap/src/ui/react-app/src/apps/campaign-detail/vite.config.ts b/servers/keap/src/ui/react-app/src/apps/campaign-detail/vite.config.ts new file mode 100644 index 0000000..85b0339 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/campaign-detail/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/campaign-detail', + emptyOutDir: true, + rollupOptions: { + input: resolve(__dirname, 'index.html'), + output: { + entryFileNames: 'assets/[name].js', + chunkFileNames: 'assets/[name].js', + assetFileNames: 'assets/[name].[ext]' + } + } + } +}); diff --git a/servers/keap/src/ui/react-app/src/apps/contact-dashboard/App.tsx b/servers/keap/src/ui/react-app/src/apps/contact-dashboard/App.tsx new file mode 100644 index 0000000..86a8054 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/contact-dashboard/App.tsx @@ -0,0 +1,84 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import '../../styles/global.css'; + +export default function ContactDashboard() { + const { callTool, loading, error } = useCallTool(); + const [contacts, setContacts] = useState([]); + const [stats, setStats] = useState({ total: 0, recent: 0 }); + + useEffect(() => { + loadContacts(); + }, []); + + const loadContacts = async () => { + try { + const result = await callTool('keap_list_contacts', { limit: 10 }); + if (result?.contacts) { + setContacts(result.contacts); + setStats({ total: result.count || 0, recent: result.contacts.length }); + } + } catch (err) { + console.error('Failed to load contacts:', err); + } + }; + + return ( +
+

Contact Dashboard

+ +
+
+

Total Contacts

+
+ {stats.total} +
+
+
+

Recent Contacts

+
+ {stats.recent} +
+
+
+

Quick Actions

+ +
+
+ + {error &&
{error}
} + +
+

Recent Contacts

+ {loading ? ( +
Loading contacts...
+ ) : ( + + + + + + + + + + + + {contacts.map((contact) => ( + + + + + + + + ))} + +
NameEmailPhoneCompanyCreated
{contact.given_name} {contact.family_name}{contact.email_addresses?.[0]?.email || '-'}{contact.phone_numbers?.[0]?.number || '-'}{contact.company?.company_name || '-'}{contact.date_created ? new Date(contact.date_created).toLocaleDateString() : '-'}
+ )} +
+
+ ); +} diff --git a/servers/keap/src/ui/react-app/src/apps/contact-dashboard/index.html b/servers/keap/src/ui/react-app/src/apps/contact-dashboard/index.html new file mode 100644 index 0000000..3a43667 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/contact-dashboard/index.html @@ -0,0 +1,12 @@ + + + + + + Contact Dashboard - Keap + + +
+ + + diff --git a/servers/keap/src/ui/react-app/src/apps/contact-dashboard/main.tsx b/servers/keap/src/ui/react-app/src/apps/contact-dashboard/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/contact-dashboard/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/keap/src/ui/react-app/src/apps/contact-dashboard/vite.config.ts b/servers/keap/src/ui/react-app/src/apps/contact-dashboard/vite.config.ts new file mode 100644 index 0000000..5040fe5 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/contact-dashboard/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/contact-dashboard', + emptyOutDir: true, + rollupOptions: { + input: resolve(__dirname, 'index.html'), + output: { + entryFileNames: 'assets/[name].js', + chunkFileNames: 'assets/[name].js', + assetFileNames: 'assets/[name].[ext]' + } + } + } +}); diff --git a/servers/keap/src/ui/react-app/src/apps/contact-detail/App.tsx b/servers/keap/src/ui/react-app/src/apps/contact-detail/App.tsx new file mode 100644 index 0000000..be34593 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/contact-detail/App.tsx @@ -0,0 +1,39 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import '../../styles/global.css'; + +export default function ContactDetail() { + const { callTool, loading, error } = useCallTool(); + const [data, setData] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + const result = await callTool('keap_get_contact', {}); + setData(result); + } catch (err) { + console.error('Failed to load data:', err); + } + }; + + return ( +
+

Contact Detail

+ + {error &&
{error}
} + + {loading ? ( +
Loading...
+ ) : ( +
+
+            {JSON.stringify(data, null, 2)}
+          
+
+ )} +
+ ); +} diff --git a/servers/keap/src/ui/react-app/src/apps/contact-detail/index.html b/servers/keap/src/ui/react-app/src/apps/contact-detail/index.html new file mode 100644 index 0000000..9f934fa --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/contact-detail/index.html @@ -0,0 +1,12 @@ + + + + + + Contact Detail - Keap + + +
+ + + diff --git a/servers/keap/src/ui/react-app/src/apps/contact-detail/main.tsx b/servers/keap/src/ui/react-app/src/apps/contact-detail/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/contact-detail/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/keap/src/ui/react-app/src/apps/contact-detail/vite.config.ts b/servers/keap/src/ui/react-app/src/apps/contact-detail/vite.config.ts new file mode 100644 index 0000000..3a548cf --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/contact-detail/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/contact-detail', + emptyOutDir: true, + rollupOptions: { + input: resolve(__dirname, 'index.html'), + output: { + entryFileNames: 'assets/[name].js', + chunkFileNames: 'assets/[name].js', + assetFileNames: 'assets/[name].[ext]' + } + } + } +}); diff --git a/servers/keap/src/ui/react-app/src/apps/contact-grid/App.tsx b/servers/keap/src/ui/react-app/src/apps/contact-grid/App.tsx new file mode 100644 index 0000000..64d3643 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/contact-grid/App.tsx @@ -0,0 +1,39 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import '../../styles/global.css'; + +export default function ContactGrid() { + const { callTool, loading, error } = useCallTool(); + const [data, setData] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + const result = await callTool('keap_list_contacts', {}); + setData(result); + } catch (err) { + console.error('Failed to load data:', err); + } + }; + + return ( +
+

Contact Grid

+ + {error &&
{error}
} + + {loading ? ( +
Loading...
+ ) : ( +
+
+            {JSON.stringify(data, null, 2)}
+          
+
+ )} +
+ ); +} diff --git a/servers/keap/src/ui/react-app/src/apps/contact-grid/index.html b/servers/keap/src/ui/react-app/src/apps/contact-grid/index.html new file mode 100644 index 0000000..5912a58 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/contact-grid/index.html @@ -0,0 +1,12 @@ + + + + + + Contact Grid - Keap + + +
+ + + diff --git a/servers/keap/src/ui/react-app/src/apps/contact-grid/main.tsx b/servers/keap/src/ui/react-app/src/apps/contact-grid/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/contact-grid/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/keap/src/ui/react-app/src/apps/contact-grid/vite.config.ts b/servers/keap/src/ui/react-app/src/apps/contact-grid/vite.config.ts new file mode 100644 index 0000000..1424cbd --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/contact-grid/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/contact-grid', + emptyOutDir: true, + rollupOptions: { + input: resolve(__dirname, 'index.html'), + output: { + entryFileNames: 'assets/[name].js', + chunkFileNames: 'assets/[name].js', + assetFileNames: 'assets/[name].[ext]' + } + } + } +}); diff --git a/servers/keap/src/ui/react-app/src/apps/deal-detail/App.tsx b/servers/keap/src/ui/react-app/src/apps/deal-detail/App.tsx new file mode 100644 index 0000000..fd68e10 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/deal-detail/App.tsx @@ -0,0 +1,39 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import '../../styles/global.css'; + +export default function DealDetail() { + const { callTool, loading, error } = useCallTool(); + const [data, setData] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + const result = await callTool('keap_get_opportunity', {}); + setData(result); + } catch (err) { + console.error('Failed to load data:', err); + } + }; + + return ( +
+

Deal Detail

+ + {error &&
{error}
} + + {loading ? ( +
Loading...
+ ) : ( +
+
+            {JSON.stringify(data, null, 2)}
+          
+
+ )} +
+ ); +} diff --git a/servers/keap/src/ui/react-app/src/apps/deal-detail/index.html b/servers/keap/src/ui/react-app/src/apps/deal-detail/index.html new file mode 100644 index 0000000..ed634ca --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/deal-detail/index.html @@ -0,0 +1,12 @@ + + + + + + Deal Detail - Keap + + +
+ + + diff --git a/servers/keap/src/ui/react-app/src/apps/deal-detail/main.tsx b/servers/keap/src/ui/react-app/src/apps/deal-detail/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/deal-detail/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/keap/src/ui/react-app/src/apps/deal-detail/vite.config.ts b/servers/keap/src/ui/react-app/src/apps/deal-detail/vite.config.ts new file mode 100644 index 0000000..6c9a4ca --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/deal-detail/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/deal-detail', + emptyOutDir: true, + rollupOptions: { + input: resolve(__dirname, 'index.html'), + output: { + entryFileNames: 'assets/[name].js', + chunkFileNames: 'assets/[name].js', + assetFileNames: 'assets/[name].[ext]' + } + } + } +}); diff --git a/servers/keap/src/ui/react-app/src/apps/email-composer/App.tsx b/servers/keap/src/ui/react-app/src/apps/email-composer/App.tsx new file mode 100644 index 0000000..08366d1 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/email-composer/App.tsx @@ -0,0 +1,39 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import '../../styles/global.css'; + +export default function EmailComposer() { + const { callTool, loading, error } = useCallTool(); + const [data, setData] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + const result = await callTool('keap_send_email', {}); + setData(result); + } catch (err) { + console.error('Failed to load data:', err); + } + }; + + return ( +
+

Email Composer

+ + {error &&
{error}
} + + {loading ? ( +
Loading...
+ ) : ( +
+
+            {JSON.stringify(data, null, 2)}
+          
+
+ )} +
+ ); +} diff --git a/servers/keap/src/ui/react-app/src/apps/email-composer/index.html b/servers/keap/src/ui/react-app/src/apps/email-composer/index.html new file mode 100644 index 0000000..91e8c7b --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/email-composer/index.html @@ -0,0 +1,12 @@ + + + + + + Email Composer - Keap + + +
+ + + diff --git a/servers/keap/src/ui/react-app/src/apps/email-composer/main.tsx b/servers/keap/src/ui/react-app/src/apps/email-composer/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/email-composer/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/keap/src/ui/react-app/src/apps/email-composer/vite.config.ts b/servers/keap/src/ui/react-app/src/apps/email-composer/vite.config.ts new file mode 100644 index 0000000..82712f1 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/email-composer/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/email-composer', + emptyOutDir: true, + rollupOptions: { + input: resolve(__dirname, 'index.html'), + output: { + entryFileNames: 'assets/[name].js', + chunkFileNames: 'assets/[name].js', + assetFileNames: 'assets/[name].[ext]' + } + } + } +}); diff --git a/servers/keap/src/ui/react-app/src/apps/file-browser/App.tsx b/servers/keap/src/ui/react-app/src/apps/file-browser/App.tsx new file mode 100644 index 0000000..29b5720 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/file-browser/App.tsx @@ -0,0 +1,39 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import '../../styles/global.css'; + +export default function FileBrowser() { + const { callTool, loading, error } = useCallTool(); + const [data, setData] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + const result = await callTool('keap_list_files', {}); + setData(result); + } catch (err) { + console.error('Failed to load data:', err); + } + }; + + return ( +
+

File Browser

+ + {error &&
{error}
} + + {loading ? ( +
Loading...
+ ) : ( +
+
+            {JSON.stringify(data, null, 2)}
+          
+
+ )} +
+ ); +} diff --git a/servers/keap/src/ui/react-app/src/apps/file-browser/index.html b/servers/keap/src/ui/react-app/src/apps/file-browser/index.html new file mode 100644 index 0000000..298bbaa --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/file-browser/index.html @@ -0,0 +1,12 @@ + + + + + + File Browser - Keap + + +
+ + + diff --git a/servers/keap/src/ui/react-app/src/apps/file-browser/main.tsx b/servers/keap/src/ui/react-app/src/apps/file-browser/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/file-browser/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/keap/src/ui/react-app/src/apps/file-browser/vite.config.ts b/servers/keap/src/ui/react-app/src/apps/file-browser/vite.config.ts new file mode 100644 index 0000000..29b4335 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/file-browser/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/file-browser', + emptyOutDir: true, + rollupOptions: { + input: resolve(__dirname, 'index.html'), + output: { + entryFileNames: 'assets/[name].js', + chunkFileNames: 'assets/[name].js', + assetFileNames: 'assets/[name].[ext]' + } + } + } +}); diff --git a/servers/keap/src/ui/react-app/src/apps/order-dashboard/App.tsx b/servers/keap/src/ui/react-app/src/apps/order-dashboard/App.tsx new file mode 100644 index 0000000..5edb075 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/order-dashboard/App.tsx @@ -0,0 +1,39 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import '../../styles/global.css'; + +export default function OrderDashboard() { + const { callTool, loading, error } = useCallTool(); + const [data, setData] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + const result = await callTool('keap_list_orders', {}); + setData(result); + } catch (err) { + console.error('Failed to load data:', err); + } + }; + + return ( +
+

Order Dashboard

+ + {error &&
{error}
} + + {loading ? ( +
Loading...
+ ) : ( +
+
+            {JSON.stringify(data, null, 2)}
+          
+
+ )} +
+ ); +} diff --git a/servers/keap/src/ui/react-app/src/apps/order-dashboard/index.html b/servers/keap/src/ui/react-app/src/apps/order-dashboard/index.html new file mode 100644 index 0000000..c82d48f --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/order-dashboard/index.html @@ -0,0 +1,12 @@ + + + + + + Order Dashboard - Keap + + +
+ + + diff --git a/servers/keap/src/ui/react-app/src/apps/order-dashboard/main.tsx b/servers/keap/src/ui/react-app/src/apps/order-dashboard/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/order-dashboard/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/keap/src/ui/react-app/src/apps/order-dashboard/vite.config.ts b/servers/keap/src/ui/react-app/src/apps/order-dashboard/vite.config.ts new file mode 100644 index 0000000..a0c2439 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/order-dashboard/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/order-dashboard', + emptyOutDir: true, + rollupOptions: { + input: resolve(__dirname, 'index.html'), + output: { + entryFileNames: 'assets/[name].js', + chunkFileNames: 'assets/[name].js', + assetFileNames: 'assets/[name].[ext]' + } + } + } +}); diff --git a/servers/keap/src/ui/react-app/src/apps/order-detail/App.tsx b/servers/keap/src/ui/react-app/src/apps/order-detail/App.tsx new file mode 100644 index 0000000..810d1a7 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/order-detail/App.tsx @@ -0,0 +1,39 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import '../../styles/global.css'; + +export default function OrderDetail() { + const { callTool, loading, error } = useCallTool(); + const [data, setData] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + const result = await callTool('keap_get_order', {}); + setData(result); + } catch (err) { + console.error('Failed to load data:', err); + } + }; + + return ( +
+

Order Detail

+ + {error &&
{error}
} + + {loading ? ( +
Loading...
+ ) : ( +
+
+            {JSON.stringify(data, null, 2)}
+          
+
+ )} +
+ ); +} diff --git a/servers/keap/src/ui/react-app/src/apps/order-detail/index.html b/servers/keap/src/ui/react-app/src/apps/order-detail/index.html new file mode 100644 index 0000000..dece41e --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/order-detail/index.html @@ -0,0 +1,12 @@ + + + + + + Order Detail - Keap + + +
+ + + diff --git a/servers/keap/src/ui/react-app/src/apps/order-detail/main.tsx b/servers/keap/src/ui/react-app/src/apps/order-detail/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/order-detail/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/keap/src/ui/react-app/src/apps/order-detail/vite.config.ts b/servers/keap/src/ui/react-app/src/apps/order-detail/vite.config.ts new file mode 100644 index 0000000..2ac43c9 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/order-detail/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/order-detail', + emptyOutDir: true, + rollupOptions: { + input: resolve(__dirname, 'index.html'), + output: { + entryFileNames: 'assets/[name].js', + chunkFileNames: 'assets/[name].js', + assetFileNames: 'assets/[name].[ext]' + } + } + } +}); diff --git a/servers/keap/src/ui/react-app/src/apps/pipeline-kanban/App.tsx b/servers/keap/src/ui/react-app/src/apps/pipeline-kanban/App.tsx new file mode 100644 index 0000000..cd30814 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/pipeline-kanban/App.tsx @@ -0,0 +1,76 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import '../../styles/global.css'; + +export default function PipelineKanban() { + const { callTool, loading } = useCallTool(); + const [opportunities, setOpportunities] = useState([]); + const [stages, setStages] = useState([]); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + const [opps, stgs] = await Promise.all([ + callTool('keap_list_opportunities', { limit: 50 }), + callTool('keap_list_opportunity_stage_pipeline', {}), + ]); + + setOpportunities(opps?.opportunities || []); + setStages(stgs?.stages || []); + } catch (err) { + console.error('Failed to load data:', err); + } + }; + + const groupByStage = () => { + const grouped: any = {}; + stages.forEach(stage => { + grouped[stage.id] = opportunities.filter(opp => opp.stage?.id === stage.id); + }); + return grouped; + }; + + const stageGroups = groupByStage(); + + return ( +
+

Sales Pipeline - Kanban Board

+ + {loading ? ( +
Loading pipeline...
+ ) : ( +
+ {stages.map(stage => ( +
+

{stage.name}

+
+ {stageGroups[stage.id]?.length || 0} deals +
+ +
+ {(stageGroups[stage.id] || []).map((opp: any) => ( +
+
+ {opp.opportunity_title} +
+
+ ${opp.projected_revenue_high || opp.projected_revenue_low || 0} +
+ {opp.estimated_close_date && ( +
+ Close: {new Date(opp.estimated_close_date).toLocaleDateString()} +
+ )} +
+ ))} +
+
+ ))} +
+ )} +
+ ); +} diff --git a/servers/keap/src/ui/react-app/src/apps/pipeline-kanban/index.html b/servers/keap/src/ui/react-app/src/apps/pipeline-kanban/index.html new file mode 100644 index 0000000..9434d87 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/pipeline-kanban/index.html @@ -0,0 +1,12 @@ + + + + + + Pipeline Kanban - Keap + + +
+ + + diff --git a/servers/keap/src/ui/react-app/src/apps/pipeline-kanban/main.tsx b/servers/keap/src/ui/react-app/src/apps/pipeline-kanban/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/pipeline-kanban/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/keap/src/ui/react-app/src/apps/pipeline-kanban/vite.config.ts b/servers/keap/src/ui/react-app/src/apps/pipeline-kanban/vite.config.ts new file mode 100644 index 0000000..61f6efb --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/pipeline-kanban/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/pipeline-kanban', + emptyOutDir: true, + rollupOptions: { + input: resolve(__dirname, 'index.html'), + output: { + entryFileNames: 'assets/[name].js', + chunkFileNames: 'assets/[name].js', + assetFileNames: 'assets/[name].[ext]' + } + } + } +}); diff --git a/servers/keap/src/ui/react-app/src/apps/product-catalog/App.tsx b/servers/keap/src/ui/react-app/src/apps/product-catalog/App.tsx new file mode 100644 index 0000000..69b39d6 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/product-catalog/App.tsx @@ -0,0 +1,39 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import '../../styles/global.css'; + +export default function ProductCatalog() { + const { callTool, loading, error } = useCallTool(); + const [data, setData] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + const result = await callTool('keap_list_products', {}); + setData(result); + } catch (err) { + console.error('Failed to load data:', err); + } + }; + + return ( +
+

Product Catalog

+ + {error &&
{error}
} + + {loading ? ( +
Loading...
+ ) : ( +
+
+            {JSON.stringify(data, null, 2)}
+          
+
+ )} +
+ ); +} diff --git a/servers/keap/src/ui/react-app/src/apps/product-catalog/index.html b/servers/keap/src/ui/react-app/src/apps/product-catalog/index.html new file mode 100644 index 0000000..98e2d09 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/product-catalog/index.html @@ -0,0 +1,12 @@ + + + + + + Product Catalog - Keap + + +
+ + + diff --git a/servers/keap/src/ui/react-app/src/apps/product-catalog/main.tsx b/servers/keap/src/ui/react-app/src/apps/product-catalog/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/product-catalog/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/keap/src/ui/react-app/src/apps/product-catalog/vite.config.ts b/servers/keap/src/ui/react-app/src/apps/product-catalog/vite.config.ts new file mode 100644 index 0000000..dcd6360 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/product-catalog/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/product-catalog', + emptyOutDir: true, + rollupOptions: { + input: resolve(__dirname, 'index.html'), + output: { + entryFileNames: 'assets/[name].js', + chunkFileNames: 'assets/[name].js', + assetFileNames: 'assets/[name].[ext]' + } + } + } +}); diff --git a/servers/keap/src/ui/react-app/src/apps/settings-panel/App.tsx b/servers/keap/src/ui/react-app/src/apps/settings-panel/App.tsx new file mode 100644 index 0000000..c00b90a --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/settings-panel/App.tsx @@ -0,0 +1,39 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import '../../styles/global.css'; + +export default function SettingsPanel() { + const { callTool, loading, error } = useCallTool(); + const [data, setData] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + const result = await callTool('keap_get_account_profile', {}); + setData(result); + } catch (err) { + console.error('Failed to load data:', err); + } + }; + + return ( +
+

Settings Panel

+ + {error &&
{error}
} + + {loading ? ( +
Loading...
+ ) : ( +
+
+            {JSON.stringify(data, null, 2)}
+          
+
+ )} +
+ ); +} diff --git a/servers/keap/src/ui/react-app/src/apps/settings-panel/index.html b/servers/keap/src/ui/react-app/src/apps/settings-panel/index.html new file mode 100644 index 0000000..bd8621f --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/settings-panel/index.html @@ -0,0 +1,12 @@ + + + + + + Settings Panel - Keap + + +
+ + + diff --git a/servers/keap/src/ui/react-app/src/apps/settings-panel/main.tsx b/servers/keap/src/ui/react-app/src/apps/settings-panel/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/settings-panel/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/keap/src/ui/react-app/src/apps/settings-panel/vite.config.ts b/servers/keap/src/ui/react-app/src/apps/settings-panel/vite.config.ts new file mode 100644 index 0000000..4e2acf4 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/settings-panel/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/settings-panel', + emptyOutDir: true, + rollupOptions: { + input: resolve(__dirname, 'index.html'), + output: { + entryFileNames: 'assets/[name].js', + chunkFileNames: 'assets/[name].js', + assetFileNames: 'assets/[name].[ext]' + } + } + } +}); diff --git a/servers/keap/src/ui/react-app/src/apps/subscription-manager/App.tsx b/servers/keap/src/ui/react-app/src/apps/subscription-manager/App.tsx new file mode 100644 index 0000000..b22591a --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/subscription-manager/App.tsx @@ -0,0 +1,39 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import '../../styles/global.css'; + +export default function SubscriptionManager() { + const { callTool, loading, error } = useCallTool(); + const [data, setData] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + const result = await callTool('keap_list_subscriptions', {}); + setData(result); + } catch (err) { + console.error('Failed to load data:', err); + } + }; + + return ( +
+

Subscription Manager

+ + {error &&
{error}
} + + {loading ? ( +
Loading...
+ ) : ( +
+
+            {JSON.stringify(data, null, 2)}
+          
+
+ )} +
+ ); +} diff --git a/servers/keap/src/ui/react-app/src/apps/subscription-manager/index.html b/servers/keap/src/ui/react-app/src/apps/subscription-manager/index.html new file mode 100644 index 0000000..cab631b --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/subscription-manager/index.html @@ -0,0 +1,12 @@ + + + + + + Subscription Manager - Keap + + +
+ + + diff --git a/servers/keap/src/ui/react-app/src/apps/subscription-manager/main.tsx b/servers/keap/src/ui/react-app/src/apps/subscription-manager/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/subscription-manager/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/keap/src/ui/react-app/src/apps/subscription-manager/vite.config.ts b/servers/keap/src/ui/react-app/src/apps/subscription-manager/vite.config.ts new file mode 100644 index 0000000..50af82a --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/subscription-manager/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/subscription-manager', + emptyOutDir: true, + rollupOptions: { + input: resolve(__dirname, 'index.html'), + output: { + entryFileNames: 'assets/[name].js', + chunkFileNames: 'assets/[name].js', + assetFileNames: 'assets/[name].[ext]' + } + } + } +}); diff --git a/servers/keap/src/ui/react-app/src/apps/tag-manager/App.tsx b/servers/keap/src/ui/react-app/src/apps/tag-manager/App.tsx new file mode 100644 index 0000000..90b75b1 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/tag-manager/App.tsx @@ -0,0 +1,39 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import '../../styles/global.css'; + +export default function TagManager() { + const { callTool, loading, error } = useCallTool(); + const [data, setData] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + const result = await callTool('keap_list_tags', {}); + setData(result); + } catch (err) { + console.error('Failed to load data:', err); + } + }; + + return ( +
+

Tag Manager

+ + {error &&
{error}
} + + {loading ? ( +
Loading...
+ ) : ( +
+
+            {JSON.stringify(data, null, 2)}
+          
+
+ )} +
+ ); +} diff --git a/servers/keap/src/ui/react-app/src/apps/tag-manager/index.html b/servers/keap/src/ui/react-app/src/apps/tag-manager/index.html new file mode 100644 index 0000000..49f81b8 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/tag-manager/index.html @@ -0,0 +1,12 @@ + + + + + + Tag Manager - Keap + + +
+ + + diff --git a/servers/keap/src/ui/react-app/src/apps/tag-manager/main.tsx b/servers/keap/src/ui/react-app/src/apps/tag-manager/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/tag-manager/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/keap/src/ui/react-app/src/apps/tag-manager/vite.config.ts b/servers/keap/src/ui/react-app/src/apps/tag-manager/vite.config.ts new file mode 100644 index 0000000..5a50d11 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/tag-manager/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/tag-manager', + emptyOutDir: true, + rollupOptions: { + input: resolve(__dirname, 'index.html'), + output: { + entryFileNames: 'assets/[name].js', + chunkFileNames: 'assets/[name].js', + assetFileNames: 'assets/[name].[ext]' + } + } + } +}); diff --git a/servers/keap/src/ui/react-app/src/apps/task-manager/App.tsx b/servers/keap/src/ui/react-app/src/apps/task-manager/App.tsx new file mode 100644 index 0000000..bfdd938 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/task-manager/App.tsx @@ -0,0 +1,98 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import '../../styles/global.css'; + +export default function TaskManager() { + const { callTool, loading } = useCallTool(); + const [tasks, setTasks] = useState([]); + const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('active'); + + useEffect(() => { + loadTasks(); + }, [filter]); + + const loadTasks = async () => { + try { + const result = await callTool('keap_list_tasks', { + limit: 50, + completed: filter === 'completed' ? true : filter === 'active' ? false : undefined, + }); + setTasks(result?.tasks || []); + } catch (err) { + console.error('Failed to load tasks:', err); + } + }; + + const completeTask = async (taskId: number) => { + try { + await callTool('keap_complete_task', { task_id: taskId }); + loadTasks(); + } catch (err) { + console.error('Failed to complete task:', err); + } + }; + + return ( +
+

Task Manager

+ +
+
+ + + +
+
+ + {loading ? ( +
Loading tasks...
+ ) : ( +
+ {tasks.map(task => ( +
+
+
+

{task.title}

+ {task.description && ( +

+ {task.description} +

+ )} + {task.due_date && ( +
+ Due: {new Date(task.due_date).toLocaleDateString()} +
+ )} +
+ {!task.completed && ( + + )} +
+
+ ))} +
+ )} +
+ ); +} diff --git a/servers/keap/src/ui/react-app/src/apps/task-manager/index.html b/servers/keap/src/ui/react-app/src/apps/task-manager/index.html new file mode 100644 index 0000000..4dcf3f6 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/task-manager/index.html @@ -0,0 +1,12 @@ + + + + + + Task Manager - Keap + + +
+ + + diff --git a/servers/keap/src/ui/react-app/src/apps/task-manager/main.tsx b/servers/keap/src/ui/react-app/src/apps/task-manager/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/task-manager/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/keap/src/ui/react-app/src/apps/task-manager/vite.config.ts b/servers/keap/src/ui/react-app/src/apps/task-manager/vite.config.ts new file mode 100644 index 0000000..43f1d43 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/apps/task-manager/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/task-manager', + emptyOutDir: true, + rollupOptions: { + input: resolve(__dirname, 'index.html'), + output: { + entryFileNames: 'assets/[name].js', + chunkFileNames: 'assets/[name].js', + assetFileNames: 'assets/[name].[ext]' + } + } + } +}); diff --git a/servers/keap/src/ui/react-app/src/hooks/useCallTool.ts b/servers/keap/src/ui/react-app/src/hooks/useCallTool.ts new file mode 100644 index 0000000..8364659 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/hooks/useCallTool.ts @@ -0,0 +1,24 @@ +import { useState } from 'react'; + +export function useCallTool() { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const callTool = async (name: string, args: any) => { + setLoading(true); + setError(null); + + try { + // @ts-ignore - MCP SDK injected + const result = await window.mcp?.callServerTool?.(name, args); + setLoading(false); + return result; + } catch (err: any) { + setError(err.message || 'Failed to call tool'); + setLoading(false); + throw err; + } + }; + + return { callTool, loading, error }; +} diff --git a/servers/keap/src/ui/react-app/src/hooks/useDirtyState.ts b/servers/keap/src/ui/react-app/src/hooks/useDirtyState.ts new file mode 100644 index 0000000..dd43445 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/hooks/useDirtyState.ts @@ -0,0 +1,17 @@ +import { useState, useEffect } from 'react'; + +export function useDirtyState(initialValue: T) { + const [value, setValue] = useState(initialValue); + const [isDirty, setIsDirty] = useState(false); + + useEffect(() => { + setIsDirty(JSON.stringify(value) !== JSON.stringify(initialValue)); + }, [value, initialValue]); + + const reset = () => { + setValue(initialValue); + setIsDirty(false); + }; + + return { value, setValue, isDirty, reset }; +} diff --git a/servers/keap/src/ui/react-app/src/styles/global.css b/servers/keap/src/ui/react-app/src/styles/global.css new file mode 100644 index 0000000..73914f9 --- /dev/null +++ b/servers/keap/src/ui/react-app/src/styles/global.css @@ -0,0 +1,121 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background: var(--vscode-editor-background, #1e1e1e); + color: var(--vscode-editor-foreground, #d4d4d4); +} + +.app-container { + padding: 20px; + max-width: 1400px; + margin: 0 auto; +} + +.card { + background: var(--vscode-editor-background, #252526); + border: 1px solid var(--vscode-panel-border, #3e3e42); + border-radius: 6px; + padding: 16px; + margin-bottom: 16px; +} + +.btn { + padding: 8px 16px; + border-radius: 4px; + border: none; + cursor: pointer; + font-size: 14px; + transition: opacity 0.2s; + background: var(--vscode-button-background, #0e639c); + color: var(--vscode-button-foreground, #ffffff); +} + +.btn:hover { + opacity: 0.9; + background: var(--vscode-button-hoverBackground, #1177bb); +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-secondary { + background: var(--vscode-button-secondaryBackground, #3a3d41); + color: var(--vscode-button-secondaryForeground, #cccccc); +} + +.input { + padding: 8px 12px; + border-radius: 4px; + border: 1px solid var(--vscode-input-border, #3e3e42); + background: var(--vscode-input-background, #3c3c3c); + color: var(--vscode-input-foreground, #cccccc); + font-size: 14px; + width: 100%; +} + +.input:focus { + outline: 1px solid var(--vscode-focusBorder, #007acc); +} + +.grid { + display: grid; + gap: 16px; +} + +.grid-2 { + grid-template-columns: repeat(2, 1fr); +} + +.grid-3 { + grid-template-columns: repeat(3, 1fr); +} + +.grid-4 { + grid-template-columns: repeat(4, 1fr); +} + +.loading { + text-align: center; + padding: 40px; + color: var(--vscode-descriptionForeground, #999999); +} + +.error { + color: var(--vscode-errorForeground, #f48771); + padding: 12px; + background: var(--vscode-inputValidation-errorBackground, #5a1d1d); + border: 1px solid var(--vscode-inputValidation-errorBorder, #be1100); + border-radius: 4px; + margin-bottom: 16px; +} + +table { + width: 100%; + border-collapse: collapse; +} + +th, td { + text-align: left; + padding: 12px; + border-bottom: 1px solid var(--vscode-panel-border, #3e3e42); +} + +th { + font-weight: 600; + color: var(--vscode-foreground, #cccccc); +} + +tr:hover { + background: var(--vscode-list-hoverBackground, #2a2d2e); +} diff --git a/servers/keap/src/ui/react-app/tsconfig.json b/servers/keap/src/ui/react-app/tsconfig.json new file mode 100644 index 0000000..a4c834a --- /dev/null +++ b/servers/keap/src/ui/react-app/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/servers/keap/tsconfig.json b/servers/keap/tsconfig.json index 6fe6699..b534769 100644 --- a/servers/keap/tsconfig.json +++ b/servers/keap/tsconfig.json @@ -2,8 +2,8 @@ "compilerOptions": { "target": "ES2022", "module": "Node16", - "lib": ["ES2022", "DOM", "DOM.Iterable"], "moduleResolution": "Node16", + "lib": ["ES2022"], "outDir": "./dist", "rootDir": "./src", "strict": true, @@ -13,9 +13,8 @@ "resolveJsonModule": true, "declaration": true, "declarationMap": true, - "sourceMap": true, - "jsx": "react" + "sourceMap": true }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "src/ui"] }