helpscout: Complete MCP server with 47 tools and 18 apps
- Comprehensive HelpScout Mailbox API v2 integration - 47 tools across 10 categories: conversations, customers, mailboxes, users, tags, workflows, saved-replies, teams, webhooks, reporting - 18 interactive MCP apps: dashboards, detail views, reports, managers - OAuth2 authentication with automatic token refresh - Full pagination support and error handling - Complete TypeScript types for all API entities
This commit is contained in:
parent
1dd639f67f
commit
ec4a7475d9
5
servers/helpscout/.env.example
Normal file
5
servers/helpscout/.env.example
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# HelpScout API Credentials
|
||||||
|
# Get these from: https://secure.helpscout.net/apps/
|
||||||
|
|
||||||
|
HELPSCOUT_APP_ID=your_app_id_here
|
||||||
|
HELPSCOUT_APP_SECRET=your_app_secret_here
|
||||||
5
servers/helpscout/.gitignore
vendored
Normal file
5
servers/helpscout/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
239
servers/helpscout/README.md
Normal file
239
servers/helpscout/README.md
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
# HelpScout MCP Server
|
||||||
|
|
||||||
|
A comprehensive Model Context Protocol (MCP) server for HelpScout's Mailbox API v2. This server provides 47+ tools and 18 interactive MCP apps for complete customer support management.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### 🛠️ Tools (47)
|
||||||
|
|
||||||
|
#### Conversations (12 tools)
|
||||||
|
- List, get, create, update, delete conversations
|
||||||
|
- Manage threads (list, create reply, create note, create phone)
|
||||||
|
- Update tags, change status, assign conversations
|
||||||
|
|
||||||
|
#### Customers (12 tools)
|
||||||
|
- List, get, create, update, delete customers
|
||||||
|
- Manage customer emails, phones, addresses
|
||||||
|
- List customer properties
|
||||||
|
|
||||||
|
#### Mailboxes (5 tools)
|
||||||
|
- List and get mailboxes
|
||||||
|
- List folders and custom fields
|
||||||
|
|
||||||
|
#### Users (3 tools)
|
||||||
|
- List and get users
|
||||||
|
- Get current authenticated user
|
||||||
|
|
||||||
|
#### Tags (4 tools)
|
||||||
|
- List, create, update, delete tags
|
||||||
|
|
||||||
|
#### Workflows (5 tools)
|
||||||
|
- List, get workflows
|
||||||
|
- Activate/deactivate workflows
|
||||||
|
- Get workflow statistics
|
||||||
|
|
||||||
|
#### Saved Replies (5 tools)
|
||||||
|
- List, get, create, update, delete saved replies
|
||||||
|
|
||||||
|
#### Teams (3 tools)
|
||||||
|
- List, get teams
|
||||||
|
- List team members
|
||||||
|
|
||||||
|
#### Webhooks (5 tools)
|
||||||
|
- List, get, create, update, delete webhooks
|
||||||
|
|
||||||
|
#### Reporting (5 tools)
|
||||||
|
- Company overview report
|
||||||
|
- Conversations report
|
||||||
|
- Happiness report
|
||||||
|
- Productivity report
|
||||||
|
- User-specific reports
|
||||||
|
|
||||||
|
### 📱 MCP Apps (18)
|
||||||
|
|
||||||
|
1. **conversation-dashboard** - Dashboard view of conversations with filters
|
||||||
|
2. **conversation-detail** - Detailed single conversation view
|
||||||
|
3. **conversation-grid** - Card-based grid layout
|
||||||
|
4. **conversation-timeline** - Visual timeline of conversation history
|
||||||
|
5. **customer-grid** - Grid view of all customers
|
||||||
|
6. **customer-detail** - Detailed customer profile
|
||||||
|
7. **mailbox-overview** - Overview of all mailboxes
|
||||||
|
8. **folder-browser** - Browse and manage folders
|
||||||
|
9. **user-stats** - Individual user performance metrics
|
||||||
|
10. **tag-manager** - Manage tags with usage stats
|
||||||
|
11. **workflow-dashboard** - Workflow management dashboard
|
||||||
|
12. **workflow-detail** - Detailed workflow configuration
|
||||||
|
13. **saved-replies** - Browse and manage saved replies
|
||||||
|
14. **team-overview** - Team structure and member stats
|
||||||
|
15. **happiness-report** - Customer satisfaction metrics
|
||||||
|
16. **productivity-report** - Team productivity metrics
|
||||||
|
17. **company-report** - Overall company performance KPIs
|
||||||
|
18. **search-results** - Search interface for conversations
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Create a `.env` file with your HelpScout credentials:
|
||||||
|
|
||||||
|
```env
|
||||||
|
HELPSCOUT_APP_ID=your_app_id
|
||||||
|
HELPSCOUT_APP_SECRET=your_app_secret
|
||||||
|
```
|
||||||
|
|
||||||
|
### Getting HelpScout Credentials
|
||||||
|
|
||||||
|
1. Go to https://secure.helpscout.net/apps/
|
||||||
|
2. Create a new app
|
||||||
|
3. Copy the App ID and App Secret
|
||||||
|
4. Add them to your `.env` file
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### With MCP Client
|
||||||
|
|
||||||
|
Add to your MCP client configuration:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"helpscout": {
|
||||||
|
"command": "node",
|
||||||
|
"args": ["/path/to/helpscout/dist/main.js"],
|
||||||
|
"env": {
|
||||||
|
"HELPSCOUT_APP_ID": "your_app_id",
|
||||||
|
"HELPSCOUT_APP_SECRET": "your_app_secret"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Standalone
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Coverage
|
||||||
|
|
||||||
|
This server implements the HelpScout Mailbox API v2:
|
||||||
|
- **Base URL**: https://api.helpscout.net/v2/
|
||||||
|
- **Authentication**: OAuth2 Client Credentials
|
||||||
|
- **Features**: Pagination, error handling, automatic token refresh
|
||||||
|
|
||||||
|
## Tool Examples
|
||||||
|
|
||||||
|
### List Active Conversations
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"name": "helpscout_list_conversations",
|
||||||
|
"arguments": {
|
||||||
|
"status": "active",
|
||||||
|
"mailbox": 123456
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create a Conversation
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"name": "helpscout_create_conversation",
|
||||||
|
"arguments": {
|
||||||
|
"subject": "Need help with billing",
|
||||||
|
"type": "email",
|
||||||
|
"mailboxId": 123456,
|
||||||
|
"customerEmail": "customer@example.com",
|
||||||
|
"status": "active"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Happiness Report
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"name": "helpscout_get_happiness_report",
|
||||||
|
"arguments": {
|
||||||
|
"start": "2025-01-01",
|
||||||
|
"end": "2025-01-31"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## MCP Apps Usage
|
||||||
|
|
||||||
|
Access MCP apps as resources:
|
||||||
|
|
||||||
|
```
|
||||||
|
helpscout://app/conversation-dashboard
|
||||||
|
helpscout://app/customer-detail
|
||||||
|
helpscout://app/happiness-report
|
||||||
|
```
|
||||||
|
|
||||||
|
Each app provides a rich HTML interface for interacting with HelpScout data.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Watch mode
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Prepare for publishing
|
||||||
|
npm run prepare
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── api/
|
||||||
|
│ └── client.ts # HelpScout API client with OAuth2
|
||||||
|
├── tools/
|
||||||
|
│ ├── conversations-tools.ts
|
||||||
|
│ ├── customers-tools.ts
|
||||||
|
│ ├── mailboxes-tools.ts
|
||||||
|
│ ├── users-tools.ts
|
||||||
|
│ ├── tags-tools.ts
|
||||||
|
│ ├── workflows-tools.ts
|
||||||
|
│ ├── saved-replies-tools.ts
|
||||||
|
│ ├── teams-tools.ts
|
||||||
|
│ ├── webhooks-tools.ts
|
||||||
|
│ └── reporting-tools.ts
|
||||||
|
├── apps/
|
||||||
|
│ ├── conversation-dashboard.ts
|
||||||
|
│ ├── conversation-detail.ts
|
||||||
|
│ └── ... (18 apps total)
|
||||||
|
├── types/
|
||||||
|
│ └── index.ts # TypeScript type definitions
|
||||||
|
├── server.ts # MCP server implementation
|
||||||
|
└── main.ts # Entry point
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
The server includes comprehensive error handling:
|
||||||
|
- API errors are caught and returned with detailed messages
|
||||||
|
- Token refresh is automatic
|
||||||
|
- Pagination handles large datasets gracefully
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
This is part of the MCPEngine project. For contributions, please refer to the main repository guidelines.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- [HelpScout API Documentation](https://developer.helpscout.com/)
|
||||||
|
- [MCP Documentation](https://modelcontextprotocol.io/)
|
||||||
|
- [MCPEngine](https://github.com/BusyBee3333/mcpengine)
|
||||||
@ -1,20 +1,34 @@
|
|||||||
{
|
{
|
||||||
"name": "mcp-server-helpscout",
|
"name": "@mcpengine/helpscout-server",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"description": "HelpScout MCP Server - Complete Mailbox API v2 integration",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/main.js",
|
||||||
|
"bin": {
|
||||||
|
"helpscout-mcp": "./dist/main.js"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"start": "node dist/index.js",
|
"dev": "tsc --watch",
|
||||||
"dev": "tsx src/index.ts"
|
"start": "node dist/main.js",
|
||||||
|
"prepare": "npm run build"
|
||||||
},
|
},
|
||||||
|
"keywords": [
|
||||||
|
"mcp",
|
||||||
|
"helpscout",
|
||||||
|
"customer-support",
|
||||||
|
"mailbox",
|
||||||
|
"conversations"
|
||||||
|
],
|
||||||
|
"author": "MCPEngine",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^0.5.0",
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||||
"zod": "^3.22.4"
|
"axios": "^1.7.9",
|
||||||
|
"dotenv": "^16.4.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.10.0",
|
"@types/node": "^22.10.2",
|
||||||
"tsx": "^4.7.0",
|
"typescript": "^5.7.2"
|
||||||
"typescript": "^5.3.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
138
servers/helpscout/src/api/client.ts
Normal file
138
servers/helpscout/src/api/client.ts
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||||
|
import type {
|
||||||
|
HelpScoutConfig,
|
||||||
|
AccessTokenResponse,
|
||||||
|
PaginatedResponse
|
||||||
|
} from '../types/index.js';
|
||||||
|
|
||||||
|
export class HelpScoutClient {
|
||||||
|
private config: HelpScoutConfig;
|
||||||
|
private client: AxiosInstance;
|
||||||
|
private baseURL = 'https://api.helpscout.net/v2';
|
||||||
|
|
||||||
|
constructor(config: HelpScoutConfig) {
|
||||||
|
this.config = config;
|
||||||
|
this.client = axios.create({
|
||||||
|
baseURL: this.baseURL,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add auth interceptor
|
||||||
|
this.client.interceptors.request.use(async (config) => {
|
||||||
|
const token = await this.getAccessToken();
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add error handler
|
||||||
|
this.client.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
(error) => {
|
||||||
|
if (error.response) {
|
||||||
|
const { status, data } = error.response;
|
||||||
|
throw new Error(
|
||||||
|
`HelpScout API Error (${status}): ${JSON.stringify(data)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getAccessToken(): Promise<string> {
|
||||||
|
// Check if we have a valid token
|
||||||
|
if (this.config.accessToken && this.config.tokenExpiry) {
|
||||||
|
if (Date.now() < this.config.tokenExpiry) {
|
||||||
|
return this.config.accessToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get new token
|
||||||
|
const response = await axios.post<AccessTokenResponse>(
|
||||||
|
'https://api.helpscout.net/v2/oauth2/token',
|
||||||
|
{
|
||||||
|
grant_type: 'client_credentials',
|
||||||
|
client_id: this.config.appId,
|
||||||
|
client_secret: this.config.appSecret,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.config.accessToken = response.data.access_token;
|
||||||
|
this.config.tokenExpiry = Date.now() + response.data.expires_in * 1000;
|
||||||
|
|
||||||
|
return this.config.accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
async get<T>(path: string, params?: any): Promise<T> {
|
||||||
|
const response = await this.client.get<T>(path, { params });
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async post<T>(path: string, data?: any): Promise<T> {
|
||||||
|
const response = await this.client.post<T>(path, data);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async put<T>(path: string, data?: any): Promise<T> {
|
||||||
|
const response = await this.client.put<T>(path, data);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async patch<T>(path: string, data?: any): Promise<T> {
|
||||||
|
const response = await this.client.patch<T>(path, data);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(path: string): Promise<void> {
|
||||||
|
await this.client.delete(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
async *paginate<T>(
|
||||||
|
path: string,
|
||||||
|
params?: any,
|
||||||
|
embedKey?: string
|
||||||
|
): AsyncGenerator<T[], void, unknown> {
|
||||||
|
let nextUrl: string | undefined = path;
|
||||||
|
let page = 1;
|
||||||
|
|
||||||
|
while (nextUrl) {
|
||||||
|
const fullParams = { ...params, page };
|
||||||
|
const response = await this.get<PaginatedResponse<T>>(nextUrl, fullParams);
|
||||||
|
|
||||||
|
// Extract items from embedded response
|
||||||
|
let items: T[] = [];
|
||||||
|
if (response._embedded && embedKey && response._embedded[embedKey]) {
|
||||||
|
items = response._embedded[embedKey];
|
||||||
|
} else if (Array.isArray(response)) {
|
||||||
|
items = response as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield items;
|
||||||
|
|
||||||
|
// Check for next page
|
||||||
|
if (
|
||||||
|
response._links?.next &&
|
||||||
|
response.page &&
|
||||||
|
response.page.number < response.page.totalPages
|
||||||
|
) {
|
||||||
|
page++;
|
||||||
|
} else {
|
||||||
|
nextUrl = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllPages<T>(
|
||||||
|
path: string,
|
||||||
|
params?: any,
|
||||||
|
embedKey?: string
|
||||||
|
): Promise<T[]> {
|
||||||
|
const allItems: T[] = [];
|
||||||
|
for await (const items of this.paginate<T>(path, params, embedKey)) {
|
||||||
|
allItems.push(...items);
|
||||||
|
}
|
||||||
|
return allItems;
|
||||||
|
}
|
||||||
|
}
|
||||||
105
servers/helpscout/src/apps/company-report.ts
Normal file
105
servers/helpscout/src/apps/company-report.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
export const companyReportApp = {
|
||||||
|
name: 'company-report',
|
||||||
|
description: 'Overall company performance and KPIs',
|
||||||
|
content: `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Company Report</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f7f9fc; padding: 20px; }
|
||||||
|
.container { max-width: 1400px; margin: 0 auto; }
|
||||||
|
h1 { font-size: 28px; color: #1f2d3d; margin-bottom: 8px; }
|
||||||
|
.subtitle { font-size: 14px; color: #6f7b8a; margin-bottom: 24px; }
|
||||||
|
.kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }
|
||||||
|
.kpi-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||||
|
.kpi-value { font-size: 32px; font-weight: bold; color: #1f2d3d; margin-bottom: 4px; }
|
||||||
|
.kpi-label { font-size: 13px; color: #6f7b8a; margin-bottom: 8px; }
|
||||||
|
.kpi-change { font-size: 12px; font-weight: 500; }
|
||||||
|
.kpi-change.positive { color: #10b981; }
|
||||||
|
.kpi-change.negative { color: #ef4444; }
|
||||||
|
.sections-grid { display: grid; grid-template-columns: 2fr 1fr; gap: 20px; }
|
||||||
|
.section { background: white; padding: 24px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||||
|
.section-title { font-size: 18px; font-weight: 600; color: #1f2d3d; margin-bottom: 16px; }
|
||||||
|
.chart-placeholder { height: 200px; background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); border-radius: 6px; display: flex; align-items: center; justify-content: center; color: #0369a1; font-size: 14px; }
|
||||||
|
.summary-item { padding: 12px 0; border-bottom: 1px solid #f0f2f5; display: flex; justify-content: space-between; }
|
||||||
|
.summary-item:last-child { border-bottom: none; }
|
||||||
|
.summary-label { font-size: 14px; color: #6f7b8a; }
|
||||||
|
.summary-value { font-size: 14px; font-weight: 600; color: #1f2d3d; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🏢 Company Report</h1>
|
||||||
|
<div class="subtitle">Overall performance overview for last 30 days</div>
|
||||||
|
|
||||||
|
<div class="kpi-grid">
|
||||||
|
<div class="kpi-card">
|
||||||
|
<div class="kpi-label">CONVERSATIONS CREATED</div>
|
||||||
|
<div class="kpi-value">1,524</div>
|
||||||
|
<div class="kpi-change positive">↑ 15% vs last month</div>
|
||||||
|
</div>
|
||||||
|
<div class="kpi-card">
|
||||||
|
<div class="kpi-label">CONVERSATIONS CLOSED</div>
|
||||||
|
<div class="kpi-value">1,389</div>
|
||||||
|
<div class="kpi-change positive">↑ 12% vs last month</div>
|
||||||
|
</div>
|
||||||
|
<div class="kpi-card">
|
||||||
|
<div class="kpi-label">CUSTOMERS HELPED</div>
|
||||||
|
<div class="kpi-value">847</div>
|
||||||
|
<div class="kpi-change positive">↑ 8% vs last month</div>
|
||||||
|
</div>
|
||||||
|
<div class="kpi-card">
|
||||||
|
<div class="kpi-label">HAPPINESS SCORE</div>
|
||||||
|
<div class="kpi-value">94%</div>
|
||||||
|
<div class="kpi-change positive">↑ 3% vs last month</div>
|
||||||
|
</div>
|
||||||
|
<div class="kpi-card">
|
||||||
|
<div class="kpi-label">REPLIES SENT</div>
|
||||||
|
<div class="kpi-value">3,247</div>
|
||||||
|
<div class="kpi-change positive">↑ 18% vs last month</div>
|
||||||
|
</div>
|
||||||
|
<div class="kpi-card">
|
||||||
|
<div class="kpi-label">AVG RESOLUTION TIME</div>
|
||||||
|
<div class="kpi-value">8.4h</div>
|
||||||
|
<div class="kpi-change negative">↑ 1.2h vs last month</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sections-grid">
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Conversation Volume Trend</div>
|
||||||
|
<div class="chart-placeholder">📈 7-day conversation trend chart</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Key Metrics Summary</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="summary-label">First Response Time</span>
|
||||||
|
<span class="summary-value">3.2 hours</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="summary-label">Active Conversations</span>
|
||||||
|
<span class="summary-value">147</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="summary-label">Pending Conversations</span>
|
||||||
|
<span class="summary-value">23</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="summary-label">Team Members</span>
|
||||||
|
<span class="summary-value">12 agents</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="summary-label">Active Mailboxes</span>
|
||||||
|
<span class="summary-value">3</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
};
|
||||||
141
servers/helpscout/src/apps/conversation-dashboard.ts
Normal file
141
servers/helpscout/src/apps/conversation-dashboard.ts
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
export const conversationDashboardApp = {
|
||||||
|
name: 'conversation-dashboard',
|
||||||
|
description: 'Dashboard view of conversations with filters and quick actions',
|
||||||
|
content: `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Conversation Dashboard</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #f7f9fc;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.dashboard { max-width: 1400px; margin: 0 auto; }
|
||||||
|
h1 { font-size: 28px; color: #1f2d3d; margin-bottom: 20px; }
|
||||||
|
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }
|
||||||
|
.stat-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||||
|
.stat-value { font-size: 32px; font-weight: bold; color: #3197d6; }
|
||||||
|
.stat-label { font-size: 14px; color: #6f7b8a; margin-top: 4px; }
|
||||||
|
.filters { background: white; padding: 16px; border-radius: 8px; margin-bottom: 20px; display: flex; gap: 12px; flex-wrap: wrap; }
|
||||||
|
.filter-group { display: flex; flex-direction: column; gap: 4px; }
|
||||||
|
.filter-group label { font-size: 12px; color: #6f7b8a; }
|
||||||
|
.filter-group select, .filter-group input { padding: 8px; border: 1px solid #d9dee4; border-radius: 4px; font-size: 14px; }
|
||||||
|
.conversations { background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||||
|
.conv-header { display: grid; grid-template-columns: 1fr 150px 120px 100px 100px; padding: 12px 16px; background: #f7f9fc; border-bottom: 1px solid #d9dee4; font-size: 12px; font-weight: 600; color: #6f7b8a; }
|
||||||
|
.conv-row { display: grid; grid-template-columns: 1fr 150px 120px 100px 100px; padding: 16px; border-bottom: 1px solid #f0f2f5; align-items: center; cursor: pointer; transition: background 0.2s; }
|
||||||
|
.conv-row:hover { background: #f7f9fc; }
|
||||||
|
.conv-subject { font-weight: 500; color: #1f2d3d; }
|
||||||
|
.conv-preview { font-size: 13px; color: #6f7b8a; margin-top: 4px; }
|
||||||
|
.conv-customer { font-size: 14px; color: #3197d6; }
|
||||||
|
.conv-assignee { font-size: 14px; color: #6f7b8a; }
|
||||||
|
.status-badge { padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: 500; text-align: center; }
|
||||||
|
.status-active { background: #d4f4dd; color: #0a6640; }
|
||||||
|
.status-pending { background: #fff4cc; color: #b45309; }
|
||||||
|
.status-closed { background: #e5e7eb; color: #4b5563; }
|
||||||
|
.conv-date { font-size: 13px; color: #6f7b8a; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="dashboard">
|
||||||
|
<h1>📬 Conversation Dashboard</h1>
|
||||||
|
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value">147</div>
|
||||||
|
<div class="stat-label">Active Conversations</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value">23</div>
|
||||||
|
<div class="stat-label">Pending</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value">8</div>
|
||||||
|
<div class="stat-label">Unassigned</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value">4.2h</div>
|
||||||
|
<div class="stat-label">Avg Response Time</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filters">
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>Status</label>
|
||||||
|
<select>
|
||||||
|
<option>All</option>
|
||||||
|
<option>Active</option>
|
||||||
|
<option>Pending</option>
|
||||||
|
<option>Closed</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>Mailbox</label>
|
||||||
|
<select>
|
||||||
|
<option>All Mailboxes</option>
|
||||||
|
<option>Support</option>
|
||||||
|
<option>Sales</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>Assigned To</label>
|
||||||
|
<select>
|
||||||
|
<option>Everyone</option>
|
||||||
|
<option>Me</option>
|
||||||
|
<option>Unassigned</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>Search</label>
|
||||||
|
<input type="text" placeholder="Search conversations...">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="conversations">
|
||||||
|
<div class="conv-header">
|
||||||
|
<div>SUBJECT</div>
|
||||||
|
<div>CUSTOMER</div>
|
||||||
|
<div>ASSIGNED</div>
|
||||||
|
<div>STATUS</div>
|
||||||
|
<div>UPDATED</div>
|
||||||
|
</div>
|
||||||
|
<div class="conv-row" onclick="alert('Open conversation detail')">
|
||||||
|
<div>
|
||||||
|
<div class="conv-subject">Payment issue with subscription</div>
|
||||||
|
<div class="conv-preview">Customer having trouble with their credit card...</div>
|
||||||
|
</div>
|
||||||
|
<div class="conv-customer">Sarah Chen</div>
|
||||||
|
<div class="conv-assignee">John Smith</div>
|
||||||
|
<div><span class="status-badge status-active">Active</span></div>
|
||||||
|
<div class="conv-date">2 hours ago</div>
|
||||||
|
</div>
|
||||||
|
<div class="conv-row" onclick="alert('Open conversation detail')">
|
||||||
|
<div>
|
||||||
|
<div class="conv-subject">Feature request: Dark mode</div>
|
||||||
|
<div class="conv-preview">Would love to have a dark mode option...</div>
|
||||||
|
</div>
|
||||||
|
<div class="conv-customer">Mike Johnson</div>
|
||||||
|
<div class="conv-assignee">Emily Davis</div>
|
||||||
|
<div><span class="status-badge status-pending">Pending</span></div>
|
||||||
|
<div class="conv-date">5 hours ago</div>
|
||||||
|
</div>
|
||||||
|
<div class="conv-row" onclick="alert('Open conversation detail')">
|
||||||
|
<div>
|
||||||
|
<div class="conv-subject">Login problems</div>
|
||||||
|
<div class="conv-preview">Cannot log in with my account credentials...</div>
|
||||||
|
</div>
|
||||||
|
<div class="conv-customer">Alex Brown</div>
|
||||||
|
<div class="conv-assignee">—</div>
|
||||||
|
<div><span class="status-badge status-active">Active</span></div>
|
||||||
|
<div class="conv-date">1 day ago</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
};
|
||||||
134
servers/helpscout/src/apps/conversation-detail.ts
Normal file
134
servers/helpscout/src/apps/conversation-detail.ts
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
export const conversationDetailApp = {
|
||||||
|
name: 'conversation-detail',
|
||||||
|
description: 'Detailed view of a single conversation with thread history and actions',
|
||||||
|
content: `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Conversation Detail</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #f7f9fc;
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
.sidebar { width: 300px; background: white; border-right: 1px solid #d9dee4; padding: 20px; overflow-y: auto; }
|
||||||
|
.main { flex: 1; display: flex; flex-direction: column; }
|
||||||
|
.header { background: white; padding: 16px 24px; border-bottom: 1px solid #d9dee4; display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.subject { font-size: 20px; font-weight: 600; color: #1f2d3d; }
|
||||||
|
.actions { display: flex; gap: 8px; }
|
||||||
|
.btn { padding: 8px 16px; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; border: 1px solid #d9dee4; background: white; }
|
||||||
|
.btn:hover { background: #f7f9fc; }
|
||||||
|
.btn-primary { background: #3197d6; color: white; border: none; }
|
||||||
|
.btn-primary:hover { background: #2578af; }
|
||||||
|
.threads { flex: 1; overflow-y: auto; padding: 24px; }
|
||||||
|
.thread { background: white; border-radius: 8px; padding: 20px; margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||||
|
.thread-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
|
||||||
|
.thread-author { font-weight: 600; color: #1f2d3d; }
|
||||||
|
.thread-time { font-size: 13px; color: #6f7b8a; }
|
||||||
|
.thread-body { color: #3e4c59; line-height: 1.6; }
|
||||||
|
.thread-note { background: #fff9e6; border-left: 4px solid #f59e0b; }
|
||||||
|
.sidebar-section { margin-bottom: 24px; }
|
||||||
|
.sidebar-label { font-size: 12px; font-weight: 600; color: #6f7b8a; margin-bottom: 8px; text-transform: uppercase; }
|
||||||
|
.sidebar-value { font-size: 14px; color: #1f2d3d; }
|
||||||
|
.tag { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 12px; background: #e0e7ff; color: #3730a3; margin-right: 4px; margin-bottom: 4px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="sidebar-section">
|
||||||
|
<div class="sidebar-label">Customer</div>
|
||||||
|
<div class="sidebar-value" style="color: #3197d6; font-weight: 500;">Sarah Chen</div>
|
||||||
|
<div style="font-size: 13px; color: #6f7b8a; margin-top: 4px;">sarah.chen@example.com</div>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-section">
|
||||||
|
<div class="sidebar-label">Status</div>
|
||||||
|
<div class="sidebar-value">
|
||||||
|
<select style="width: 100%; padding: 8px; border: 1px solid #d9dee4; border-radius: 4px;">
|
||||||
|
<option>Active</option>
|
||||||
|
<option>Pending</option>
|
||||||
|
<option>Closed</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-section">
|
||||||
|
<div class="sidebar-label">Assigned To</div>
|
||||||
|
<div class="sidebar-value">
|
||||||
|
<select style="width: 100%; padding: 8px; border: 1px solid #d9dee4; border-radius: 4px;">
|
||||||
|
<option>John Smith</option>
|
||||||
|
<option>Emily Davis</option>
|
||||||
|
<option>Unassigned</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-section">
|
||||||
|
<div class="sidebar-label">Tags</div>
|
||||||
|
<div>
|
||||||
|
<span class="tag">billing</span>
|
||||||
|
<span class="tag">urgent</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-section">
|
||||||
|
<div class="sidebar-label">Mailbox</div>
|
||||||
|
<div class="sidebar-value">Support</div>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-section">
|
||||||
|
<div class="sidebar-label">Created</div>
|
||||||
|
<div class="sidebar-value" style="font-size: 13px;">Jan 15, 2025 2:34 PM</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main">
|
||||||
|
<div class="header">
|
||||||
|
<div class="subject">Payment issue with subscription</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn">Add Note</button>
|
||||||
|
<button class="btn btn-primary">Reply</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="threads">
|
||||||
|
<div class="thread">
|
||||||
|
<div class="thread-header">
|
||||||
|
<div class="thread-author">Sarah Chen (Customer)</div>
|
||||||
|
<div class="thread-time">2 hours ago</div>
|
||||||
|
</div>
|
||||||
|
<div class="thread-body">
|
||||||
|
Hi, I'm having trouble updating my payment method. Every time I try to save my new credit card, I get an error message. Can you help?
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="thread thread-note">
|
||||||
|
<div class="thread-header">
|
||||||
|
<div class="thread-author">🔒 John Smith (Internal Note)</div>
|
||||||
|
<div class="thread-time">1 hour ago</div>
|
||||||
|
</div>
|
||||||
|
<div class="thread-body">
|
||||||
|
Checking with billing team - seems like a known issue with Visa cards from Canada. Should have a fix deployed by EOD.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="thread">
|
||||||
|
<div class="thread-header">
|
||||||
|
<div class="thread-author">John Smith (Agent)</div>
|
||||||
|
<div class="thread-time">30 minutes ago</div>
|
||||||
|
</div>
|
||||||
|
<div class="thread-body">
|
||||||
|
Hi Sarah,<br><br>
|
||||||
|
Thanks for reaching out! I've identified the issue - there's a temporary problem with processing Canadian Visa cards. Our engineering team is working on a fix that should be live within the next few hours.<br><br>
|
||||||
|
In the meantime, if you have an alternative payment method (Mastercard or American Express), that should work without issues.<br><br>
|
||||||
|
I'll follow up once the fix is deployed!<br><br>
|
||||||
|
Best regards,<br>
|
||||||
|
John
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
};
|
||||||
104
servers/helpscout/src/apps/conversation-grid.ts
Normal file
104
servers/helpscout/src/apps/conversation-grid.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
export const conversationGridApp = {
|
||||||
|
name: 'conversation-grid',
|
||||||
|
description: 'Alternative grid layout for conversations with card view',
|
||||||
|
content: `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Conversations Grid</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f7f9fc; padding: 20px; }
|
||||||
|
.container { max-width: 1400px; margin: 0 auto; }
|
||||||
|
h1 { font-size: 28px; color: #1f2d3d; margin-bottom: 20px; }
|
||||||
|
.conv-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 16px; }
|
||||||
|
.conv-card { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); cursor: pointer; transition: transform 0.2s; }
|
||||||
|
.conv-card:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.15); }
|
||||||
|
.conv-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; }
|
||||||
|
.conv-number { font-size: 12px; color: #9ca3af; font-weight: 500; }
|
||||||
|
.status-badge { padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 500; }
|
||||||
|
.status-active { background: #d4f4dd; color: #0a6640; }
|
||||||
|
.status-pending { background: #fff4cc; color: #b45309; }
|
||||||
|
.conv-subject { font-size: 16px; font-weight: 600; color: #1f2d3d; margin-bottom: 8px; }
|
||||||
|
.conv-preview { font-size: 13px; color: #6f7b8a; line-height: 1.5; margin-bottom: 16px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||||
|
.conv-footer { display: flex; justify-content: space-between; align-items: center; padding-top: 12px; border-top: 1px solid #f0f2f5; }
|
||||||
|
.customer-info { display: flex; align-items: center; gap: 8px; }
|
||||||
|
.customer-avatar { width: 28px; height: 28px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; font-size: 11px; font-weight: 600; }
|
||||||
|
.customer-name { font-size: 13px; color: #3197d6; font-weight: 500; }
|
||||||
|
.conv-time { font-size: 12px; color: #9ca3af; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>📬 Conversations</h1>
|
||||||
|
<div class="conv-grid">
|
||||||
|
<div class="conv-card" onclick="alert('Open conversation')">
|
||||||
|
<div class="conv-header">
|
||||||
|
<span class="conv-number">#1247</span>
|
||||||
|
<span class="status-badge status-active">Active</span>
|
||||||
|
</div>
|
||||||
|
<div class="conv-subject">Payment issue with subscription</div>
|
||||||
|
<div class="conv-preview">Customer having trouble with their credit card. Error appears when trying to update payment method...</div>
|
||||||
|
<div class="conv-footer">
|
||||||
|
<div class="customer-info">
|
||||||
|
<div class="customer-avatar">SC</div>
|
||||||
|
<span class="customer-name">Sarah Chen</span>
|
||||||
|
</div>
|
||||||
|
<span class="conv-time">2h ago</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="conv-card" onclick="alert('Open conversation')">
|
||||||
|
<div class="conv-header">
|
||||||
|
<span class="conv-number">#1246</span>
|
||||||
|
<span class="status-badge status-pending">Pending</span>
|
||||||
|
</div>
|
||||||
|
<div class="conv-subject">Feature request: Dark mode</div>
|
||||||
|
<div class="conv-preview">Would love to have a dark mode option for the application. Many users work late hours...</div>
|
||||||
|
<div class="conv-footer">
|
||||||
|
<div class="customer-info">
|
||||||
|
<div class="customer-avatar" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">MJ</div>
|
||||||
|
<span class="customer-name">Mike Johnson</span>
|
||||||
|
</div>
|
||||||
|
<span class="conv-time">5h ago</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="conv-card" onclick="alert('Open conversation')">
|
||||||
|
<div class="conv-header">
|
||||||
|
<span class="conv-number">#1245</span>
|
||||||
|
<span class="status-badge status-active">Active</span>
|
||||||
|
</div>
|
||||||
|
<div class="conv-subject">Login problems</div>
|
||||||
|
<div class="conv-preview">Cannot log in with my account credentials. Password reset doesn't seem to work either...</div>
|
||||||
|
<div class="conv-footer">
|
||||||
|
<div class="customer-info">
|
||||||
|
<div class="customer-avatar" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);">AB</div>
|
||||||
|
<span class="customer-name">Alex Brown</span>
|
||||||
|
</div>
|
||||||
|
<span class="conv-time">1d ago</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="conv-card" onclick="alert('Open conversation')">
|
||||||
|
<div class="conv-header">
|
||||||
|
<span class="conv-number">#1244</span>
|
||||||
|
<span class="status-badge status-active">Active</span>
|
||||||
|
</div>
|
||||||
|
<div class="conv-subject">Question about API limits</div>
|
||||||
|
<div class="conv-preview">What are the rate limits for the API? We're planning to scale our integration...</div>
|
||||||
|
<div class="conv-footer">
|
||||||
|
<div class="customer-info">
|
||||||
|
<div class="customer-avatar" style="background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);">ED</div>
|
||||||
|
<span class="customer-name">Emily Davis</span>
|
||||||
|
</div>
|
||||||
|
<span class="conv-time">1d ago</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
};
|
||||||
127
servers/helpscout/src/apps/conversation-timeline.ts
Normal file
127
servers/helpscout/src/apps/conversation-timeline.ts
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
export const conversationTimelineApp = {
|
||||||
|
name: 'conversation-timeline',
|
||||||
|
description: 'Visual timeline view of conversation history and events',
|
||||||
|
content: `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Conversation Timeline</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f7f9fc; padding: 20px; }
|
||||||
|
.container { max-width: 900px; margin: 0 auto; }
|
||||||
|
.header { background: white; padding: 24px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||||
|
.conv-subject { font-size: 24px; font-weight: 600; color: #1f2d3d; margin-bottom: 8px; }
|
||||||
|
.conv-meta { font-size: 14px; color: #6f7b8a; }
|
||||||
|
.timeline { position: relative; padding-left: 40px; }
|
||||||
|
.timeline::before { content: ''; position: absolute; left: 16px; top: 0; bottom: 0; width: 2px; background: #d9dee4; }
|
||||||
|
.timeline-item { position: relative; background: white; padding: 20px; border-radius: 8px; margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||||
|
.timeline-marker { position: absolute; left: -29px; width: 12px; height: 12px; border-radius: 50%; background: #3197d6; border: 3px solid white; box-shadow: 0 0 0 2px #3197d6; }
|
||||||
|
.timeline-marker.note { background: #f59e0b; box-shadow: 0 0 0 2px #f59e0b; }
|
||||||
|
.timeline-marker.event { background: #10b981; box-shadow: 0 0 0 2px #10b981; }
|
||||||
|
.timeline-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
|
||||||
|
.timeline-author { font-weight: 600; color: #1f2d3d; }
|
||||||
|
.timeline-type { font-size: 12px; color: #6f7b8a; margin-left: 8px; }
|
||||||
|
.timeline-time { font-size: 13px; color: #9ca3af; }
|
||||||
|
.timeline-body { color: #3e4c59; line-height: 1.6; }
|
||||||
|
.event-label { display: inline-block; padding: 6px 12px; border-radius: 4px; font-size: 13px; font-weight: 500; background: #f0fdf4; color: #166534; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<div class="conv-subject">Payment issue with subscription</div>
|
||||||
|
<div class="conv-meta">Conversation #1247 • Created 2 days ago by Sarah Chen</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="timeline">
|
||||||
|
<div class="timeline-item">
|
||||||
|
<div class="timeline-marker"></div>
|
||||||
|
<div class="timeline-header">
|
||||||
|
<div>
|
||||||
|
<span class="timeline-author">Sarah Chen</span>
|
||||||
|
<span class="timeline-type">(Customer)</span>
|
||||||
|
</div>
|
||||||
|
<div class="timeline-time">2 days ago • 2:34 PM</div>
|
||||||
|
</div>
|
||||||
|
<div class="timeline-body">
|
||||||
|
Hi, I'm having trouble updating my payment method. Every time I try to save my new credit card, I get an error message. Can you help?
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="timeline-item">
|
||||||
|
<div class="timeline-marker event"></div>
|
||||||
|
<div class="timeline-header">
|
||||||
|
<div>
|
||||||
|
<span class="timeline-author">System Event</span>
|
||||||
|
</div>
|
||||||
|
<div class="timeline-time">2 days ago • 2:35 PM</div>
|
||||||
|
</div>
|
||||||
|
<div class="timeline-body">
|
||||||
|
<span class="event-label">✓ Assigned to John Smith</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="timeline-item">
|
||||||
|
<div class="timeline-marker event"></div>
|
||||||
|
<div class="timeline-header">
|
||||||
|
<div>
|
||||||
|
<span class="timeline-author">System Event</span>
|
||||||
|
</div>
|
||||||
|
<div class="timeline-time">2 days ago • 2:35 PM</div>
|
||||||
|
</div>
|
||||||
|
<div class="timeline-body">
|
||||||
|
<span class="event-label">🏷️ Tags added: billing, urgent</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="timeline-item">
|
||||||
|
<div class="timeline-marker note"></div>
|
||||||
|
<div class="timeline-header">
|
||||||
|
<div>
|
||||||
|
<span class="timeline-author">John Smith</span>
|
||||||
|
<span class="timeline-type">(Internal Note)</span>
|
||||||
|
</div>
|
||||||
|
<div class="timeline-time">2 days ago • 3:15 PM</div>
|
||||||
|
</div>
|
||||||
|
<div class="timeline-body">
|
||||||
|
Checking with billing team - seems like a known issue with Visa cards from Canada. Should have a fix deployed by EOD.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="timeline-item">
|
||||||
|
<div class="timeline-marker"></div>
|
||||||
|
<div class="timeline-header">
|
||||||
|
<div>
|
||||||
|
<span class="timeline-author">John Smith</span>
|
||||||
|
<span class="timeline-type">(Agent Reply)</span>
|
||||||
|
</div>
|
||||||
|
<div class="timeline-time">2 days ago • 4:30 PM</div>
|
||||||
|
</div>
|
||||||
|
<div class="timeline-body">
|
||||||
|
Hi Sarah,<br><br>
|
||||||
|
Thanks for reaching out! I've identified the issue - there's a temporary problem with processing Canadian Visa cards. Our engineering team is working on a fix that should be live within the next few hours.<br><br>
|
||||||
|
In the meantime, if you have an alternative payment method (Mastercard or American Express), that should work without issues.<br><br>
|
||||||
|
I'll follow up once the fix is deployed!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="timeline-item">
|
||||||
|
<div class="timeline-marker event"></div>
|
||||||
|
<div class="timeline-header">
|
||||||
|
<div>
|
||||||
|
<span class="timeline-author">System Event</span>
|
||||||
|
</div>
|
||||||
|
<div class="timeline-time">2 days ago • 4:31 PM</div>
|
||||||
|
</div>
|
||||||
|
<div class="timeline-body">
|
||||||
|
<span class="event-label">Status changed: Active → Pending</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
};
|
||||||
114
servers/helpscout/src/apps/customer-detail.ts
Normal file
114
servers/helpscout/src/apps/customer-detail.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
export const customerDetailApp = {
|
||||||
|
name: 'customer-detail',
|
||||||
|
description: 'Detailed customer profile with history and contact information',
|
||||||
|
content: `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Customer Detail</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f7f9fc; }
|
||||||
|
.container { max-width: 1200px; margin: 0 auto; padding: 20px; display: grid; grid-template-columns: 350px 1fr; gap: 20px; }
|
||||||
|
.sidebar { background: white; border-radius: 8px; padding: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||||
|
.main { background: white; border-radius: 8px; padding: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||||
|
.avatar { width: 80px; height: 80px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; font-size: 32px; font-weight: bold; margin: 0 auto 16px; }
|
||||||
|
.customer-name { font-size: 22px; font-weight: 600; color: #1f2d3d; text-align: center; margin-bottom: 8px; }
|
||||||
|
.customer-org { font-size: 14px; color: #6f7b8a; text-align: center; margin-bottom: 24px; }
|
||||||
|
.info-section { margin-bottom: 24px; }
|
||||||
|
.info-label { font-size: 12px; font-weight: 600; color: #6f7b8a; margin-bottom: 8px; text-transform: uppercase; }
|
||||||
|
.info-value { font-size: 14px; color: #1f2d3d; margin-bottom: 8px; }
|
||||||
|
.info-value a { color: #3197d6; text-decoration: none; }
|
||||||
|
.btn { width: 100%; padding: 10px; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; border: 1px solid #d9dee4; background: white; margin-bottom: 8px; }
|
||||||
|
.btn:hover { background: #f7f9fc; }
|
||||||
|
.tabs { display: flex; border-bottom: 1px solid #d9dee4; margin-bottom: 24px; }
|
||||||
|
.tab { padding: 12px 24px; font-size: 14px; font-weight: 500; color: #6f7b8a; cursor: pointer; border-bottom: 2px solid transparent; }
|
||||||
|
.tab.active { color: #3197d6; border-bottom-color: #3197d6; }
|
||||||
|
.conversation-item { padding: 16px; border: 1px solid #e5e7eb; border-radius: 6px; margin-bottom: 12px; cursor: pointer; }
|
||||||
|
.conversation-item:hover { background: #f7f9fc; }
|
||||||
|
.conv-subject { font-weight: 500; color: #1f2d3d; margin-bottom: 4px; }
|
||||||
|
.conv-meta { font-size: 13px; color: #6f7b8a; }
|
||||||
|
.status-badge { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 500; margin-left: 8px; }
|
||||||
|
.status-active { background: #d4f4dd; color: #0a6640; }
|
||||||
|
.status-closed { background: #e5e7eb; color: #4b5563; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="avatar">SC</div>
|
||||||
|
<div class="customer-name">Sarah Chen</div>
|
||||||
|
<div class="customer-org">Acme Corporation</div>
|
||||||
|
|
||||||
|
<button class="btn">📧 Send Email</button>
|
||||||
|
<button class="btn">💬 Start Conversation</button>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<div class="info-label">Email</div>
|
||||||
|
<div class="info-value"><a href="mailto:sarah.chen@example.com">sarah.chen@example.com</a></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<div class="info-label">Phone</div>
|
||||||
|
<div class="info-value"><a href="tel:+15551234567">+1 (555) 123-4567</a></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<div class="info-label">Job Title</div>
|
||||||
|
<div class="info-value">Product Manager</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<div class="info-label">Location</div>
|
||||||
|
<div class="info-value">San Francisco, CA</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<div class="info-label">Customer Since</div>
|
||||||
|
<div class="info-value">January 2024</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<div class="info-label">Background</div>
|
||||||
|
<div class="info-value" style="font-size: 13px;">Long-time customer, frequently provides valuable product feedback.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main">
|
||||||
|
<div class="tabs">
|
||||||
|
<div class="tab active">Conversations (12)</div>
|
||||||
|
<div class="tab">Notes (3)</div>
|
||||||
|
<div class="tab">Activity</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="conversation-item" onclick="alert('Open conversation')">
|
||||||
|
<div class="conv-subject">
|
||||||
|
Payment issue with subscription
|
||||||
|
<span class="status-badge status-active">Active</span>
|
||||||
|
</div>
|
||||||
|
<div class="conv-meta">Last updated 2 hours ago • Assigned to John Smith</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="conversation-item" onclick="alert('Open conversation')">
|
||||||
|
<div class="conv-subject">
|
||||||
|
Feature request: Dark mode
|
||||||
|
<span class="status-badge status-closed">Closed</span>
|
||||||
|
</div>
|
||||||
|
<div class="conv-meta">Closed 3 days ago • Assigned to Emily Davis</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="conversation-item" onclick="alert('Open conversation')">
|
||||||
|
<div class="conv-subject">
|
||||||
|
Question about API limits
|
||||||
|
<span class="status-badge status-closed">Closed</span>
|
||||||
|
</div>
|
||||||
|
<div class="conv-meta">Closed 1 week ago • Assigned to John Smith</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
};
|
||||||
85
servers/helpscout/src/apps/customer-grid.ts
Normal file
85
servers/helpscout/src/apps/customer-grid.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
export const customerGridApp = {
|
||||||
|
name: 'customer-grid',
|
||||||
|
description: 'Grid view of all customers with search and filtering',
|
||||||
|
content: `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Customers</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f7f9fc; padding: 20px; }
|
||||||
|
.container { max-width: 1400px; margin: 0 auto; }
|
||||||
|
h1 { font-size: 28px; color: #1f2d3d; margin-bottom: 20px; }
|
||||||
|
.toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||||
|
.search { flex: 1; max-width: 400px; }
|
||||||
|
.search input { width: 100%; padding: 10px 16px; border: 1px solid #d9dee4; border-radius: 6px; font-size: 14px; }
|
||||||
|
.btn { padding: 10px 20px; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; border: none; }
|
||||||
|
.btn-primary { background: #3197d6; color: white; }
|
||||||
|
.customer-grid { background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||||
|
.grid-header { display: grid; grid-template-columns: 250px 200px 150px 1fr 120px; padding: 12px 16px; background: #f7f9fc; border-bottom: 1px solid #d9dee4; font-size: 12px; font-weight: 600; color: #6f7b8a; }
|
||||||
|
.grid-row { display: grid; grid-template-columns: 250px 200px 150px 1fr 120px; padding: 16px; border-bottom: 1px solid #f0f2f5; align-items: center; cursor: pointer; transition: background 0.2s; }
|
||||||
|
.grid-row:hover { background: #f7f9fc; }
|
||||||
|
.customer-name { font-weight: 500; color: #1f2d3d; }
|
||||||
|
.customer-email { font-size: 13px; color: #3197d6; margin-top: 2px; }
|
||||||
|
.customer-phone { font-size: 14px; color: #6f7b8a; }
|
||||||
|
.customer-org { font-size: 14px; color: #6f7b8a; }
|
||||||
|
.customer-location { font-size: 14px; color: #6f7b8a; }
|
||||||
|
.customer-count { font-size: 13px; color: #6f7b8a; text-align: center; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>👥 Customers</h1>
|
||||||
|
<div class="toolbar">
|
||||||
|
<div class="search">
|
||||||
|
<input type="text" placeholder="Search customers by name, email, or organization...">
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary">+ Add Customer</button>
|
||||||
|
</div>
|
||||||
|
<div class="customer-grid">
|
||||||
|
<div class="grid-header">
|
||||||
|
<div>NAME</div>
|
||||||
|
<div>EMAIL</div>
|
||||||
|
<div>PHONE</div>
|
||||||
|
<div>ORGANIZATION</div>
|
||||||
|
<div>CONVERSATIONS</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid-row" onclick="alert('Open customer detail')">
|
||||||
|
<div>
|
||||||
|
<div class="customer-name">Sarah Chen</div>
|
||||||
|
<div class="customer-email">sarah.chen@example.com</div>
|
||||||
|
</div>
|
||||||
|
<div class="customer-email">sarah.chen@example.com</div>
|
||||||
|
<div class="customer-phone">+1 (555) 123-4567</div>
|
||||||
|
<div class="customer-org">Acme Corp</div>
|
||||||
|
<div class="customer-count">12 conversations</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid-row" onclick="alert('Open customer detail')">
|
||||||
|
<div>
|
||||||
|
<div class="customer-name">Mike Johnson</div>
|
||||||
|
<div class="customer-email">mike.j@techstart.io</div>
|
||||||
|
</div>
|
||||||
|
<div class="customer-email">mike.j@techstart.io</div>
|
||||||
|
<div class="customer-phone">+1 (555) 987-6543</div>
|
||||||
|
<div class="customer-org">TechStart</div>
|
||||||
|
<div class="customer-count">8 conversations</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid-row" onclick="alert('Open customer detail')">
|
||||||
|
<div>
|
||||||
|
<div class="customer-name">Emily Davis</div>
|
||||||
|
<div class="customer-email">emily@startup.com</div>
|
||||||
|
</div>
|
||||||
|
<div class="customer-email">emily@startup.com</div>
|
||||||
|
<div class="customer-phone">+1 (555) 456-7890</div>
|
||||||
|
<div class="customer-org">Startup Inc</div>
|
||||||
|
<div class="customer-count">5 conversations</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
};
|
||||||
116
servers/helpscout/src/apps/folder-browser.ts
Normal file
116
servers/helpscout/src/apps/folder-browser.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
export const folderBrowserApp = {
|
||||||
|
name: 'folder-browser',
|
||||||
|
description: 'Browse and manage mailbox folders',
|
||||||
|
content: `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Folder Browser</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f7f9fc; padding: 20px; }
|
||||||
|
.container { max-width: 1200px; margin: 0 auto; display: grid; grid-template-columns: 300px 1fr; gap: 20px; }
|
||||||
|
.sidebar { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); height: fit-content; }
|
||||||
|
.main { background: white; border-radius: 8px; padding: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||||
|
.mailbox-section { margin-bottom: 24px; }
|
||||||
|
.mailbox-name { font-size: 14px; font-weight: 600; color: #6f7b8a; margin-bottom: 12px; text-transform: uppercase; }
|
||||||
|
.folder-list { display: flex; flex-direction: column; gap: 4px; }
|
||||||
|
.folder-item { padding: 10px 12px; border-radius: 6px; cursor: pointer; transition: background 0.2s; display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.folder-item:hover { background: #f7f9fc; }
|
||||||
|
.folder-item.active { background: #e0f2fe; color: #0369a1; font-weight: 500; }
|
||||||
|
.folder-name { font-size: 14px; display: flex; align-items: center; gap: 8px; }
|
||||||
|
.folder-count { font-size: 12px; padding: 2px 8px; border-radius: 10px; background: #e5e7eb; color: #4b5563; font-weight: 600; }
|
||||||
|
.folder-item.active .folder-count { background: #0369a1; color: white; }
|
||||||
|
.main-header { margin-bottom: 24px; }
|
||||||
|
.main-title { font-size: 24px; font-weight: 600; color: #1f2d3d; margin-bottom: 8px; }
|
||||||
|
.main-subtitle { font-size: 14px; color: #6f7b8a; }
|
||||||
|
.conv-list { display: flex; flex-direction: column; gap: 12px; }
|
||||||
|
.conv-item { padding: 16px; border: 1px solid #e5e7eb; border-radius: 6px; cursor: pointer; transition: all 0.2s; }
|
||||||
|
.conv-item:hover { border-color: #3197d6; background: #f7f9fc; }
|
||||||
|
.conv-subject { font-size: 15px; font-weight: 500; color: #1f2d3d; margin-bottom: 4px; }
|
||||||
|
.conv-meta { font-size: 13px; color: #6f7b8a; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="mailbox-section">
|
||||||
|
<div class="mailbox-name">Support Mailbox</div>
|
||||||
|
<div class="folder-list">
|
||||||
|
<div class="folder-item active">
|
||||||
|
<span class="folder-name">📥 Unassigned</span>
|
||||||
|
<span class="folder-count">8</span>
|
||||||
|
</div>
|
||||||
|
<div class="folder-item">
|
||||||
|
<span class="folder-name">📋 My Conversations</span>
|
||||||
|
<span class="folder-count">23</span>
|
||||||
|
</div>
|
||||||
|
<div class="folder-item">
|
||||||
|
<span class="folder-name">📌 Drafts</span>
|
||||||
|
<span class="folder-count">2</span>
|
||||||
|
</div>
|
||||||
|
<div class="folder-item">
|
||||||
|
<span class="folder-name">👥 Assigned</span>
|
||||||
|
<span class="folder-count">47</span>
|
||||||
|
</div>
|
||||||
|
<div class="folder-item">
|
||||||
|
<span class="folder-name">✅ Closed</span>
|
||||||
|
<span class="folder-count">147</span>
|
||||||
|
</div>
|
||||||
|
<div class="folder-item">
|
||||||
|
<span class="folder-name">🚫 Spam</span>
|
||||||
|
<span class="folder-count">3</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mailbox-section">
|
||||||
|
<div class="mailbox-name">Sales Mailbox</div>
|
||||||
|
<div class="folder-list">
|
||||||
|
<div class="folder-item">
|
||||||
|
<span class="folder-name">📥 Unassigned</span>
|
||||||
|
<span class="folder-count">3</span>
|
||||||
|
</div>
|
||||||
|
<div class="folder-item">
|
||||||
|
<span class="folder-name">📋 My Conversations</span>
|
||||||
|
<span class="folder-count">12</span>
|
||||||
|
</div>
|
||||||
|
<div class="folder-item">
|
||||||
|
<span class="folder-name">✅ Closed</span>
|
||||||
|
<span class="folder-count">89</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main">
|
||||||
|
<div class="main-header">
|
||||||
|
<div class="main-title">📥 Unassigned</div>
|
||||||
|
<div class="main-subtitle">8 conversations need to be assigned</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="conv-list">
|
||||||
|
<div class="conv-item" onclick="alert('Open conversation')">
|
||||||
|
<div class="conv-subject">Payment issue with subscription</div>
|
||||||
|
<div class="conv-meta">Sarah Chen • 2 hours ago • #1247</div>
|
||||||
|
</div>
|
||||||
|
<div class="conv-item" onclick="alert('Open conversation')">
|
||||||
|
<div class="conv-subject">Login problems</div>
|
||||||
|
<div class="conv-meta">Alex Brown • 1 day ago • #1245</div>
|
||||||
|
</div>
|
||||||
|
<div class="conv-item" onclick="alert('Open conversation')">
|
||||||
|
<div class="conv-subject">Question about API limits</div>
|
||||||
|
<div class="conv-meta">Emily Davis • 1 day ago • #1244</div>
|
||||||
|
</div>
|
||||||
|
<div class="conv-item" onclick="alert('Open conversation')">
|
||||||
|
<div class="conv-subject">Account upgrade request</div>
|
||||||
|
<div class="conv-meta">Mike Johnson • 2 days ago • #1238</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
};
|
||||||
93
servers/helpscout/src/apps/happiness-report.ts
Normal file
93
servers/helpscout/src/apps/happiness-report.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
export const happinessReportApp = {
|
||||||
|
name: 'happiness-report',
|
||||||
|
description: 'Customer satisfaction and happiness metrics',
|
||||||
|
content: `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Happiness Report</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f7f9fc; padding: 20px; }
|
||||||
|
.container { max-width: 1200px; margin: 0 auto; }
|
||||||
|
h1 { font-size: 28px; color: #1f2d3d; margin-bottom: 8px; }
|
||||||
|
.subtitle { font-size: 14px; color: #6f7b8a; margin-bottom: 24px; }
|
||||||
|
.hero-score { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 40px; border-radius: 12px; text-align: center; margin-bottom: 24px; }
|
||||||
|
.score-value { font-size: 72px; font-weight: bold; margin-bottom: 8px; }
|
||||||
|
.score-label { font-size: 16px; opacity: 0.9; }
|
||||||
|
.score-change { font-size: 14px; margin-top: 8px; }
|
||||||
|
.score-change.positive { color: #a7f3d0; }
|
||||||
|
.metrics { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 24px; }
|
||||||
|
.metric-card { background: white; padding: 24px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||||
|
.metric-value { font-size: 32px; font-weight: bold; margin-bottom: 4px; }
|
||||||
|
.metric-label { font-size: 13px; color: #6f7b8a; }
|
||||||
|
.metric-great { color: #10b981; }
|
||||||
|
.metric-okay { color: #f59e0b; }
|
||||||
|
.metric-bad { color: #ef4444; }
|
||||||
|
.chart-section { background: white; padding: 24px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 24px; }
|
||||||
|
.chart-title { font-size: 18px; font-weight: 600; color: #1f2d3d; margin-bottom: 16px; }
|
||||||
|
.bar-chart { display: flex; flex-direction: column; gap: 12px; }
|
||||||
|
.bar-item { display: flex; align-items: center; gap: 12px; }
|
||||||
|
.bar-label { width: 100px; font-size: 14px; color: #6f7b8a; }
|
||||||
|
.bar-container { flex: 1; background: #f0f2f5; border-radius: 4px; height: 32px; position: relative; overflow: hidden; }
|
||||||
|
.bar-fill { height: 100%; border-radius: 4px; display: flex; align-items: center; padding: 0 12px; font-size: 13px; font-weight: 600; color: white; }
|
||||||
|
.bar-great { background: #10b981; }
|
||||||
|
.bar-okay { background: #f59e0b; }
|
||||||
|
.bar-bad { background: #ef4444; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>😊 Happiness Report</h1>
|
||||||
|
<div class="subtitle">Customer satisfaction metrics for last 30 days</div>
|
||||||
|
|
||||||
|
<div class="hero-score">
|
||||||
|
<div class="score-value">94%</div>
|
||||||
|
<div class="score-label">Overall Happiness Score</div>
|
||||||
|
<div class="score-change positive">↑ 3% from previous period</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="metrics">
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-value metric-great">237</div>
|
||||||
|
<div class="metric-label">😊 Great Ratings</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-value metric-okay">18</div>
|
||||||
|
<div class="metric-label">😐 Okay Ratings</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-value metric-bad">7</div>
|
||||||
|
<div class="metric-label">😞 Not Good Ratings</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chart-section">
|
||||||
|
<div class="chart-title">Ratings Distribution</div>
|
||||||
|
<div class="bar-chart">
|
||||||
|
<div class="bar-item">
|
||||||
|
<div class="bar-label">Great</div>
|
||||||
|
<div class="bar-container">
|
||||||
|
<div class="bar-fill bar-great" style="width: 90%;">237 (90%)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bar-item">
|
||||||
|
<div class="bar-label">Okay</div>
|
||||||
|
<div class="bar-container">
|
||||||
|
<div class="bar-fill bar-okay" style="width: 7%;">18 (7%)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bar-item">
|
||||||
|
<div class="bar-label">Not Good</div>
|
||||||
|
<div class="bar-container">
|
||||||
|
<div class="bar-fill bar-bad" style="width: 3%;">7 (3%)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
};
|
||||||
99
servers/helpscout/src/apps/mailbox-overview.ts
Normal file
99
servers/helpscout/src/apps/mailbox-overview.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
export const mailboxOverviewApp = {
|
||||||
|
name: 'mailbox-overview',
|
||||||
|
description: 'Overview of all mailboxes with folder counts and activity',
|
||||||
|
content: `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Mailbox Overview</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f7f9fc; padding: 20px; }
|
||||||
|
.container { max-width: 1200px; margin: 0 auto; }
|
||||||
|
h1 { font-size: 28px; color: #1f2d3d; margin-bottom: 20px; }
|
||||||
|
.mailboxes { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 20px; }
|
||||||
|
.mailbox-card { background: white; border-radius: 8px; padding: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); cursor: pointer; transition: transform 0.2s; }
|
||||||
|
.mailbox-card:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.15); }
|
||||||
|
.mailbox-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
||||||
|
.mailbox-name { font-size: 18px; font-weight: 600; color: #1f2d3d; }
|
||||||
|
.mailbox-email { font-size: 13px; color: #6f7b8a; margin-bottom: 20px; }
|
||||||
|
.folder-list { display: flex; flex-direction: column; gap: 12px; }
|
||||||
|
.folder-item { display: flex; justify-content: space-between; align-items: center; padding: 8px; border-radius: 4px; background: #f7f9fc; }
|
||||||
|
.folder-name { font-size: 14px; color: #3e4c59; }
|
||||||
|
.folder-count { font-size: 14px; font-weight: 600; color: #3197d6; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>📮 Mailboxes</h1>
|
||||||
|
<div class="mailboxes">
|
||||||
|
<div class="mailbox-card" onclick="alert('Open mailbox detail')">
|
||||||
|
<div class="mailbox-header">
|
||||||
|
<div class="mailbox-name">Support</div>
|
||||||
|
<div style="font-size: 24px;">💬</div>
|
||||||
|
</div>
|
||||||
|
<div class="mailbox-email">support@company.com</div>
|
||||||
|
<div class="folder-list">
|
||||||
|
<div class="folder-item">
|
||||||
|
<span class="folder-name">📥 Unassigned</span>
|
||||||
|
<span class="folder-count">8</span>
|
||||||
|
</div>
|
||||||
|
<div class="folder-item">
|
||||||
|
<span class="folder-name">📋 My Conversations</span>
|
||||||
|
<span class="folder-count">23</span>
|
||||||
|
</div>
|
||||||
|
<div class="folder-item">
|
||||||
|
<span class="folder-name">✅ Closed</span>
|
||||||
|
<span class="folder-count">147</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mailbox-card" onclick="alert('Open mailbox detail')">
|
||||||
|
<div class="mailbox-header">
|
||||||
|
<div class="mailbox-name">Sales</div>
|
||||||
|
<div style="font-size: 24px;">💰</div>
|
||||||
|
</div>
|
||||||
|
<div class="mailbox-email">sales@company.com</div>
|
||||||
|
<div class="folder-list">
|
||||||
|
<div class="folder-item">
|
||||||
|
<span class="folder-name">📥 Unassigned</span>
|
||||||
|
<span class="folder-count">3</span>
|
||||||
|
</div>
|
||||||
|
<div class="folder-item">
|
||||||
|
<span class="folder-name">📋 My Conversations</span>
|
||||||
|
<span class="folder-count">12</span>
|
||||||
|
</div>
|
||||||
|
<div class="folder-item">
|
||||||
|
<span class="folder-name">✅ Closed</span>
|
||||||
|
<span class="folder-count">89</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mailbox-card" onclick="alert('Open mailbox detail')">
|
||||||
|
<div class="mailbox-header">
|
||||||
|
<div class="mailbox-name">Billing</div>
|
||||||
|
<div style="font-size: 24px;">💳</div>
|
||||||
|
</div>
|
||||||
|
<div class="mailbox-email">billing@company.com</div>
|
||||||
|
<div class="folder-list">
|
||||||
|
<div class="folder-item">
|
||||||
|
<span class="folder-name">📥 Unassigned</span>
|
||||||
|
<span class="folder-count">2</span>
|
||||||
|
</div>
|
||||||
|
<div class="folder-item">
|
||||||
|
<span class="folder-name">📋 My Conversations</span>
|
||||||
|
<span class="folder-count">7</span>
|
||||||
|
</div>
|
||||||
|
<div class="folder-item">
|
||||||
|
<span class="folder-name">✅ Closed</span>
|
||||||
|
<span class="folder-count">64</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
};
|
||||||
105
servers/helpscout/src/apps/productivity-report.ts
Normal file
105
servers/helpscout/src/apps/productivity-report.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
export const productivityReportApp = {
|
||||||
|
name: 'productivity-report',
|
||||||
|
description: 'Team productivity metrics and response times',
|
||||||
|
content: `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Productivity Report</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f7f9fc; padding: 20px; }
|
||||||
|
.container { max-width: 1200px; margin: 0 auto; }
|
||||||
|
h1 { font-size: 28px; color: #1f2d3d; margin-bottom: 8px; }
|
||||||
|
.subtitle { font-size: 14px; color: #6f7b8a; margin-bottom: 24px; }
|
||||||
|
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; margin-bottom: 24px; }
|
||||||
|
.stat-card { background: white; padding: 24px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||||
|
.stat-value { font-size: 36px; font-weight: bold; color: #3197d6; margin-bottom: 4px; }
|
||||||
|
.stat-label { font-size: 14px; color: #6f7b8a; }
|
||||||
|
.stat-change { font-size: 12px; margin-top: 4px; }
|
||||||
|
.stat-change.positive { color: #10b981; }
|
||||||
|
.stat-change.negative { color: #ef4444; }
|
||||||
|
.chart-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
|
||||||
|
.chart-section { background: white; padding: 24px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||||
|
.chart-title { font-size: 18px; font-weight: 600; color: #1f2d3d; margin-bottom: 16px; }
|
||||||
|
.metric-item { display: flex; justify-content: space-between; padding: 12px 0; border-bottom: 1px solid #f0f2f5; }
|
||||||
|
.metric-item:last-child { border-bottom: none; }
|
||||||
|
.metric-name { font-size: 14px; color: #3e4c59; }
|
||||||
|
.metric-val { font-size: 14px; font-weight: 600; color: #1f2d3d; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>📊 Productivity Report</h1>
|
||||||
|
<div class="subtitle">Team performance metrics for last 30 days</div>
|
||||||
|
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value">1,247</div>
|
||||||
|
<div class="stat-label">Replies Sent</div>
|
||||||
|
<div class="stat-change positive">↑ 12% from last month</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value">3.2h</div>
|
||||||
|
<div class="stat-label">Avg First Response Time</div>
|
||||||
|
<div class="stat-change positive">↓ 0.5h from last month</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value">8.4h</div>
|
||||||
|
<div class="stat-label">Avg Resolution Time</div>
|
||||||
|
<div class="stat-change negative">↑ 1.2h from last month</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value">892</div>
|
||||||
|
<div class="stat-label">Resolved Conversations</div>
|
||||||
|
<div class="stat-change positive">↑ 8% from last month</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chart-grid">
|
||||||
|
<div class="chart-section">
|
||||||
|
<div class="chart-title">Top Performers (Replies)</div>
|
||||||
|
<div class="metric-item">
|
||||||
|
<span class="metric-name">🥇 John Smith</span>
|
||||||
|
<span class="metric-val">324 replies</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric-item">
|
||||||
|
<span class="metric-name">🥈 Emily Davis</span>
|
||||||
|
<span class="metric-val">287 replies</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric-item">
|
||||||
|
<span class="metric-name">🥉 Mike Johnson</span>
|
||||||
|
<span class="metric-val">219 replies</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric-item">
|
||||||
|
<span class="metric-name">Sarah Wilson</span>
|
||||||
|
<span class="metric-val">198 replies</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chart-section">
|
||||||
|
<div class="chart-title">Response Time by Agent</div>
|
||||||
|
<div class="metric-item">
|
||||||
|
<span class="metric-name">John Smith</span>
|
||||||
|
<span class="metric-val">2.8h</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric-item">
|
||||||
|
<span class="metric-name">Emily Davis</span>
|
||||||
|
<span class="metric-val">3.1h</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric-item">
|
||||||
|
<span class="metric-name">Mike Johnson</span>
|
||||||
|
<span class="metric-val">3.5h</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric-item">
|
||||||
|
<span class="metric-name">Sarah Wilson</span>
|
||||||
|
<span class="metric-val">4.2h</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
};
|
||||||
101
servers/helpscout/src/apps/saved-replies.ts
Normal file
101
servers/helpscout/src/apps/saved-replies.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
export const savedRepliesApp = {
|
||||||
|
name: 'saved-replies',
|
||||||
|
description: 'Manage and browse saved reply templates',
|
||||||
|
content: `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Saved Replies</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f7f9fc; padding: 20px; }
|
||||||
|
.container { max-width: 1200px; margin: 0 auto; }
|
||||||
|
h1 { font-size: 28px; color: #1f2d3d; margin-bottom: 20px; }
|
||||||
|
.toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||||
|
.search { flex: 1; max-width: 400px; }
|
||||||
|
.search input { width: 100%; padding: 10px 16px; border: 1px solid #d9dee4; border-radius: 6px; font-size: 14px; }
|
||||||
|
.btn { padding: 10px 20px; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; border: none; background: #3197d6; color: white; }
|
||||||
|
.replies-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 16px; }
|
||||||
|
.reply-card { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); cursor: pointer; transition: transform 0.2s; }
|
||||||
|
.reply-card:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.15); }
|
||||||
|
.reply-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; }
|
||||||
|
.reply-name { font-size: 16px; font-weight: 600; color: #1f2d3d; }
|
||||||
|
.reply-actions { display: flex; gap: 8px; }
|
||||||
|
.icon-btn { padding: 4px 8px; border-radius: 4px; border: 1px solid #d9dee4; background: white; cursor: pointer; font-size: 14px; }
|
||||||
|
.icon-btn:hover { background: #f7f9fc; }
|
||||||
|
.reply-preview { font-size: 14px; color: #6f7b8a; line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; margin-bottom: 12px; }
|
||||||
|
.reply-meta { font-size: 12px; color: #9ca3af; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>💬 Saved Replies</h1>
|
||||||
|
<div class="toolbar">
|
||||||
|
<div class="search">
|
||||||
|
<input type="text" placeholder="Search saved replies...">
|
||||||
|
</div>
|
||||||
|
<button class="btn">+ Create Reply</button>
|
||||||
|
</div>
|
||||||
|
<div class="replies-grid">
|
||||||
|
<div class="reply-card" onclick="alert('Edit reply')">
|
||||||
|
<div class="reply-header">
|
||||||
|
<div class="reply-name">Welcome Message</div>
|
||||||
|
<div class="reply-actions">
|
||||||
|
<button class="icon-btn">✏️</button>
|
||||||
|
<button class="icon-btn">🗑️</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="reply-preview">
|
||||||
|
Hi there! Thanks for reaching out to our support team. We're here to help! Could you please provide more details about the issue you're experiencing?
|
||||||
|
</div>
|
||||||
|
<div class="reply-meta">Support mailbox • Used 47 times</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="reply-card" onclick="alert('Edit reply')">
|
||||||
|
<div class="reply-header">
|
||||||
|
<div class="reply-name">Billing Issue Response</div>
|
||||||
|
<div class="reply-actions">
|
||||||
|
<button class="icon-btn">✏️</button>
|
||||||
|
<button class="icon-btn">🗑️</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="reply-preview">
|
||||||
|
I understand you're experiencing a billing issue. I've escalated this to our billing team and they'll review your account within the next 24 hours. You'll receive an email update shortly.
|
||||||
|
</div>
|
||||||
|
<div class="reply-meta">Billing mailbox • Used 23 times</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="reply-card" onclick="alert('Edit reply')">
|
||||||
|
<div class="reply-header">
|
||||||
|
<div class="reply-name">Feature Request Acknowledgment</div>
|
||||||
|
<div class="reply-actions">
|
||||||
|
<button class="icon-btn">✏️</button>
|
||||||
|
<button class="icon-btn">🗑️</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="reply-preview">
|
||||||
|
Thank you for this feature suggestion! We really appreciate feedback from our users. I've passed this along to our product team for consideration in future updates.
|
||||||
|
</div>
|
||||||
|
<div class="reply-meta">All mailboxes • Used 34 times</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="reply-card" onclick="alert('Edit reply')">
|
||||||
|
<div class="reply-header">
|
||||||
|
<div class="reply-name">Account Access Help</div>
|
||||||
|
<div class="reply-actions">
|
||||||
|
<button class="icon-btn">✏️</button>
|
||||||
|
<button class="icon-btn">🗑️</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="reply-preview">
|
||||||
|
Let me help you regain access to your account. Please try resetting your password using the "Forgot Password" link on the login page. If that doesn't work, I can manually send you a reset link.
|
||||||
|
</div>
|
||||||
|
<div class="reply-meta">Support mailbox • Used 89 times</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
};
|
||||||
88
servers/helpscout/src/apps/search-results.ts
Normal file
88
servers/helpscout/src/apps/search-results.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
export const searchResultsApp = {
|
||||||
|
name: 'search-results',
|
||||||
|
description: 'Search results interface for finding conversations and customers',
|
||||||
|
content: `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Search Results</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f7f9fc; padding: 20px; }
|
||||||
|
.container { max-width: 1000px; margin: 0 auto; }
|
||||||
|
.search-bar { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||||
|
.search-bar input { width: 100%; padding: 12px 16px; border: 2px solid #3197d6; border-radius: 6px; font-size: 16px; }
|
||||||
|
.results-header { font-size: 14px; color: #6f7b8a; margin-bottom: 16px; }
|
||||||
|
.results-count { font-weight: 600; color: #1f2d3d; }
|
||||||
|
.tabs { display: flex; gap: 8px; margin-bottom: 20px; }
|
||||||
|
.tab { padding: 10px 20px; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; border: 1px solid #d9dee4; background: white; }
|
||||||
|
.tab.active { background: #3197d6; color: white; border-color: #3197d6; }
|
||||||
|
.result-item { background: white; padding: 20px; border-radius: 8px; margin-bottom: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); cursor: pointer; transition: transform 0.2s; }
|
||||||
|
.result-item:hover { transform: translateX(4px); box-shadow: 0 2px 6px rgba(0,0,0,0.15); }
|
||||||
|
.result-type { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; text-transform: uppercase; margin-bottom: 8px; }
|
||||||
|
.type-conversation { background: #e0f2fe; color: #0369a1; }
|
||||||
|
.type-customer { background: #fef3c7; color: #92400e; }
|
||||||
|
.result-title { font-size: 16px; font-weight: 600; color: #1f2d3d; margin-bottom: 8px; }
|
||||||
|
.result-snippet { font-size: 14px; color: #6f7b8a; line-height: 1.5; margin-bottom: 8px; }
|
||||||
|
.highlight { background: #fef08a; padding: 2px 4px; border-radius: 2px; }
|
||||||
|
.result-meta { font-size: 12px; color: #9ca3af; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="search-bar">
|
||||||
|
<input type="text" placeholder="Search conversations, customers, and more..." value="payment issue">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="results-header">
|
||||||
|
Found <span class="results-count">12 results</span> for "payment issue"
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tabs">
|
||||||
|
<button class="tab active">All (12)</button>
|
||||||
|
<button class="tab">Conversations (8)</button>
|
||||||
|
<button class="tab">Customers (3)</button>
|
||||||
|
<button class="tab">Articles (1)</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-item" onclick="alert('Open conversation')">
|
||||||
|
<span class="result-type type-conversation">Conversation #1247</span>
|
||||||
|
<div class="result-title">Payment issue with subscription</div>
|
||||||
|
<div class="result-snippet">
|
||||||
|
Customer having trouble with their credit card. Error appears when trying to update <span class="highlight">payment</span> method. The <span class="highlight">issue</span> started yesterday...
|
||||||
|
</div>
|
||||||
|
<div class="result-meta">Sarah Chen • Active • 2 hours ago</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-item" onclick="alert('Open conversation')">
|
||||||
|
<span class="result-type type-conversation">Conversation #1189</span>
|
||||||
|
<div class="result-title">Billing problem - double charge</div>
|
||||||
|
<div class="result-snippet">
|
||||||
|
I was charged twice for my subscription this month. Can you help resolve this <span class="highlight">payment issue</span>?
|
||||||
|
</div>
|
||||||
|
<div class="result-meta">Mike Johnson • Closed • 3 days ago</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-item" onclick="alert('Open customer')">
|
||||||
|
<span class="result-type type-customer">Customer</span>
|
||||||
|
<div class="result-title">Sarah Chen</div>
|
||||||
|
<div class="result-snippet">
|
||||||
|
Background: VIP customer, frequently reports <span class="highlight">payment issues</span>. Works at Acme Corp as Product Manager.
|
||||||
|
</div>
|
||||||
|
<div class="result-meta">sarah.chen@example.com • 12 conversations</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-item" onclick="alert('Open conversation')">
|
||||||
|
<span class="result-type type-conversation">Conversation #1156</span>
|
||||||
|
<div class="result-title">Can't complete checkout</div>
|
||||||
|
<div class="result-snippet">
|
||||||
|
Checkout process fails at the <span class="highlight">payment</span> step. Tried multiple cards, same <span class="highlight">issue</span>.
|
||||||
|
</div>
|
||||||
|
<div class="result-meta">Alex Brown • Closed • 1 week ago</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
};
|
||||||
97
servers/helpscout/src/apps/tag-manager.ts
Normal file
97
servers/helpscout/src/apps/tag-manager.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
export const tagManagerApp = {
|
||||||
|
name: 'tag-manager',
|
||||||
|
description: 'Manage tags with usage stats and color coding',
|
||||||
|
content: `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Tag Manager</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f7f9fc; padding: 20px; }
|
||||||
|
.container { max-width: 1000px; margin: 0 auto; }
|
||||||
|
h1 { font-size: 28px; color: #1f2d3d; margin-bottom: 20px; }
|
||||||
|
.toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||||
|
.btn { padding: 10px 20px; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; border: none; background: #3197d6; color: white; }
|
||||||
|
.tags-grid { background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||||
|
.grid-header { display: grid; grid-template-columns: 1fr 100px 150px 120px; padding: 12px 16px; background: #f7f9fc; border-bottom: 1px solid #d9dee4; font-size: 12px; font-weight: 600; color: #6f7b8a; }
|
||||||
|
.grid-row { display: grid; grid-template-columns: 1fr 100px 150px 120px; padding: 16px; border-bottom: 1px solid #f0f2f5; align-items: center; }
|
||||||
|
.tag-display { display: inline-flex; align-items: center; gap: 8px; }
|
||||||
|
.tag-color { width: 16px; height: 16px; border-radius: 3px; }
|
||||||
|
.tag-name { font-size: 14px; font-weight: 500; color: #1f2d3d; }
|
||||||
|
.tag-count { font-size: 14px; color: #6f7b8a; text-align: center; }
|
||||||
|
.tag-created { font-size: 13px; color: #6f7b8a; }
|
||||||
|
.tag-actions { display: flex; gap: 8px; justify-content: flex-end; }
|
||||||
|
.action-btn { padding: 6px 12px; font-size: 12px; border-radius: 4px; cursor: pointer; border: 1px solid #d9dee4; background: white; }
|
||||||
|
.action-btn:hover { background: #f7f9fc; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🏷️ Tag Manager</h1>
|
||||||
|
<div class="toolbar">
|
||||||
|
<div style="font-size: 14px; color: #6f7b8a;">Managing 12 tags</div>
|
||||||
|
<button class="btn">+ Create Tag</button>
|
||||||
|
</div>
|
||||||
|
<div class="tags-grid">
|
||||||
|
<div class="grid-header">
|
||||||
|
<div>TAG NAME</div>
|
||||||
|
<div style="text-align: center;">USAGE</div>
|
||||||
|
<div>CREATED</div>
|
||||||
|
<div style="text-align: right;">ACTIONS</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid-row">
|
||||||
|
<div class="tag-display">
|
||||||
|
<div class="tag-color" style="background: #ef4444;"></div>
|
||||||
|
<span class="tag-name">urgent</span>
|
||||||
|
</div>
|
||||||
|
<div class="tag-count">23 conversations</div>
|
||||||
|
<div class="tag-created">Jan 2024</div>
|
||||||
|
<div class="tag-actions">
|
||||||
|
<button class="action-btn">Edit</button>
|
||||||
|
<button class="action-btn" style="color: #ef4444;">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid-row">
|
||||||
|
<div class="tag-display">
|
||||||
|
<div class="tag-color" style="background: #3b82f6;"></div>
|
||||||
|
<span class="tag-name">billing</span>
|
||||||
|
</div>
|
||||||
|
<div class="tag-count">47 conversations</div>
|
||||||
|
<div class="tag-created">Dec 2023</div>
|
||||||
|
<div class="tag-actions">
|
||||||
|
<button class="action-btn">Edit</button>
|
||||||
|
<button class="action-btn" style="color: #ef4444;">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid-row">
|
||||||
|
<div class="tag-display">
|
||||||
|
<div class="tag-color" style="background: #10b981;"></div>
|
||||||
|
<span class="tag-name">feature-request</span>
|
||||||
|
</div>
|
||||||
|
<div class="tag-count">31 conversations</div>
|
||||||
|
<div class="tag-created">Nov 2023</div>
|
||||||
|
<div class="tag-actions">
|
||||||
|
<button class="action-btn">Edit</button>
|
||||||
|
<button class="action-btn" style="color: #ef4444;">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid-row">
|
||||||
|
<div class="tag-display">
|
||||||
|
<div class="tag-color" style="background: #f59e0b;"></div>
|
||||||
|
<span class="tag-name">bug</span>
|
||||||
|
</div>
|
||||||
|
<div class="tag-count">18 conversations</div>
|
||||||
|
<div class="tag-created">Oct 2023</div>
|
||||||
|
<div class="tag-actions">
|
||||||
|
<button class="action-btn">Edit</button>
|
||||||
|
<button class="action-btn" style="color: #ef4444;">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
};
|
||||||
104
servers/helpscout/src/apps/team-overview.ts
Normal file
104
servers/helpscout/src/apps/team-overview.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
export const teamOverviewApp = {
|
||||||
|
name: 'team-overview',
|
||||||
|
description: 'Team structure and member statistics',
|
||||||
|
content: `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Teams</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f7f9fc; padding: 20px; }
|
||||||
|
.container { max-width: 1200px; margin: 0 auto; }
|
||||||
|
h1 { font-size: 28px; color: #1f2d3d; margin-bottom: 20px; }
|
||||||
|
.teams { display: grid; gap: 20px; }
|
||||||
|
.team-card { background: white; border-radius: 8px; padding: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||||
|
.team-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||||
|
.team-name { font-size: 20px; font-weight: 600; color: #1f2d3d; }
|
||||||
|
.team-count { font-size: 14px; color: #6f7b8a; }
|
||||||
|
.members-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 16px; }
|
||||||
|
.member-card { padding: 16px; border: 1px solid #e5e7eb; border-radius: 6px; display: flex; align-items: center; gap: 12px; }
|
||||||
|
.avatar { width: 48px; height: 48px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; font-weight: 600; font-size: 16px; }
|
||||||
|
.member-info { flex: 1; }
|
||||||
|
.member-name { font-size: 14px; font-weight: 500; color: #1f2d3d; margin-bottom: 2px; }
|
||||||
|
.member-email { font-size: 12px; color: #6f7b8a; }
|
||||||
|
.member-role { font-size: 11px; color: #9ca3af; margin-top: 2px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>👥 Teams</h1>
|
||||||
|
|
||||||
|
<div class="teams">
|
||||||
|
<div class="team-card">
|
||||||
|
<div class="team-header">
|
||||||
|
<div class="team-name">Support Team</div>
|
||||||
|
<div class="team-count">6 members</div>
|
||||||
|
</div>
|
||||||
|
<div class="members-grid">
|
||||||
|
<div class="member-card">
|
||||||
|
<div class="avatar" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">JS</div>
|
||||||
|
<div class="member-info">
|
||||||
|
<div class="member-name">John Smith</div>
|
||||||
|
<div class="member-email">john@company.com</div>
|
||||||
|
<div class="member-role">Team Lead</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="member-card">
|
||||||
|
<div class="avatar" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">ED</div>
|
||||||
|
<div class="member-info">
|
||||||
|
<div class="member-name">Emily Davis</div>
|
||||||
|
<div class="member-email">emily@company.com</div>
|
||||||
|
<div class="member-role">Senior Agent</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="member-card">
|
||||||
|
<div class="avatar" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);">MJ</div>
|
||||||
|
<div class="member-info">
|
||||||
|
<div class="member-name">Mike Johnson</div>
|
||||||
|
<div class="member-email">mike@company.com</div>
|
||||||
|
<div class="member-role">Agent</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="member-card">
|
||||||
|
<div class="avatar" style="background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);">SW</div>
|
||||||
|
<div class="member-info">
|
||||||
|
<div class="member-name">Sarah Wilson</div>
|
||||||
|
<div class="member-email">sarah@company.com</div>
|
||||||
|
<div class="member-role">Agent</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="team-card">
|
||||||
|
<div class="team-header">
|
||||||
|
<div class="team-name">Sales Team</div>
|
||||||
|
<div class="team-count">4 members</div>
|
||||||
|
</div>
|
||||||
|
<div class="members-grid">
|
||||||
|
<div class="member-card">
|
||||||
|
<div class="avatar" style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);">AB</div>
|
||||||
|
<div class="member-info">
|
||||||
|
<div class="member-name">Alex Brown</div>
|
||||||
|
<div class="member-email">alex@company.com</div>
|
||||||
|
<div class="member-role">Sales Lead</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="member-card">
|
||||||
|
<div class="avatar" style="background: linear-gradient(135deg, #30cfd0 0%, #330867 100%);">LG</div>
|
||||||
|
<div class="member-info">
|
||||||
|
<div class="member-name">Lisa Garcia</div>
|
||||||
|
<div class="member-email">lisa@company.com</div>
|
||||||
|
<div class="member-role">Sales Rep</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
};
|
||||||
86
servers/helpscout/src/apps/user-stats.ts
Normal file
86
servers/helpscout/src/apps/user-stats.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
export const userStatsApp = {
|
||||||
|
name: 'user-stats',
|
||||||
|
description: 'Individual user performance statistics and metrics',
|
||||||
|
content: `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>User Statistics</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f7f9fc; padding: 20px; }
|
||||||
|
.container { max-width: 1000px; margin: 0 auto; }
|
||||||
|
.profile { background: white; border-radius: 8px; padding: 32px; text-align: center; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||||
|
.avatar { width: 100px; height: 100px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; font-size: 40px; font-weight: bold; margin: 0 auto 16px; }
|
||||||
|
.user-name { font-size: 24px; font-weight: 600; color: #1f2d3d; margin-bottom: 4px; }
|
||||||
|
.user-role { font-size: 14px; color: #6f7b8a; margin-bottom: 4px; }
|
||||||
|
.user-email { font-size: 13px; color: #9ca3af; }
|
||||||
|
.stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 20px; }
|
||||||
|
.stat-card { background: white; padding: 24px; border-radius: 8px; text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||||
|
.stat-value { font-size: 36px; font-weight: bold; color: #3197d6; margin-bottom: 8px; }
|
||||||
|
.stat-label { font-size: 13px; color: #6f7b8a; }
|
||||||
|
.metrics-section { background: white; border-radius: 8px; padding: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||||
|
.metrics-title { font-size: 18px; font-weight: 600; color: #1f2d3d; margin-bottom: 16px; }
|
||||||
|
.metric-row { display: flex; justify-content: space-between; padding: 12px 0; border-bottom: 1px solid #f0f2f5; }
|
||||||
|
.metric-row:last-child { border-bottom: none; }
|
||||||
|
.metric-label { font-size: 14px; color: #6f7b8a; }
|
||||||
|
.metric-value { font-size: 14px; font-weight: 600; color: #1f2d3d; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="profile">
|
||||||
|
<div class="avatar">JS</div>
|
||||||
|
<div class="user-name">John Smith</div>
|
||||||
|
<div class="user-role">Support Team Lead</div>
|
||||||
|
<div class="user-email">john.smith@company.com</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value">324</div>
|
||||||
|
<div class="stat-label">Replies Sent (30d)</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value">97%</div>
|
||||||
|
<div class="stat-label">Happiness Score</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value">2.8h</div>
|
||||||
|
<div class="stat-label">Avg Response Time</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="metrics-section">
|
||||||
|
<div class="metrics-title">Performance Metrics (Last 30 Days)</div>
|
||||||
|
<div class="metric-row">
|
||||||
|
<span class="metric-label">Conversations Created</span>
|
||||||
|
<span class="metric-value">89</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric-row">
|
||||||
|
<span class="metric-label">Conversations Closed</span>
|
||||||
|
<span class="metric-value">147</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric-row">
|
||||||
|
<span class="metric-label">Average Resolution Time</span>
|
||||||
|
<span class="metric-value">7.2 hours</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric-row">
|
||||||
|
<span class="metric-label">First Response Time</span>
|
||||||
|
<span class="metric-value">2.8 hours</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric-row">
|
||||||
|
<span class="metric-label">Active Conversations</span>
|
||||||
|
<span class="metric-value">23</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric-row">
|
||||||
|
<span class="metric-label">Customer Ratings</span>
|
||||||
|
<span class="metric-value">72 ratings (97% positive)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
};
|
||||||
128
servers/helpscout/src/apps/workflow-dashboard.ts
Normal file
128
servers/helpscout/src/apps/workflow-dashboard.ts
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
export const workflowDashboardApp = {
|
||||||
|
name: 'workflow-dashboard',
|
||||||
|
description: 'Dashboard for workflows with activation status and stats',
|
||||||
|
content: `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Workflows</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f7f9fc; padding: 20px; }
|
||||||
|
.container { max-width: 1200px; margin: 0 auto; }
|
||||||
|
h1 { font-size: 28px; color: #1f2d3d; margin-bottom: 20px; }
|
||||||
|
.workflows { background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||||
|
.workflow-item { padding: 20px; border-bottom: 1px solid #f0f2f5; display: flex; justify-content: between; align-items: center; gap: 16px; }
|
||||||
|
.workflow-info { flex: 1; }
|
||||||
|
.workflow-name { font-size: 16px; font-weight: 600; color: #1f2d3d; margin-bottom: 4px; }
|
||||||
|
.workflow-meta { font-size: 13px; color: #6f7b8a; }
|
||||||
|
.workflow-type { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: 500; background: #e0e7ff; color: #3730a3; margin-right: 8px; }
|
||||||
|
.workflow-stats { display: flex; gap: 24px; align-items: center; }
|
||||||
|
.stat { text-align: center; }
|
||||||
|
.stat-value { font-size: 20px; font-weight: 600; color: #1f2d3d; }
|
||||||
|
.stat-label { font-size: 11px; color: #6f7b8a; text-transform: uppercase; }
|
||||||
|
.toggle { width: 50px; height: 26px; background: #d1d5db; border-radius: 13px; position: relative; cursor: pointer; transition: background 0.3s; }
|
||||||
|
.toggle.active { background: #10b981; }
|
||||||
|
.toggle-knob { width: 22px; height: 22px; background: white; border-radius: 50%; position: absolute; top: 2px; left: 2px; transition: left 0.3s; box-shadow: 0 1px 3px rgba(0,0,0,0.3); }
|
||||||
|
.toggle.active .toggle-knob { left: 26px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>⚡ Workflows</h1>
|
||||||
|
<div class="workflows">
|
||||||
|
<div class="workflow-item">
|
||||||
|
<div class="workflow-info">
|
||||||
|
<div class="workflow-name">
|
||||||
|
<span class="workflow-type">Automatic</span>
|
||||||
|
Auto-tag billing conversations
|
||||||
|
</div>
|
||||||
|
<div class="workflow-meta">Support mailbox • Modified 2 weeks ago</div>
|
||||||
|
</div>
|
||||||
|
<div class="workflow-stats">
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value">247</div>
|
||||||
|
<div class="stat-label">Total Runs</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value">245</div>
|
||||||
|
<div class="stat-label">Successful</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="toggle active" onclick="this.classList.toggle('active')">
|
||||||
|
<div class="toggle-knob"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="workflow-item">
|
||||||
|
<div class="workflow-info">
|
||||||
|
<div class="workflow-name">
|
||||||
|
<span class="workflow-type" style="background: #fef3c7; color: #92400e;">Manual</span>
|
||||||
|
Escalate to senior support
|
||||||
|
</div>
|
||||||
|
<div class="workflow-meta">Support mailbox • Modified 1 month ago</div>
|
||||||
|
</div>
|
||||||
|
<div class="workflow-stats">
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value">34</div>
|
||||||
|
<div class="stat-label">Total Runs</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value">34</div>
|
||||||
|
<div class="stat-label">Successful</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="toggle active" onclick="this.classList.toggle('active')">
|
||||||
|
<div class="toggle-knob"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="workflow-item">
|
||||||
|
<div class="workflow-info">
|
||||||
|
<div class="workflow-name">
|
||||||
|
<span class="workflow-type">Automatic</span>
|
||||||
|
Close spam conversations
|
||||||
|
</div>
|
||||||
|
<div class="workflow-meta">All mailboxes • Modified 3 days ago</div>
|
||||||
|
</div>
|
||||||
|
<div class="workflow-stats">
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value">89</div>
|
||||||
|
<div class="stat-label">Total Runs</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value">89</div>
|
||||||
|
<div class="stat-label">Successful</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="toggle active" onclick="this.classList.toggle('active')">
|
||||||
|
<div class="toggle-knob"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="workflow-item">
|
||||||
|
<div class="workflow-info">
|
||||||
|
<div class="workflow-name">
|
||||||
|
<span class="workflow-type">Automatic</span>
|
||||||
|
Send satisfaction survey
|
||||||
|
</div>
|
||||||
|
<div class="workflow-meta">Support mailbox • Modified 1 week ago</div>
|
||||||
|
</div>
|
||||||
|
<div class="workflow-stats">
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value">156</div>
|
||||||
|
<div class="stat-label">Total Runs</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value">154</div>
|
||||||
|
<div class="stat-label">Successful</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="toggle" onclick="this.classList.toggle('active')">
|
||||||
|
<div class="toggle-knob"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
};
|
||||||
118
servers/helpscout/src/apps/workflow-detail.ts
Normal file
118
servers/helpscout/src/apps/workflow-detail.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
export const workflowDetailApp = {
|
||||||
|
name: 'workflow-detail',
|
||||||
|
description: 'Detailed view of workflow configuration and execution history',
|
||||||
|
content: `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Workflow Detail</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f7f9fc; padding: 20px; }
|
||||||
|
.container { max-width: 1000px; margin: 0 auto; }
|
||||||
|
.header { background: white; padding: 24px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.workflow-info { flex: 1; }
|
||||||
|
.workflow-name { font-size: 24px; font-weight: 600; color: #1f2d3d; margin-bottom: 8px; }
|
||||||
|
.workflow-meta { font-size: 14px; color: #6f7b8a; }
|
||||||
|
.workflow-type { display: inline-block; padding: 4px 10px; border-radius: 4px; font-size: 12px; font-weight: 500; background: #e0e7ff; color: #3730a3; margin-right: 8px; }
|
||||||
|
.toggle { width: 50px; height: 26px; background: #10b981; border-radius: 13px; position: relative; cursor: pointer; }
|
||||||
|
.toggle-knob { width: 22px; height: 22px; background: white; border-radius: 50%; position: absolute; top: 2px; left: 26px; box-shadow: 0 1px 3px rgba(0,0,0,0.3); }
|
||||||
|
.stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 20px; }
|
||||||
|
.stat-card { background: white; padding: 20px; border-radius: 8px; text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||||
|
.stat-value { font-size: 32px; font-weight: bold; color: #3197d6; margin-bottom: 4px; }
|
||||||
|
.stat-label { font-size: 13px; color: #6f7b8a; }
|
||||||
|
.section { background: white; padding: 24px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||||
|
.section-title { font-size: 18px; font-weight: 600; color: #1f2d3d; margin-bottom: 16px; }
|
||||||
|
.condition-item { padding: 16px; background: #f7f9fc; border-radius: 6px; margin-bottom: 12px; border-left: 4px solid #3197d6; }
|
||||||
|
.condition-label { font-size: 12px; font-weight: 600; color: #6f7b8a; margin-bottom: 4px; text-transform: uppercase; }
|
||||||
|
.condition-value { font-size: 14px; color: #1f2d3d; }
|
||||||
|
.action-item { padding: 16px; background: #f0fdf4; border-radius: 6px; margin-bottom: 12px; border-left: 4px solid #10b981; }
|
||||||
|
.action-label { font-size: 12px; font-weight: 600; color: #166534; margin-bottom: 4px; text-transform: uppercase; }
|
||||||
|
.action-value { font-size: 14px; color: #166534; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<div class="workflow-info">
|
||||||
|
<div class="workflow-name">
|
||||||
|
<span class="workflow-type">Automatic</span>
|
||||||
|
Auto-tag billing conversations
|
||||||
|
</div>
|
||||||
|
<div class="workflow-meta">Support mailbox • Modified 2 weeks ago</div>
|
||||||
|
</div>
|
||||||
|
<div class="toggle">
|
||||||
|
<div class="toggle-knob"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value">247</div>
|
||||||
|
<div class="stat-label">Total Runs</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value">245</div>
|
||||||
|
<div class="stat-label">Successful</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value">2</div>
|
||||||
|
<div class="stat-label">Failed</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">⚙️ Conditions (When to run)</div>
|
||||||
|
<div class="condition-item">
|
||||||
|
<div class="condition-label">Trigger</div>
|
||||||
|
<div class="condition-value">Conversation is created</div>
|
||||||
|
</div>
|
||||||
|
<div class="condition-item">
|
||||||
|
<div class="condition-label">Subject contains</div>
|
||||||
|
<div class="condition-value">payment, billing, invoice, subscription</div>
|
||||||
|
</div>
|
||||||
|
<div class="condition-item">
|
||||||
|
<div class="condition-label">Mailbox</div>
|
||||||
|
<div class="condition-value">Support</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">🎯 Actions (What to do)</div>
|
||||||
|
<div class="action-item">
|
||||||
|
<div class="action-label">Add Tags</div>
|
||||||
|
<div class="action-value">billing</div>
|
||||||
|
</div>
|
||||||
|
<div class="action-item">
|
||||||
|
<div class="action-label">Assign To</div>
|
||||||
|
<div class="action-value">Billing Team</div>
|
||||||
|
</div>
|
||||||
|
<div class="action-item">
|
||||||
|
<div class="action-label">Set Priority</div>
|
||||||
|
<div class="action-value">High</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">📊 Recent Executions</div>
|
||||||
|
<div style="padding: 16px; border: 1px solid #e5e7eb; border-radius: 6px; margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<div>
|
||||||
|
<div style="font-weight: 500; color: #1f2d3d;">Conversation #1247: Payment issue</div>
|
||||||
|
<div style="font-size: 13px; color: #6f7b8a;">2 hours ago</div>
|
||||||
|
</div>
|
||||||
|
<div style="color: #10b981; font-weight: 600;">✓ Success</div>
|
||||||
|
</div>
|
||||||
|
<div style="padding: 16px; border: 1px solid #e5e7eb; border-radius: 6px; margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<div>
|
||||||
|
<div style="font-weight: 500; color: #1f2d3d;">Conversation #1189: Billing problem</div>
|
||||||
|
<div style="font-size: 13px; color: #6f7b8a;">3 days ago</div>
|
||||||
|
</div>
|
||||||
|
<div style="color: #10b981; font-weight: 600;">✓ Success</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
};
|
||||||
@ -1,333 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
||||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
||||||
import {
|
|
||||||
CallToolRequestSchema,
|
|
||||||
ListToolsRequestSchema,
|
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// CONFIGURATION
|
|
||||||
// ============================================
|
|
||||||
const MCP_NAME = "helpscout";
|
|
||||||
const MCP_VERSION = "1.0.0";
|
|
||||||
const API_BASE_URL = "https://api.helpscout.net/v2";
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// API CLIENT (OAuth 2.0)
|
|
||||||
// ============================================
|
|
||||||
class HelpScoutClient {
|
|
||||||
private accessToken: string;
|
|
||||||
private baseUrl: string;
|
|
||||||
|
|
||||||
constructor(accessToken: string) {
|
|
||||||
this.accessToken = accessToken;
|
|
||||||
this.baseUrl = API_BASE_URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
async request(endpoint: string, options: RequestInit = {}) {
|
|
||||||
const url = `${this.baseUrl}${endpoint}`;
|
|
||||||
const response = await fetch(url, {
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
"Authorization": `Bearer ${this.accessToken}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...options.headers,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.text();
|
|
||||||
throw new Error(`Help Scout API error: ${response.status} - ${error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some endpoints return 201/204 with no body
|
|
||||||
const text = await response.text();
|
|
||||||
return text ? JSON.parse(text) : { success: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
async get(endpoint: string, params: Record<string, any> = {}) {
|
|
||||||
const url = new URL(`${this.baseUrl}${endpoint}`);
|
|
||||||
for (const [key, value] of Object.entries(params)) {
|
|
||||||
if (value !== undefined && value !== null) {
|
|
||||||
url.searchParams.set(key, String(value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this.request(url.pathname + url.search, { method: "GET" });
|
|
||||||
}
|
|
||||||
|
|
||||||
async post(endpoint: string, data: any) {
|
|
||||||
return this.request(endpoint, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// TOOL DEFINITIONS
|
|
||||||
// ============================================
|
|
||||||
const tools = [
|
|
||||||
{
|
|
||||||
name: "list_conversations",
|
|
||||||
description: "List conversations (tickets) from Help Scout. Returns paginated list with embedded conversation data.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object" as const,
|
|
||||||
properties: {
|
|
||||||
mailbox: { type: "number", description: "Filter by mailbox ID" },
|
|
||||||
status: {
|
|
||||||
type: "string",
|
|
||||||
description: "Filter by status",
|
|
||||||
enum: ["active", "open", "closed", "pending", "spam"]
|
|
||||||
},
|
|
||||||
tag: { type: "string", description: "Filter by tag" },
|
|
||||||
assigned_to: { type: "number", description: "Filter by assigned user ID" },
|
|
||||||
folder: { type: "number", description: "Filter by folder ID" },
|
|
||||||
page: { type: "number", description: "Page number (default 1)" },
|
|
||||||
sortField: { type: "string", description: "Sort field (createdAt, modifiedAt, number)" },
|
|
||||||
sortOrder: { type: "string", enum: ["asc", "desc"], description: "Sort order" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "get_conversation",
|
|
||||||
description: "Get a specific conversation by ID with full thread details",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object" as const,
|
|
||||||
properties: {
|
|
||||||
id: { type: "number", description: "Conversation ID" },
|
|
||||||
},
|
|
||||||
required: ["id"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "create_conversation",
|
|
||||||
description: "Create a new conversation (ticket) in Help Scout",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object" as const,
|
|
||||||
properties: {
|
|
||||||
mailboxId: { type: "number", description: "Mailbox ID (required)" },
|
|
||||||
subject: { type: "string", description: "Conversation subject (required)" },
|
|
||||||
customer: {
|
|
||||||
type: "object",
|
|
||||||
description: "Customer object with email (required): {email: 'customer@example.com'}",
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
type: "string",
|
|
||||||
enum: ["email", "phone", "chat"],
|
|
||||||
description: "Conversation type (default: email)"
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
type: "string",
|
|
||||||
enum: ["active", "closed", "pending"],
|
|
||||||
description: "Initial status (default: active)"
|
|
||||||
},
|
|
||||||
threads: {
|
|
||||||
type: "array",
|
|
||||||
description: "Initial threads [{type: 'customer', text: 'message content'}]",
|
|
||||||
items: { type: "object" }
|
|
||||||
},
|
|
||||||
tags: {
|
|
||||||
type: "array",
|
|
||||||
items: { type: "string" },
|
|
||||||
description: "Tags to apply"
|
|
||||||
},
|
|
||||||
assignTo: { type: "number", description: "User ID to assign to" },
|
|
||||||
},
|
|
||||||
required: ["mailboxId", "subject", "customer"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "reply_conversation",
|
|
||||||
description: "Reply to an existing conversation",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object" as const,
|
|
||||||
properties: {
|
|
||||||
conversationId: { type: "number", description: "Conversation ID to reply to (required)" },
|
|
||||||
text: { type: "string", description: "Reply text/HTML content (required)" },
|
|
||||||
user: { type: "number", description: "User ID sending reply (required for agent replies)" },
|
|
||||||
customer: {
|
|
||||||
type: "object",
|
|
||||||
description: "Customer object for customer replies: {email: 'customer@example.com'}"
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
type: "string",
|
|
||||||
enum: ["reply", "note"],
|
|
||||||
description: "Thread type (reply=visible to customer, note=internal)"
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
type: "string",
|
|
||||||
enum: ["active", "closed", "pending"],
|
|
||||||
description: "Set conversation status after reply"
|
|
||||||
},
|
|
||||||
draft: { type: "boolean", description: "Save as draft" },
|
|
||||||
cc: { type: "array", items: { type: "string" }, description: "CC email addresses" },
|
|
||||||
bcc: { type: "array", items: { type: "string" }, description: "BCC email addresses" },
|
|
||||||
},
|
|
||||||
required: ["conversationId", "text"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "list_customers",
|
|
||||||
description: "List customers from Help Scout",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object" as const,
|
|
||||||
properties: {
|
|
||||||
email: { type: "string", description: "Filter by email address" },
|
|
||||||
firstName: { type: "string", description: "Filter by first name" },
|
|
||||||
lastName: { type: "string", description: "Filter by last name" },
|
|
||||||
query: { type: "string", description: "Search query" },
|
|
||||||
page: { type: "number", description: "Page number" },
|
|
||||||
sortField: { type: "string", description: "Sort field (firstName, lastName, modifiedAt)" },
|
|
||||||
sortOrder: { type: "string", enum: ["asc", "desc"], description: "Sort order" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "list_mailboxes",
|
|
||||||
description: "List all mailboxes accessible to the authenticated user",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object" as const,
|
|
||||||
properties: {
|
|
||||||
page: { type: "number", description: "Page number (default 1)" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "search",
|
|
||||||
description: "Search conversations using Help Scout's search syntax",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object" as const,
|
|
||||||
properties: {
|
|
||||||
query: {
|
|
||||||
type: "string",
|
|
||||||
description: "Search query (required). Supports: subject:, customer:, status:, tag:, mailbox:, etc."
|
|
||||||
},
|
|
||||||
page: { type: "number", description: "Page number" },
|
|
||||||
sortField: { type: "string", description: "Sort field" },
|
|
||||||
sortOrder: { type: "string", enum: ["asc", "desc"], description: "Sort order" },
|
|
||||||
},
|
|
||||||
required: ["query"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// TOOL HANDLERS
|
|
||||||
// ============================================
|
|
||||||
async function handleTool(client: HelpScoutClient, name: string, args: any) {
|
|
||||||
switch (name) {
|
|
||||||
case "list_conversations": {
|
|
||||||
const params: Record<string, any> = {};
|
|
||||||
if (args.mailbox) params.mailbox = args.mailbox;
|
|
||||||
if (args.status) params.status = args.status;
|
|
||||||
if (args.tag) params.tag = args.tag;
|
|
||||||
if (args.assigned_to) params["assigned_to"] = args.assigned_to;
|
|
||||||
if (args.folder) params.folder = args.folder;
|
|
||||||
if (args.page) params.page = args.page;
|
|
||||||
if (args.sortField) params.sortField = args.sortField;
|
|
||||||
if (args.sortOrder) params.sortOrder = args.sortOrder;
|
|
||||||
return await client.get("/conversations", params);
|
|
||||||
}
|
|
||||||
case "get_conversation": {
|
|
||||||
const { id } = args;
|
|
||||||
return await client.get(`/conversations/${id}`);
|
|
||||||
}
|
|
||||||
case "create_conversation": {
|
|
||||||
const payload: any = {
|
|
||||||
mailboxId: args.mailboxId,
|
|
||||||
subject: args.subject,
|
|
||||||
customer: args.customer,
|
|
||||||
type: args.type || "email",
|
|
||||||
status: args.status || "active",
|
|
||||||
};
|
|
||||||
if (args.threads) payload.threads = args.threads;
|
|
||||||
if (args.tags) payload.tags = args.tags;
|
|
||||||
if (args.assignTo) payload.assignTo = args.assignTo;
|
|
||||||
return await client.post("/conversations", payload);
|
|
||||||
}
|
|
||||||
case "reply_conversation": {
|
|
||||||
const { conversationId, ...threadData } = args;
|
|
||||||
const payload: any = {
|
|
||||||
text: threadData.text,
|
|
||||||
type: threadData.type || "reply",
|
|
||||||
};
|
|
||||||
if (threadData.user) payload.user = threadData.user;
|
|
||||||
if (threadData.customer) payload.customer = threadData.customer;
|
|
||||||
if (threadData.status) payload.status = threadData.status;
|
|
||||||
if (threadData.draft) payload.draft = threadData.draft;
|
|
||||||
if (threadData.cc) payload.cc = threadData.cc;
|
|
||||||
if (threadData.bcc) payload.bcc = threadData.bcc;
|
|
||||||
return await client.post(`/conversations/${conversationId}/reply`, payload);
|
|
||||||
}
|
|
||||||
case "list_customers": {
|
|
||||||
const params: Record<string, any> = {};
|
|
||||||
if (args.email) params.email = args.email;
|
|
||||||
if (args.firstName) params.firstName = args.firstName;
|
|
||||||
if (args.lastName) params.lastName = args.lastName;
|
|
||||||
if (args.query) params.query = args.query;
|
|
||||||
if (args.page) params.page = args.page;
|
|
||||||
if (args.sortField) params.sortField = args.sortField;
|
|
||||||
if (args.sortOrder) params.sortOrder = args.sortOrder;
|
|
||||||
return await client.get("/customers", params);
|
|
||||||
}
|
|
||||||
case "list_mailboxes": {
|
|
||||||
return await client.get("/mailboxes", { page: args.page });
|
|
||||||
}
|
|
||||||
case "search": {
|
|
||||||
const params: Record<string, any> = { query: args.query };
|
|
||||||
if (args.page) params.page = args.page;
|
|
||||||
if (args.sortField) params.sortField = args.sortField;
|
|
||||||
if (args.sortOrder) params.sortOrder = args.sortOrder;
|
|
||||||
return await client.get("/conversations/search", params);
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown tool: ${name}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// SERVER SETUP
|
|
||||||
// ============================================
|
|
||||||
async function main() {
|
|
||||||
const accessToken = process.env.HELPSCOUT_ACCESS_TOKEN;
|
|
||||||
if (!accessToken) {
|
|
||||||
console.error("Error: HELPSCOUT_ACCESS_TOKEN environment variable required");
|
|
||||||
console.error("Obtain via OAuth 2.0 flow at https://developer.helpscout.com/mailbox-api/overview/authentication/");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = new HelpScoutClient(accessToken);
|
|
||||||
|
|
||||||
const server = new Server(
|
|
||||||
{ name: `${MCP_NAME}-mcp`, version: MCP_VERSION },
|
|
||||||
{ capabilities: { tools: {} } }
|
|
||||||
);
|
|
||||||
|
|
||||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
||||||
tools,
|
|
||||||
}));
|
|
||||||
|
|
||||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
||||||
const { name, arguments: args } = request.params;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await handleTool(client, name, args || {});
|
|
||||||
return {
|
|
||||||
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
|
||||||
return {
|
|
||||||
content: [{ type: "text", text: `Error: ${message}` }],
|
|
||||||
isError: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const transport = new StdioServerTransport();
|
|
||||||
await server.connect(transport);
|
|
||||||
console.error(`${MCP_NAME} MCP server running on stdio`);
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(console.error);
|
|
||||||
11
servers/helpscout/src/main.ts
Normal file
11
servers/helpscout/src/main.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { runServer } from './server.js';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
|
// Load environment variables from .env file if it exists
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
runServer().catch((error) => {
|
||||||
|
console.error('Fatal error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
186
servers/helpscout/src/server.ts
Normal file
186
servers/helpscout/src/server.ts
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
|
import {
|
||||||
|
ListResourcesRequestSchema,
|
||||||
|
ReadResourceRequestSchema,
|
||||||
|
ListToolsRequestSchema,
|
||||||
|
CallToolRequestSchema,
|
||||||
|
} from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
import { HelpScoutClient } from './api/client.js';
|
||||||
|
import { registerConversationTools } from './tools/conversations-tools.js';
|
||||||
|
import { registerCustomerTools } from './tools/customers-tools.js';
|
||||||
|
import { registerMailboxTools } from './tools/mailboxes-tools.js';
|
||||||
|
import { registerUserTools } from './tools/users-tools.js';
|
||||||
|
import { registerTagTools } from './tools/tags-tools.js';
|
||||||
|
import { registerWorkflowTools } from './tools/workflows-tools.js';
|
||||||
|
import { registerSavedReplyTools } from './tools/saved-replies-tools.js';
|
||||||
|
import { registerTeamTools } from './tools/teams-tools.js';
|
||||||
|
import { registerWebhookTools } from './tools/webhooks-tools.js';
|
||||||
|
import { registerReportingTools } from './tools/reporting-tools.js';
|
||||||
|
|
||||||
|
// Import MCP apps
|
||||||
|
import { conversationDashboardApp } from './apps/conversation-dashboard.js';
|
||||||
|
import { conversationDetailApp } from './apps/conversation-detail.js';
|
||||||
|
import { conversationGridApp } from './apps/conversation-grid.js';
|
||||||
|
import { conversationTimelineApp } from './apps/conversation-timeline.js';
|
||||||
|
import { customerGridApp } from './apps/customer-grid.js';
|
||||||
|
import { customerDetailApp } from './apps/customer-detail.js';
|
||||||
|
import { mailboxOverviewApp } from './apps/mailbox-overview.js';
|
||||||
|
import { folderBrowserApp } from './apps/folder-browser.js';
|
||||||
|
import { userStatsApp } from './apps/user-stats.js';
|
||||||
|
import { tagManagerApp } from './apps/tag-manager.js';
|
||||||
|
import { workflowDashboardApp } from './apps/workflow-dashboard.js';
|
||||||
|
import { workflowDetailApp } from './apps/workflow-detail.js';
|
||||||
|
import { savedRepliesApp } from './apps/saved-replies.js';
|
||||||
|
import { teamOverviewApp } from './apps/team-overview.js';
|
||||||
|
import { happinessReportApp } from './apps/happiness-report.js';
|
||||||
|
import { productivityReportApp } from './apps/productivity-report.js';
|
||||||
|
import { companyReportApp } from './apps/company-report.js';
|
||||||
|
import { searchResultsApp } from './apps/search-results.js';
|
||||||
|
|
||||||
|
export async function runServer() {
|
||||||
|
const appId = process.env.HELPSCOUT_APP_ID;
|
||||||
|
const appSecret = process.env.HELPSCOUT_APP_SECRET;
|
||||||
|
|
||||||
|
if (!appId || !appSecret) {
|
||||||
|
throw new Error(
|
||||||
|
'HELPSCOUT_APP_ID and HELPSCOUT_APP_SECRET environment variables are required'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new HelpScoutClient({ appId, appSecret });
|
||||||
|
|
||||||
|
const server = new Server(
|
||||||
|
{
|
||||||
|
name: 'helpscout-server',
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
resources: {},
|
||||||
|
tools: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Register all tools
|
||||||
|
const allTools = [
|
||||||
|
...registerConversationTools(client),
|
||||||
|
...registerCustomerTools(client),
|
||||||
|
...registerMailboxTools(client),
|
||||||
|
...registerUserTools(client),
|
||||||
|
...registerTagTools(client),
|
||||||
|
...registerWorkflowTools(client),
|
||||||
|
...registerSavedReplyTools(client),
|
||||||
|
...registerTeamTools(client),
|
||||||
|
...registerWebhookTools(client),
|
||||||
|
...registerReportingTools(client),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Register all MCP apps as resources
|
||||||
|
const allApps = [
|
||||||
|
conversationDashboardApp,
|
||||||
|
conversationDetailApp,
|
||||||
|
conversationGridApp,
|
||||||
|
conversationTimelineApp,
|
||||||
|
customerGridApp,
|
||||||
|
customerDetailApp,
|
||||||
|
mailboxOverviewApp,
|
||||||
|
folderBrowserApp,
|
||||||
|
userStatsApp,
|
||||||
|
tagManagerApp,
|
||||||
|
workflowDashboardApp,
|
||||||
|
workflowDetailApp,
|
||||||
|
savedRepliesApp,
|
||||||
|
teamOverviewApp,
|
||||||
|
happinessReportApp,
|
||||||
|
productivityReportApp,
|
||||||
|
companyReportApp,
|
||||||
|
searchResultsApp,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Handle list_resources
|
||||||
|
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
||||||
|
return {
|
||||||
|
resources: allApps.map((app) => ({
|
||||||
|
uri: `helpscout://app/${app.name}`,
|
||||||
|
name: app.name,
|
||||||
|
description: app.description,
|
||||||
|
mimeType: 'text/html',
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle read_resource
|
||||||
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||||
|
const uri = request.params.uri;
|
||||||
|
const appName = uri.replace('helpscout://app/', '');
|
||||||
|
const app = allApps.find((a) => a.name === appName);
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
throw new Error(`App not found: ${appName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
uri,
|
||||||
|
mimeType: 'text/html',
|
||||||
|
text: app.content,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle list_tools
|
||||||
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
|
return {
|
||||||
|
tools: allTools.map((tool) => ({
|
||||||
|
name: tool.name,
|
||||||
|
description: tool.description,
|
||||||
|
inputSchema: tool.inputSchema,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle call_tool
|
||||||
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
const { name, arguments: args } = request.params;
|
||||||
|
const tool = allTools.find((t) => t.name === name);
|
||||||
|
|
||||||
|
if (!tool) {
|
||||||
|
throw new Error(`Tool not found: ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await tool.handler(args || {});
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify({ error: errorMessage }, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await server.connect(transport);
|
||||||
|
|
||||||
|
console.error('HelpScout MCP Server running on stdio');
|
||||||
|
console.error(`Registered ${allTools.length} tools`);
|
||||||
|
console.error(`Registered ${allApps.length} apps`);
|
||||||
|
}
|
||||||
308
servers/helpscout/src/tools/conversations-tools.ts
Normal file
308
servers/helpscout/src/tools/conversations-tools.ts
Normal file
@ -0,0 +1,308 @@
|
|||||||
|
import type { HelpScoutClient } from '../api/client.js';
|
||||||
|
import type { Conversation, Thread } from '../types/index.js';
|
||||||
|
|
||||||
|
export function registerConversationTools(client: HelpScoutClient) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'helpscout_list_conversations',
|
||||||
|
description: 'List conversations with optional filters (mailbox, folder, status, tag, assignee, customer)',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
mailbox: { type: 'number', description: 'Filter by mailbox ID' },
|
||||||
|
folder: { type: 'number', description: 'Filter by folder ID' },
|
||||||
|
status: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['active', 'pending', 'closed', 'spam'],
|
||||||
|
description: 'Filter by status',
|
||||||
|
},
|
||||||
|
tag: { type: 'string', description: 'Filter by tag name' },
|
||||||
|
assignedTo: { type: 'number', description: 'Filter by assigned user ID' },
|
||||||
|
customerId: { type: 'number', description: 'Filter by customer ID' },
|
||||||
|
query: { type: 'string', description: 'Search query' },
|
||||||
|
page: { type: 'number', description: 'Page number (default: 1)' },
|
||||||
|
sortField: { type: 'string', description: 'Field to sort by' },
|
||||||
|
sortOrder: { type: 'string', enum: ['asc', 'desc'] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const conversations = await client.getAllPages<Conversation>(
|
||||||
|
'/conversations',
|
||||||
|
args,
|
||||||
|
'conversations'
|
||||||
|
);
|
||||||
|
return { conversations, count: conversations.length };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_get_conversation',
|
||||||
|
description: 'Get a conversation by ID with full details',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'number', description: 'Conversation ID' },
|
||||||
|
},
|
||||||
|
required: ['id'],
|
||||||
|
},
|
||||||
|
handler: async (args: { id: number }) => {
|
||||||
|
const conversation = await client.get<Conversation>(`/conversations/${args.id}`);
|
||||||
|
return conversation;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_create_conversation',
|
||||||
|
description: 'Create a new conversation (email, chat, or phone)',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
subject: { type: 'string', description: 'Conversation subject' },
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['email', 'chat', 'phone'],
|
||||||
|
description: 'Conversation type',
|
||||||
|
},
|
||||||
|
mailboxId: { type: 'number', description: 'Mailbox ID' },
|
||||||
|
status: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['active', 'pending', 'closed'],
|
||||||
|
description: 'Initial status',
|
||||||
|
},
|
||||||
|
customerId: { type: 'number', description: 'Customer ID' },
|
||||||
|
customerEmail: { type: 'string', description: 'Customer email (if no customerId)' },
|
||||||
|
assignTo: { type: 'number', description: 'User ID to assign to' },
|
||||||
|
tags: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
description: 'Tags to apply',
|
||||||
|
},
|
||||||
|
threads: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'object' },
|
||||||
|
description: 'Initial threads',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['subject', 'type', 'mailboxId'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.post<{ id: number }>(
|
||||||
|
'/conversations',
|
||||||
|
args
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_update_conversation',
|
||||||
|
description: 'Update conversation properties (subject, status, assignee, mailbox, etc)',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'number', description: 'Conversation ID' },
|
||||||
|
op: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['replace', 'remove'],
|
||||||
|
description: 'Operation type',
|
||||||
|
},
|
||||||
|
path: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Property path (e.g., /subject, /status, /assignTo)',
|
||||||
|
},
|
||||||
|
value: { description: 'New value for the property' },
|
||||||
|
},
|
||||||
|
required: ['id', 'op', 'path'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { id, op, path, value } = args;
|
||||||
|
await client.patch(`/conversations/${id}`, { op, path, value });
|
||||||
|
return { success: true, message: 'Conversation updated' };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_delete_conversation',
|
||||||
|
description: 'Delete a conversation permanently',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'number', description: 'Conversation ID' },
|
||||||
|
},
|
||||||
|
required: ['id'],
|
||||||
|
},
|
||||||
|
handler: async (args: { id: number }) => {
|
||||||
|
await client.delete(`/conversations/${args.id}`);
|
||||||
|
return { success: true, message: 'Conversation deleted' };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_list_conversation_threads',
|
||||||
|
description: 'List all threads in a conversation',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
conversationId: { type: 'number', description: 'Conversation ID' },
|
||||||
|
},
|
||||||
|
required: ['conversationId'],
|
||||||
|
},
|
||||||
|
handler: async (args: { conversationId: number }) => {
|
||||||
|
const threads = await client.getAllPages<Thread>(
|
||||||
|
`/conversations/${args.conversationId}/threads`,
|
||||||
|
{},
|
||||||
|
'threads'
|
||||||
|
);
|
||||||
|
return { threads, count: threads.length };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_create_conversation_reply',
|
||||||
|
description: 'Create a reply thread in a conversation',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
conversationId: { type: 'number', description: 'Conversation ID' },
|
||||||
|
text: { type: 'string', description: 'Reply text (HTML supported)' },
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['message', 'reply'],
|
||||||
|
description: 'Thread type',
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['active', 'pending', 'closed'],
|
||||||
|
description: 'Conversation status after reply',
|
||||||
|
},
|
||||||
|
user: { type: 'number', description: 'User ID sending the reply' },
|
||||||
|
attachments: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'object' },
|
||||||
|
description: 'Attachments',
|
||||||
|
},
|
||||||
|
imported: { type: 'boolean', description: 'Mark as imported (no notifications)' },
|
||||||
|
},
|
||||||
|
required: ['conversationId', 'text', 'type'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { conversationId, ...threadData } = args;
|
||||||
|
const response = await client.post(
|
||||||
|
`/conversations/${conversationId}/threads`,
|
||||||
|
threadData
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_create_conversation_note',
|
||||||
|
description: 'Create a private note in a conversation',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
conversationId: { type: 'number', description: 'Conversation ID' },
|
||||||
|
text: { type: 'string', description: 'Note text (HTML supported)' },
|
||||||
|
user: { type: 'number', description: 'User ID creating the note' },
|
||||||
|
},
|
||||||
|
required: ['conversationId', 'text'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { conversationId, text, user } = args;
|
||||||
|
const response = await client.post(
|
||||||
|
`/conversations/${conversationId}/threads`,
|
||||||
|
{
|
||||||
|
text,
|
||||||
|
type: 'note',
|
||||||
|
user,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_create_conversation_phone',
|
||||||
|
description: 'Create a phone thread in a conversation',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
conversationId: { type: 'number', description: 'Conversation ID' },
|
||||||
|
text: { type: 'string', description: 'Phone call notes' },
|
||||||
|
user: { type: 'number', description: 'User ID' },
|
||||||
|
phone: { type: 'string', description: 'Phone number' },
|
||||||
|
},
|
||||||
|
required: ['conversationId', 'text'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { conversationId, ...threadData } = args;
|
||||||
|
const response = await client.post(
|
||||||
|
`/conversations/${conversationId}/threads`,
|
||||||
|
{
|
||||||
|
...threadData,
|
||||||
|
type: 'phone',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_update_conversation_tags',
|
||||||
|
description: 'Update tags on a conversation (add or remove)',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'number', description: 'Conversation ID' },
|
||||||
|
tags: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
description: 'Tag names to set',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['id', 'tags'],
|
||||||
|
},
|
||||||
|
handler: async (args: { id: number; tags: string[] }) => {
|
||||||
|
await client.put(`/conversations/${args.id}/tags`, {
|
||||||
|
tags: args.tags,
|
||||||
|
});
|
||||||
|
return { success: true, message: 'Tags updated' };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_change_conversation_status',
|
||||||
|
description: 'Change conversation status (active, pending, closed, spam)',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'number', description: 'Conversation ID' },
|
||||||
|
status: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['active', 'pending', 'closed', 'spam'],
|
||||||
|
description: 'New status',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['id', 'status'],
|
||||||
|
},
|
||||||
|
handler: async (args: { id: number; status: string }) => {
|
||||||
|
await client.patch(`/conversations/${args.id}`, {
|
||||||
|
op: 'replace',
|
||||||
|
path: '/status',
|
||||||
|
value: args.status,
|
||||||
|
});
|
||||||
|
return { success: true, message: `Status changed to ${args.status}` };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_assign_conversation',
|
||||||
|
description: 'Assign a conversation to a user',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'number', description: 'Conversation ID' },
|
||||||
|
userId: { type: 'number', description: 'User ID to assign to' },
|
||||||
|
},
|
||||||
|
required: ['id', 'userId'],
|
||||||
|
},
|
||||||
|
handler: async (args: { id: number; userId: number }) => {
|
||||||
|
await client.patch(`/conversations/${args.id}`, {
|
||||||
|
op: 'replace',
|
||||||
|
path: '/assignTo',
|
||||||
|
value: args.userId,
|
||||||
|
});
|
||||||
|
return { success: true, message: 'Conversation assigned' };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
268
servers/helpscout/src/tools/customers-tools.ts
Normal file
268
servers/helpscout/src/tools/customers-tools.ts
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
import type { HelpScoutClient } from '../api/client.js';
|
||||||
|
import type { Customer, CustomerEmail, CustomerPhone, CustomerAddress } from '../types/index.js';
|
||||||
|
|
||||||
|
export function registerCustomerTools(client: HelpScoutClient) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'helpscout_list_customers',
|
||||||
|
description: 'List customers with optional filters (email, first name, last name, query)',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
email: { type: 'string', description: 'Filter by email' },
|
||||||
|
firstName: { type: 'string', description: 'Filter by first name' },
|
||||||
|
lastName: { type: 'string', description: 'Filter by last name' },
|
||||||
|
query: { type: 'string', description: 'Search query' },
|
||||||
|
mailbox: { type: 'number', description: 'Filter by mailbox ID' },
|
||||||
|
page: { type: 'number', description: 'Page number (default: 1)' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const customers = await client.getAllPages<Customer>(
|
||||||
|
'/customers',
|
||||||
|
args,
|
||||||
|
'customers'
|
||||||
|
);
|
||||||
|
return { customers, count: customers.length };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_get_customer',
|
||||||
|
description: 'Get a customer by ID with full details',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'number', description: 'Customer ID' },
|
||||||
|
},
|
||||||
|
required: ['id'],
|
||||||
|
},
|
||||||
|
handler: async (args: { id: number }) => {
|
||||||
|
const customer = await client.get<Customer>(`/customers/${args.id}`);
|
||||||
|
return customer;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_create_customer',
|
||||||
|
description: 'Create a new customer',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
firstName: { type: 'string', description: 'First name' },
|
||||||
|
lastName: { type: 'string', description: 'Last name' },
|
||||||
|
email: { type: 'string', description: 'Primary email' },
|
||||||
|
phone: { type: 'string', description: 'Primary phone' },
|
||||||
|
organization: { type: 'string', description: 'Organization name' },
|
||||||
|
jobTitle: { type: 'string', description: 'Job title' },
|
||||||
|
photoUrl: { type: 'string', description: 'Photo URL' },
|
||||||
|
background: { type: 'string', description: 'Background/notes' },
|
||||||
|
location: { type: 'string', description: 'Location' },
|
||||||
|
age: { type: 'string', description: 'Age' },
|
||||||
|
gender: { type: 'string', description: 'Gender' },
|
||||||
|
},
|
||||||
|
required: ['firstName', 'lastName'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.post<{ id: number }>(
|
||||||
|
'/customers',
|
||||||
|
args
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_update_customer',
|
||||||
|
description: 'Update customer properties',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'number', description: 'Customer ID' },
|
||||||
|
op: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['replace', 'remove'],
|
||||||
|
description: 'Operation type',
|
||||||
|
},
|
||||||
|
path: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Property path (e.g., /firstName, /email)',
|
||||||
|
},
|
||||||
|
value: { description: 'New value' },
|
||||||
|
},
|
||||||
|
required: ['id', 'op', 'path'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { id, op, path, value } = args;
|
||||||
|
await client.patch(`/customers/${id}`, { op, path, value });
|
||||||
|
return { success: true, message: 'Customer updated' };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_delete_customer',
|
||||||
|
description: 'Delete a customer permanently',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'number', description: 'Customer ID' },
|
||||||
|
},
|
||||||
|
required: ['id'],
|
||||||
|
},
|
||||||
|
handler: async (args: { id: number }) => {
|
||||||
|
await client.delete(`/customers/${args.id}`);
|
||||||
|
return { success: true, message: 'Customer deleted' };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_list_customer_emails',
|
||||||
|
description: 'List all emails for a customer',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
customerId: { type: 'number', description: 'Customer ID' },
|
||||||
|
},
|
||||||
|
required: ['customerId'],
|
||||||
|
},
|
||||||
|
handler: async (args: { customerId: number }) => {
|
||||||
|
const emails = await client.getAllPages<CustomerEmail>(
|
||||||
|
`/customers/${args.customerId}/emails`,
|
||||||
|
{},
|
||||||
|
'emails'
|
||||||
|
);
|
||||||
|
return { emails, count: emails.length };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_create_customer_email',
|
||||||
|
description: 'Add an email address to a customer',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
customerId: { type: 'number', description: 'Customer ID' },
|
||||||
|
value: { type: 'string', description: 'Email address' },
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['work', 'home', 'other'],
|
||||||
|
description: 'Email type',
|
||||||
|
},
|
||||||
|
location: { type: 'string', description: 'Location label' },
|
||||||
|
},
|
||||||
|
required: ['customerId', 'value'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { customerId, ...emailData } = args;
|
||||||
|
const response = await client.post(
|
||||||
|
`/customers/${customerId}/emails`,
|
||||||
|
emailData
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_list_customer_phones',
|
||||||
|
description: 'List all phone numbers for a customer',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
customerId: { type: 'number', description: 'Customer ID' },
|
||||||
|
},
|
||||||
|
required: ['customerId'],
|
||||||
|
},
|
||||||
|
handler: async (args: { customerId: number }) => {
|
||||||
|
const phones = await client.getAllPages<CustomerPhone>(
|
||||||
|
`/customers/${args.customerId}/phones`,
|
||||||
|
{},
|
||||||
|
'phones'
|
||||||
|
);
|
||||||
|
return { phones, count: phones.length };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_create_customer_phone',
|
||||||
|
description: 'Add a phone number to a customer',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
customerId: { type: 'number', description: 'Customer ID' },
|
||||||
|
value: { type: 'string', description: 'Phone number' },
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['work', 'home', 'mobile', 'fax', 'other'],
|
||||||
|
description: 'Phone type',
|
||||||
|
},
|
||||||
|
location: { type: 'string', description: 'Location label' },
|
||||||
|
},
|
||||||
|
required: ['customerId', 'value'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { customerId, ...phoneData } = args;
|
||||||
|
const response = await client.post(
|
||||||
|
`/customers/${customerId}/phones`,
|
||||||
|
phoneData
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_list_customer_addresses',
|
||||||
|
description: 'List all addresses for a customer',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
customerId: { type: 'number', description: 'Customer ID' },
|
||||||
|
},
|
||||||
|
required: ['customerId'],
|
||||||
|
},
|
||||||
|
handler: async (args: { customerId: number }) => {
|
||||||
|
const addresses = await client.getAllPages<CustomerAddress>(
|
||||||
|
`/customers/${args.customerId}/addresses`,
|
||||||
|
{},
|
||||||
|
'addresses'
|
||||||
|
);
|
||||||
|
return { addresses, count: addresses.length };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_create_customer_address',
|
||||||
|
description: 'Add an address to a customer',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
customerId: { type: 'number', description: 'Customer ID' },
|
||||||
|
city: { type: 'string', description: 'City' },
|
||||||
|
state: { type: 'string', description: 'State/Province' },
|
||||||
|
postalCode: { type: 'string', description: 'Postal code' },
|
||||||
|
country: { type: 'string', description: 'Country code (ISO 3166-1 alpha-2)' },
|
||||||
|
lines: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
description: 'Address lines',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['customerId', 'city', 'country'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { customerId, ...addressData } = args;
|
||||||
|
const response = await client.post(
|
||||||
|
`/customers/${customerId}/addresses`,
|
||||||
|
addressData
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_list_customer_properties',
|
||||||
|
description: 'List all custom properties for a customer',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
customerId: { type: 'number', description: 'Customer ID' },
|
||||||
|
},
|
||||||
|
required: ['customerId'],
|
||||||
|
},
|
||||||
|
handler: async (args: { customerId: number }) => {
|
||||||
|
const properties = await client.get<any>(
|
||||||
|
`/customers/${args.customerId}/properties`
|
||||||
|
);
|
||||||
|
return properties;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
97
servers/helpscout/src/tools/mailboxes-tools.ts
Normal file
97
servers/helpscout/src/tools/mailboxes-tools.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import type { HelpScoutClient } from '../api/client.js';
|
||||||
|
import type { Mailbox, Folder, CustomField } from '../types/index.js';
|
||||||
|
|
||||||
|
export function registerMailboxTools(client: HelpScoutClient) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'helpscout_list_mailboxes',
|
||||||
|
description: 'List all mailboxes',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
page: { type: 'number', description: 'Page number (default: 1)' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const mailboxes = await client.getAllPages<Mailbox>(
|
||||||
|
'/mailboxes',
|
||||||
|
args,
|
||||||
|
'mailboxes'
|
||||||
|
);
|
||||||
|
return { mailboxes, count: mailboxes.length };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_get_mailbox',
|
||||||
|
description: 'Get a mailbox by ID with full details',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'number', description: 'Mailbox ID' },
|
||||||
|
},
|
||||||
|
required: ['id'],
|
||||||
|
},
|
||||||
|
handler: async (args: { id: number }) => {
|
||||||
|
const mailbox = await client.get<Mailbox>(`/mailboxes/${args.id}`);
|
||||||
|
return mailbox;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_list_mailbox_folders',
|
||||||
|
description: 'List all folders in a mailbox',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
mailboxId: { type: 'number', description: 'Mailbox ID' },
|
||||||
|
page: { type: 'number', description: 'Page number (default: 1)' },
|
||||||
|
},
|
||||||
|
required: ['mailboxId'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const folders = await client.getAllPages<Folder>(
|
||||||
|
`/mailboxes/${args.mailboxId}/folders`,
|
||||||
|
{ page: args.page },
|
||||||
|
'folders'
|
||||||
|
);
|
||||||
|
return { folders, count: folders.length };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_get_mailbox_folder',
|
||||||
|
description: 'Get a specific folder by ID',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
mailboxId: { type: 'number', description: 'Mailbox ID' },
|
||||||
|
folderId: { type: 'number', description: 'Folder ID' },
|
||||||
|
},
|
||||||
|
required: ['mailboxId', 'folderId'],
|
||||||
|
},
|
||||||
|
handler: async (args: { mailboxId: number; folderId: number }) => {
|
||||||
|
const folder = await client.get<Folder>(
|
||||||
|
`/mailboxes/${args.mailboxId}/folders/${args.folderId}`
|
||||||
|
);
|
||||||
|
return folder;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_list_mailbox_fields',
|
||||||
|
description: 'List custom fields for a mailbox',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
mailboxId: { type: 'number', description: 'Mailbox ID' },
|
||||||
|
},
|
||||||
|
required: ['mailboxId'],
|
||||||
|
},
|
||||||
|
handler: async (args: { mailboxId: number }) => {
|
||||||
|
const fields = await client.getAllPages<CustomField>(
|
||||||
|
`/mailboxes/${args.mailboxId}/fields`,
|
||||||
|
{},
|
||||||
|
'fields'
|
||||||
|
);
|
||||||
|
return { fields, count: fields.length };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
244
servers/helpscout/src/tools/reporting-tools.ts
Normal file
244
servers/helpscout/src/tools/reporting-tools.ts
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
import type { HelpScoutClient } from '../api/client.js';
|
||||||
|
import type {
|
||||||
|
CompanyReport,
|
||||||
|
ConversationReport,
|
||||||
|
HappinessReport,
|
||||||
|
ProductivityReport,
|
||||||
|
UserReport,
|
||||||
|
} from '../types/index.js';
|
||||||
|
|
||||||
|
export function registerReportingTools(client: HelpScoutClient) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'helpscout_get_company_report',
|
||||||
|
description: 'Get company overview report (conversations, customers, happiness, response times)',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
start: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Start date (YYYY-MM-DD)',
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'End date (YYYY-MM-DD)',
|
||||||
|
},
|
||||||
|
previousStart: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Previous period start (for comparison)',
|
||||||
|
},
|
||||||
|
previousEnd: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Previous period end (for comparison)',
|
||||||
|
},
|
||||||
|
mailboxes: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'number' },
|
||||||
|
description: 'Filter by mailbox IDs',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
description: 'Filter by tags',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['start', 'end'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const report = await client.get<CompanyReport>(
|
||||||
|
'/reports/company',
|
||||||
|
args
|
||||||
|
);
|
||||||
|
return report;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_get_conversations_report',
|
||||||
|
description: 'Get conversations report (volume, trends, busiest times)',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
start: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Start date (YYYY-MM-DD)',
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'End date (YYYY-MM-DD)',
|
||||||
|
},
|
||||||
|
previousStart: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Previous period start (for comparison)',
|
||||||
|
},
|
||||||
|
previousEnd: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Previous period end (for comparison)',
|
||||||
|
},
|
||||||
|
mailboxes: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'number' },
|
||||||
|
description: 'Filter by mailbox IDs',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
description: 'Filter by tags',
|
||||||
|
},
|
||||||
|
folders: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'number' },
|
||||||
|
description: 'Filter by folder IDs',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['start', 'end'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const report = await client.get<ConversationReport>(
|
||||||
|
'/reports/conversations',
|
||||||
|
args
|
||||||
|
);
|
||||||
|
return report;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_get_happiness_report',
|
||||||
|
description: 'Get happiness report (ratings, scores, sentiment)',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
start: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Start date (YYYY-MM-DD)',
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'End date (YYYY-MM-DD)',
|
||||||
|
},
|
||||||
|
previousStart: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Previous period start (for comparison)',
|
||||||
|
},
|
||||||
|
previousEnd: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Previous period end (for comparison)',
|
||||||
|
},
|
||||||
|
mailboxes: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'number' },
|
||||||
|
description: 'Filter by mailbox IDs',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
description: 'Filter by tags',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['start', 'end'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const report = await client.get<HappinessReport>(
|
||||||
|
'/reports/happiness',
|
||||||
|
args
|
||||||
|
);
|
||||||
|
return report;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_get_productivity_report',
|
||||||
|
description: 'Get productivity report (replies sent, resolution time, response time)',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
start: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Start date (YYYY-MM-DD)',
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'End date (YYYY-MM-DD)',
|
||||||
|
},
|
||||||
|
previousStart: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Previous period start (for comparison)',
|
||||||
|
},
|
||||||
|
previousEnd: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Previous period end (for comparison)',
|
||||||
|
},
|
||||||
|
mailboxes: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'number' },
|
||||||
|
description: 'Filter by mailbox IDs',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
description: 'Filter by tags',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['start', 'end'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const report = await client.get<ProductivityReport>(
|
||||||
|
'/reports/productivity',
|
||||||
|
args
|
||||||
|
);
|
||||||
|
return report;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_get_user_report',
|
||||||
|
description: 'Get report for a specific user (performance, activity)',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
userId: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'User ID to report on',
|
||||||
|
},
|
||||||
|
start: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Start date (YYYY-MM-DD)',
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'End date (YYYY-MM-DD)',
|
||||||
|
},
|
||||||
|
previousStart: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Previous period start (for comparison)',
|
||||||
|
},
|
||||||
|
previousEnd: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Previous period end (for comparison)',
|
||||||
|
},
|
||||||
|
mailboxes: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'number' },
|
||||||
|
description: 'Filter by mailbox IDs',
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
description: 'Filter by tags',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['userId', 'start', 'end'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const report = await client.get<UserReport>(
|
||||||
|
`/reports/user/${args.userId}`,
|
||||||
|
{
|
||||||
|
start: args.start,
|
||||||
|
end: args.end,
|
||||||
|
previousStart: args.previousStart,
|
||||||
|
previousEnd: args.previousEnd,
|
||||||
|
mailboxes: args.mailboxes,
|
||||||
|
tags: args.tags,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return report;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
102
servers/helpscout/src/tools/saved-replies-tools.ts
Normal file
102
servers/helpscout/src/tools/saved-replies-tools.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import type { HelpScoutClient } from '../api/client.js';
|
||||||
|
import type { SavedReply } from '../types/index.js';
|
||||||
|
|
||||||
|
export function registerSavedReplyTools(client: HelpScoutClient) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'helpscout_list_saved_replies',
|
||||||
|
description: 'List saved replies with optional filters',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
mailboxId: { type: 'number', description: 'Filter by mailbox ID' },
|
||||||
|
userId: { type: 'number', description: 'Filter by user ID' },
|
||||||
|
page: { type: 'number', description: 'Page number (default: 1)' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const replies = await client.getAllPages<SavedReply>(
|
||||||
|
'/saved-replies',
|
||||||
|
args,
|
||||||
|
'replies'
|
||||||
|
);
|
||||||
|
return { replies, count: replies.length };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_get_saved_reply',
|
||||||
|
description: 'Get a saved reply by ID',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'number', description: 'Saved reply ID' },
|
||||||
|
},
|
||||||
|
required: ['id'],
|
||||||
|
},
|
||||||
|
handler: async (args: { id: number }) => {
|
||||||
|
const reply = await client.get<SavedReply>(`/saved-replies/${args.id}`);
|
||||||
|
return reply;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_create_saved_reply',
|
||||||
|
description: 'Create a new saved reply',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
name: { type: 'string', description: 'Reply name/title' },
|
||||||
|
text: { type: 'string', description: 'Reply text (HTML supported)' },
|
||||||
|
mailboxId: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Mailbox ID (for mailbox-specific reply)',
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'User ID (for user-specific reply)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['name', 'text'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.post<{ id: number }>(
|
||||||
|
'/saved-replies',
|
||||||
|
args
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_update_saved_reply',
|
||||||
|
description: 'Update a saved reply',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'number', description: 'Saved reply ID' },
|
||||||
|
name: { type: 'string', description: 'New name' },
|
||||||
|
text: { type: 'string', description: 'New text' },
|
||||||
|
},
|
||||||
|
required: ['id'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { id, ...updates } = args;
|
||||||
|
await client.put(`/saved-replies/${id}`, updates);
|
||||||
|
return { success: true, message: 'Saved reply updated' };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_delete_saved_reply',
|
||||||
|
description: 'Delete a saved reply',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'number', description: 'Saved reply ID' },
|
||||||
|
},
|
||||||
|
required: ['id'],
|
||||||
|
},
|
||||||
|
handler: async (args: { id: number }) => {
|
||||||
|
await client.delete(`/saved-replies/${args.id}`);
|
||||||
|
return { success: true, message: 'Saved reply deleted' };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
77
servers/helpscout/src/tools/tags-tools.ts
Normal file
77
servers/helpscout/src/tools/tags-tools.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import type { HelpScoutClient } from '../api/client.js';
|
||||||
|
import type { Tag } from '../types/index.js';
|
||||||
|
|
||||||
|
export function registerTagTools(client: HelpScoutClient) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'helpscout_list_tags',
|
||||||
|
description: 'List all tags in the account',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
page: { type: 'number', description: 'Page number (default: 1)' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const tags = await client.getAllPages<Tag>(
|
||||||
|
'/tags',
|
||||||
|
args,
|
||||||
|
'tags'
|
||||||
|
);
|
||||||
|
return { tags, count: tags.length };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_create_tag',
|
||||||
|
description: 'Create a new tag',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
name: { type: 'string', description: 'Tag name' },
|
||||||
|
color: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Tag color (hex code, e.g., #FF5733)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['name'],
|
||||||
|
},
|
||||||
|
handler: async (args: { name: string; color?: string }) => {
|
||||||
|
const response = await client.post<{ id: number }>('/tags', args);
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_update_tag',
|
||||||
|
description: 'Update a tag (rename or change color)',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'number', description: 'Tag ID' },
|
||||||
|
name: { type: 'string', description: 'New tag name' },
|
||||||
|
color: { type: 'string', description: 'New tag color (hex code)' },
|
||||||
|
},
|
||||||
|
required: ['id'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { id, ...updates } = args;
|
||||||
|
await client.put(`/tags/${id}`, updates);
|
||||||
|
return { success: true, message: 'Tag updated' };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_delete_tag',
|
||||||
|
description: 'Delete a tag',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'number', description: 'Tag ID' },
|
||||||
|
},
|
||||||
|
required: ['id'],
|
||||||
|
},
|
||||||
|
handler: async (args: { id: number }) => {
|
||||||
|
await client.delete(`/tags/${args.id}`);
|
||||||
|
return { success: true, message: 'Tag deleted' };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
60
servers/helpscout/src/tools/teams-tools.ts
Normal file
60
servers/helpscout/src/tools/teams-tools.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import type { HelpScoutClient } from '../api/client.js';
|
||||||
|
import type { Team, User } from '../types/index.js';
|
||||||
|
|
||||||
|
export function registerTeamTools(client: HelpScoutClient) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'helpscout_list_teams',
|
||||||
|
description: 'List all teams',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
page: { type: 'number', description: 'Page number (default: 1)' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const teams = await client.getAllPages<Team>(
|
||||||
|
'/teams',
|
||||||
|
args,
|
||||||
|
'teams'
|
||||||
|
);
|
||||||
|
return { teams, count: teams.length };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_get_team',
|
||||||
|
description: 'Get a team by ID with full details',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'number', description: 'Team ID' },
|
||||||
|
},
|
||||||
|
required: ['id'],
|
||||||
|
},
|
||||||
|
handler: async (args: { id: number }) => {
|
||||||
|
const team = await client.get<Team>(`/teams/${args.id}`);
|
||||||
|
return team;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_list_team_members',
|
||||||
|
description: 'List all members of a team',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
teamId: { type: 'number', description: 'Team ID' },
|
||||||
|
page: { type: 'number', description: 'Page number (default: 1)' },
|
||||||
|
},
|
||||||
|
required: ['teamId'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const members = await client.getAllPages<User>(
|
||||||
|
`/teams/${args.teamId}/members`,
|
||||||
|
{ page: args.page },
|
||||||
|
'members'
|
||||||
|
);
|
||||||
|
return { members, count: members.length };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
53
servers/helpscout/src/tools/users-tools.ts
Normal file
53
servers/helpscout/src/tools/users-tools.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import type { HelpScoutClient } from '../api/client.js';
|
||||||
|
import type { User } from '../types/index.js';
|
||||||
|
|
||||||
|
export function registerUserTools(client: HelpScoutClient) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'helpscout_list_users',
|
||||||
|
description: 'List all users in the account',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
page: { type: 'number', description: 'Page number (default: 1)' },
|
||||||
|
email: { type: 'string', description: 'Filter by email' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const users = await client.getAllPages<User>(
|
||||||
|
'/users',
|
||||||
|
args,
|
||||||
|
'users'
|
||||||
|
);
|
||||||
|
return { users, count: users.length };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_get_user',
|
||||||
|
description: 'Get a user by ID with full details',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'number', description: 'User ID' },
|
||||||
|
},
|
||||||
|
required: ['id'],
|
||||||
|
},
|
||||||
|
handler: async (args: { id: number }) => {
|
||||||
|
const user = await client.get<User>(`/users/${args.id}`);
|
||||||
|
return user;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_get_current_user',
|
||||||
|
description: 'Get the resource owner (current authenticated user)',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
handler: async () => {
|
||||||
|
const user = await client.get<User>('/users/me');
|
||||||
|
return user;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
109
servers/helpscout/src/tools/webhooks-tools.ts
Normal file
109
servers/helpscout/src/tools/webhooks-tools.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import type { HelpScoutClient } from '../api/client.js';
|
||||||
|
import type { Webhook } from '../types/index.js';
|
||||||
|
|
||||||
|
export function registerWebhookTools(client: HelpScoutClient) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'helpscout_list_webhooks',
|
||||||
|
description: 'List all webhooks',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
page: { type: 'number', description: 'Page number (default: 1)' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const webhooks = await client.getAllPages<Webhook>(
|
||||||
|
'/webhooks',
|
||||||
|
args,
|
||||||
|
'webhooks'
|
||||||
|
);
|
||||||
|
return { webhooks, count: webhooks.length };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_get_webhook',
|
||||||
|
description: 'Get a webhook by ID',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'string', description: 'Webhook ID' },
|
||||||
|
},
|
||||||
|
required: ['id'],
|
||||||
|
},
|
||||||
|
handler: async (args: { id: string }) => {
|
||||||
|
const webhook = await client.get<Webhook>(`/webhooks/${args.id}`);
|
||||||
|
return webhook;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_create_webhook',
|
||||||
|
description: 'Create a new webhook',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
url: { type: 'string', description: 'Webhook URL' },
|
||||||
|
events: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
description: 'Events to subscribe to (e.g., conversation.created)',
|
||||||
|
},
|
||||||
|
secret: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Webhook secret for signature verification',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['url', 'events'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.post<{ id: string }>(
|
||||||
|
'/webhooks',
|
||||||
|
args
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_update_webhook',
|
||||||
|
description: 'Update a webhook',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'string', description: 'Webhook ID' },
|
||||||
|
url: { type: 'string', description: 'New webhook URL' },
|
||||||
|
events: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
description: 'New events list',
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['enabled', 'disabled'],
|
||||||
|
description: 'Webhook state',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['id'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { id, ...updates } = args;
|
||||||
|
await client.put(`/webhooks/${id}`, updates);
|
||||||
|
return { success: true, message: 'Webhook updated' };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_delete_webhook',
|
||||||
|
description: 'Delete a webhook',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'string', description: 'Webhook ID' },
|
||||||
|
},
|
||||||
|
required: ['id'],
|
||||||
|
},
|
||||||
|
handler: async (args: { id: string }) => {
|
||||||
|
await client.delete(`/webhooks/${args.id}`);
|
||||||
|
return { success: true, message: 'Webhook deleted' };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
96
servers/helpscout/src/tools/workflows-tools.ts
Normal file
96
servers/helpscout/src/tools/workflows-tools.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import type { HelpScoutClient } from '../api/client.js';
|
||||||
|
import type { Workflow, WorkflowStats } from '../types/index.js';
|
||||||
|
|
||||||
|
export function registerWorkflowTools(client: HelpScoutClient) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'helpscout_list_workflows',
|
||||||
|
description: 'List all workflows with optional filters',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
mailboxId: { type: 'number', description: 'Filter by mailbox ID' },
|
||||||
|
page: { type: 'number', description: 'Page number (default: 1)' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const workflows = await client.getAllPages<Workflow>(
|
||||||
|
'/workflows',
|
||||||
|
args,
|
||||||
|
'workflows'
|
||||||
|
);
|
||||||
|
return { workflows, count: workflows.length };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_get_workflow',
|
||||||
|
description: 'Get a workflow by ID with full details',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'number', description: 'Workflow ID' },
|
||||||
|
},
|
||||||
|
required: ['id'],
|
||||||
|
},
|
||||||
|
handler: async (args: { id: number }) => {
|
||||||
|
const workflow = await client.get<Workflow>(`/workflows/${args.id}`);
|
||||||
|
return workflow;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_activate_workflow',
|
||||||
|
description: 'Activate a workflow',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'number', description: 'Workflow ID' },
|
||||||
|
},
|
||||||
|
required: ['id'],
|
||||||
|
},
|
||||||
|
handler: async (args: { id: number }) => {
|
||||||
|
await client.patch(`/workflows/${args.id}`, {
|
||||||
|
op: 'replace',
|
||||||
|
path: '/status',
|
||||||
|
value: 'active',
|
||||||
|
});
|
||||||
|
return { success: true, message: 'Workflow activated' };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_deactivate_workflow',
|
||||||
|
description: 'Deactivate a workflow',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'number', description: 'Workflow ID' },
|
||||||
|
},
|
||||||
|
required: ['id'],
|
||||||
|
},
|
||||||
|
handler: async (args: { id: number }) => {
|
||||||
|
await client.patch(`/workflows/${args.id}`, {
|
||||||
|
op: 'replace',
|
||||||
|
path: '/status',
|
||||||
|
value: 'inactive',
|
||||||
|
});
|
||||||
|
return { success: true, message: 'Workflow deactivated' };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'helpscout_get_workflow_stats',
|
||||||
|
description: 'Get statistics for a workflow (run counts)',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'number', description: 'Workflow ID' },
|
||||||
|
},
|
||||||
|
required: ['id'],
|
||||||
|
},
|
||||||
|
handler: async (args: { id: number }) => {
|
||||||
|
const stats = await client.get<WorkflowStats>(
|
||||||
|
`/workflows/${args.id}/stats`
|
||||||
|
);
|
||||||
|
return stats;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
320
servers/helpscout/src/types/index.ts
Normal file
320
servers/helpscout/src/types/index.ts
Normal file
@ -0,0 +1,320 @@
|
|||||||
|
// HelpScout API Types
|
||||||
|
|
||||||
|
export interface HelpScoutConfig {
|
||||||
|
appId: string;
|
||||||
|
appSecret: string;
|
||||||
|
accessToken?: string;
|
||||||
|
tokenExpiry?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccessTokenResponse {
|
||||||
|
access_token: string;
|
||||||
|
expires_in: number;
|
||||||
|
token_type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginatedResponse<T> {
|
||||||
|
_embedded?: { [key: string]: T[] };
|
||||||
|
_links?: {
|
||||||
|
first?: { href: string };
|
||||||
|
last?: { href: string };
|
||||||
|
next?: { href: string };
|
||||||
|
prev?: { href: string };
|
||||||
|
self?: { href: string };
|
||||||
|
};
|
||||||
|
page?: {
|
||||||
|
number: number;
|
||||||
|
size: number;
|
||||||
|
totalElements: number;
|
||||||
|
totalPages: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Conversation {
|
||||||
|
id: number;
|
||||||
|
number: number;
|
||||||
|
threads: number;
|
||||||
|
type: 'email' | 'chat' | 'phone';
|
||||||
|
folderId: number;
|
||||||
|
status: 'active' | 'pending' | 'closed' | 'spam';
|
||||||
|
state: 'draft' | 'published' | 'deleted';
|
||||||
|
subject: string;
|
||||||
|
preview: string;
|
||||||
|
mailboxId: number;
|
||||||
|
assignee?: User;
|
||||||
|
createdBy: User | Customer;
|
||||||
|
createdAt: string;
|
||||||
|
closedAt?: string;
|
||||||
|
closedBy?: User;
|
||||||
|
updatedAt?: string;
|
||||||
|
userUpdatedAt?: string;
|
||||||
|
customerWaitingSince?: CustomerWaitingSince;
|
||||||
|
source?: ConversationSource;
|
||||||
|
tags?: Tag[];
|
||||||
|
cc?: string[];
|
||||||
|
bcc?: string[];
|
||||||
|
primaryCustomer: Customer;
|
||||||
|
customFields?: CustomField[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Thread {
|
||||||
|
id: number;
|
||||||
|
assignedTo?: User;
|
||||||
|
status: 'active' | 'nochange' | 'pending' | 'closed';
|
||||||
|
createdAt: string;
|
||||||
|
createdBy: User | Customer;
|
||||||
|
source?: ThreadSource;
|
||||||
|
type: 'note' | 'message' | 'reply' | 'customer' | 'lineitem' | 'forwardparent' | 'forwardchild' | 'phone';
|
||||||
|
state: 'draft' | 'published' | 'deleted' | 'hidden';
|
||||||
|
customer?: Customer;
|
||||||
|
fromMailbox?: Mailbox;
|
||||||
|
body?: string;
|
||||||
|
to?: string[];
|
||||||
|
cc?: string[];
|
||||||
|
bcc?: string[];
|
||||||
|
attachments?: Attachment[];
|
||||||
|
savedReplyId?: number;
|
||||||
|
openedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Customer {
|
||||||
|
id: number;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
phone?: string;
|
||||||
|
photoUrl?: string;
|
||||||
|
photoType?: string;
|
||||||
|
gender?: string;
|
||||||
|
age?: string;
|
||||||
|
organization?: string;
|
||||||
|
jobTitle?: string;
|
||||||
|
location?: string;
|
||||||
|
background?: string;
|
||||||
|
address?: CustomerAddress;
|
||||||
|
socialProfiles?: SocialProfile[];
|
||||||
|
chats?: Chat[];
|
||||||
|
websites?: Website[];
|
||||||
|
emails?: CustomerEmail[];
|
||||||
|
phones?: CustomerPhone[];
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomerAddress {
|
||||||
|
id: number;
|
||||||
|
city: string;
|
||||||
|
state: string;
|
||||||
|
postalCode: string;
|
||||||
|
country: string;
|
||||||
|
lines: string[];
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomerEmail {
|
||||||
|
id: number;
|
||||||
|
value: string;
|
||||||
|
type: string;
|
||||||
|
location: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomerPhone {
|
||||||
|
id: number;
|
||||||
|
value: string;
|
||||||
|
type: string;
|
||||||
|
location: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SocialProfile {
|
||||||
|
type: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Chat {
|
||||||
|
id: number;
|
||||||
|
type: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Website {
|
||||||
|
id: number;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Mailbox {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
email: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Folder {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
type: 'mine' | 'drafts' | 'assigned' | 'closed' | 'spam' | 'open';
|
||||||
|
userId: number;
|
||||||
|
totalCount: number;
|
||||||
|
activeCount: number;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: number;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
timezone: string;
|
||||||
|
photoUrl?: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
type?: 'user' | 'team';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Tag {
|
||||||
|
id?: number;
|
||||||
|
name?: string;
|
||||||
|
color?: string;
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
ticketCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Workflow {
|
||||||
|
id: number;
|
||||||
|
mailboxId: number;
|
||||||
|
type: 'manual' | 'automatic';
|
||||||
|
status: 'active' | 'inactive' | 'invalid';
|
||||||
|
order: number;
|
||||||
|
name: string;
|
||||||
|
createdAt: string;
|
||||||
|
modifiedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkflowStats {
|
||||||
|
totalRuns: number;
|
||||||
|
successfulRuns: number;
|
||||||
|
failedRuns: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SavedReply {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
text: string;
|
||||||
|
mailboxId?: number;
|
||||||
|
userId?: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Team {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
memberCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Webhook {
|
||||||
|
id: string;
|
||||||
|
url: string;
|
||||||
|
secret: string;
|
||||||
|
events: string[];
|
||||||
|
state: 'enabled' | 'disabled';
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomField {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
value: any;
|
||||||
|
type: 'TEXT' | 'NUMBER' | 'DATE' | 'DROPDOWN';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConversationSource {
|
||||||
|
type: string;
|
||||||
|
via: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThreadSource {
|
||||||
|
type: string;
|
||||||
|
via: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomerWaitingSince {
|
||||||
|
time: string;
|
||||||
|
friendly: string;
|
||||||
|
latestReplyFrom: 'customer' | 'user';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Attachment {
|
||||||
|
id: number;
|
||||||
|
mimeType: string;
|
||||||
|
filename: string;
|
||||||
|
size: number;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Report {
|
||||||
|
filterTags?: any;
|
||||||
|
current?: any;
|
||||||
|
previous?: any;
|
||||||
|
deltas?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompanyReport extends Report {
|
||||||
|
current?: {
|
||||||
|
conversationsCreated: number;
|
||||||
|
conversationsClosed: number;
|
||||||
|
customersHelped: number;
|
||||||
|
happinessScore: number;
|
||||||
|
repliesSent: number;
|
||||||
|
resolutionTime: number;
|
||||||
|
firstResponseTime: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConversationReport extends Report {
|
||||||
|
current?: {
|
||||||
|
newConversations: number;
|
||||||
|
totalConversations: number;
|
||||||
|
conversationsBusiest: any;
|
||||||
|
conversationsVolumeByDay: any[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HappinessReport extends Report {
|
||||||
|
current?: {
|
||||||
|
happinessScore: number;
|
||||||
|
ratingsCount: number;
|
||||||
|
greatCount: number;
|
||||||
|
okayCount: number;
|
||||||
|
notGoodCount: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductivityReport extends Report {
|
||||||
|
current?: {
|
||||||
|
repliesSent: number;
|
||||||
|
resolutionTime: number;
|
||||||
|
firstResponseTime: number;
|
||||||
|
responseTime: number;
|
||||||
|
resolvedConversations: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserReport extends Report {
|
||||||
|
current?: {
|
||||||
|
user: User;
|
||||||
|
repliesSent: number;
|
||||||
|
conversationsCreated: number;
|
||||||
|
conversationsClosed: number;
|
||||||
|
happinessScore: number;
|
||||||
|
resolutionTime: number;
|
||||||
|
firstResponseTime: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,14 +1,19 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "NodeNext",
|
"module": "Node16",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "Node16",
|
||||||
|
"lib": ["ES2022"],
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"declaration": true
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user