@feedmob/user-activity-reporting 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -0
- package/dist/api.js +169 -0
- package/dist/index.js +137 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# User Activity Reporting MCP
|
|
2
|
+
|
|
3
|
+
MCP server for querying client contacts, Slack messages, and HubSpot tickets.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
| Tool | Description |
|
|
8
|
+
|------|-------------|
|
|
9
|
+
| `get_all_client_contacts` | List all clients with team members |
|
|
10
|
+
| `get_client_team_members` | Get team (AA, AM, AE, PM, PA, AO) for a client |
|
|
11
|
+
| `get_clients_by_pod` | List clients in a POD team |
|
|
12
|
+
| `get_clients_by_name` | Find clients by person name |
|
|
13
|
+
| `get_user_slack_history` | Search Slack messages from a user |
|
|
14
|
+
| `get_hubspot_tickets` | Query HubSpot tickets |
|
|
15
|
+
| `get_hubspot_ticket_detail` | Get ticket details |
|
|
16
|
+
| `get_hubspot_tickets_by_user` | Find tickets by owner |
|
|
17
|
+
|
|
18
|
+
## Environment Variables
|
|
19
|
+
|
|
20
|
+
| Variable | Required | Description |
|
|
21
|
+
|----------|----------|-------------|
|
|
22
|
+
| `FEEDMOB_API_BASE` | Yes | Feedmob Admin API URL (e.g., `https://admin.feedmob.com`) |
|
|
23
|
+
| `FEEDMOB_KEY` | Yes | Feedmob API key |
|
|
24
|
+
| `FEEDMOB_SECRET` | Yes | Feedmob API secret |
|
|
25
|
+
| `SLACK_BOT_TOKEN` | No | Slack Bot token for message search |
|
|
26
|
+
| `HUBSPOT_ACCESS_TOKEN` | No | HubSpot private app token |
|
|
27
|
+
|
|
28
|
+
## Setup
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
cd src/user-activity-reporting
|
|
32
|
+
npm install
|
|
33
|
+
npm run build
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Development
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm run dev # Run with hot reload
|
|
40
|
+
npm run inspect # Test tools interactively
|
|
41
|
+
npm run build # Compile to dist/
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## MCP Configuration
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"mcpServers": {
|
|
49
|
+
"user-activity-reporting": {
|
|
50
|
+
"command": "npx",
|
|
51
|
+
"args": ["-y", "@feedmob/user-activity-reporting"],
|
|
52
|
+
"env": {
|
|
53
|
+
"FEEDMOB_API_BASE": "https://admin.feedmob.com",
|
|
54
|
+
"FEEDMOB_KEY": "your_key",
|
|
55
|
+
"FEEDMOB_SECRET": "your_secret",
|
|
56
|
+
"SLACK_BOT_TOKEN": "xoxb-xxx",
|
|
57
|
+
"HUBSPOT_ACCESS_TOKEN": "pat-xxx"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Usage Examples
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
# Get team for a client
|
|
68
|
+
Tool: get_client_team_members
|
|
69
|
+
Args: { "client_name": "Uber" }
|
|
70
|
+
|
|
71
|
+
# Find clients by person
|
|
72
|
+
Tool: get_clients_by_name
|
|
73
|
+
Args: { "name": "John", "role": "am" }
|
|
74
|
+
|
|
75
|
+
# Search Slack messages
|
|
76
|
+
Tool: get_user_slack_history
|
|
77
|
+
Args: { "user_name": "John", "query": "budget" }
|
|
78
|
+
```
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import jwt from 'jsonwebtoken';
|
|
2
|
+
import dotenv from 'dotenv';
|
|
3
|
+
dotenv.config();
|
|
4
|
+
const API_BASE = process.env.FEEDMOB_API_BASE;
|
|
5
|
+
const API_KEY = process.env.FEEDMOB_KEY;
|
|
6
|
+
const API_SECRET = process.env.FEEDMOB_SECRET;
|
|
7
|
+
if (!API_KEY || !API_SECRET) {
|
|
8
|
+
console.error("Error: FEEDMOB_KEY and FEEDMOB_SECRET must be set.");
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
function genToken() {
|
|
12
|
+
const exp = new Date();
|
|
13
|
+
exp.setDate(exp.getDate() + 7);
|
|
14
|
+
return jwt.sign({ key: API_KEY, expired_at: exp.toISOString().split('T')[0] }, API_SECRET, { algorithm: 'HS256' });
|
|
15
|
+
}
|
|
16
|
+
function buildUrl(path, params) {
|
|
17
|
+
const url = new URL(`${API_BASE}${path}`);
|
|
18
|
+
Object.entries(params).forEach(([k, v]) => url.searchParams.append(k, v));
|
|
19
|
+
return url.toString();
|
|
20
|
+
}
|
|
21
|
+
async function apiGet(path, params = {}) {
|
|
22
|
+
const res = await fetch(buildUrl(path, params), {
|
|
23
|
+
headers: {
|
|
24
|
+
'Content-Type': 'application/json', 'Accept': 'application/json',
|
|
25
|
+
'FEEDMOB-KEY': API_KEY, 'FEEDMOB-TOKEN': genToken()
|
|
26
|
+
},
|
|
27
|
+
signal: AbortSignal.timeout(30000)
|
|
28
|
+
});
|
|
29
|
+
if (res.status === 401)
|
|
30
|
+
throw new Error('Unauthorized: Invalid API Key or Token');
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
const err = await res.json().catch(() => ({}));
|
|
33
|
+
throw new Error(err.error || `API error: ${res.status}`);
|
|
34
|
+
}
|
|
35
|
+
const r = await res.json();
|
|
36
|
+
if (r.status === 404)
|
|
37
|
+
throw new Error(r.error || 'Not found');
|
|
38
|
+
if (r.status === 400)
|
|
39
|
+
throw new Error(r.error || 'Bad request');
|
|
40
|
+
return r.data;
|
|
41
|
+
}
|
|
42
|
+
export async function getAllContacts(month) {
|
|
43
|
+
return apiGet('/ai/api/client_contacts', month ? { month } : {});
|
|
44
|
+
}
|
|
45
|
+
export async function getContactByClient(name, month) {
|
|
46
|
+
const p = { client_name: name };
|
|
47
|
+
if (month)
|
|
48
|
+
p.month = month;
|
|
49
|
+
return apiGet('/ai/api/client_contacts', p);
|
|
50
|
+
}
|
|
51
|
+
export async function getClientsByPod(pod, month) {
|
|
52
|
+
const p = { pod };
|
|
53
|
+
if (month)
|
|
54
|
+
p.month = month;
|
|
55
|
+
return apiGet('/ai/api/client_contacts', p);
|
|
56
|
+
}
|
|
57
|
+
export async function getClientsByRole(role, name, month) {
|
|
58
|
+
const p = { role, name };
|
|
59
|
+
if (month)
|
|
60
|
+
p.month = month;
|
|
61
|
+
return apiGet('/ai/api/client_contacts', p);
|
|
62
|
+
}
|
|
63
|
+
export async function getClientsByName(name, month) {
|
|
64
|
+
const p = { name };
|
|
65
|
+
if (month)
|
|
66
|
+
p.month = month;
|
|
67
|
+
return apiGet('/ai/api/client_contacts', p);
|
|
68
|
+
}
|
|
69
|
+
const SLACK_TOKEN = process.env.SLACK_BOT_TOKEN;
|
|
70
|
+
const HUBSPOT_TOKEN = process.env.HUBSPOT_ACCESS_TOKEN;
|
|
71
|
+
async function slackGet(method, params = {}) {
|
|
72
|
+
if (!SLACK_TOKEN)
|
|
73
|
+
throw new Error('SLACK_BOT_TOKEN not set');
|
|
74
|
+
const url = new URL(`https://slack.com/api/${method}`);
|
|
75
|
+
Object.entries(params).forEach(([k, v]) => url.searchParams.append(k, v));
|
|
76
|
+
const r = await (await fetch(url.toString(), {
|
|
77
|
+
headers: { 'Authorization': `Bearer ${SLACK_TOKEN}`, 'Content-Type': 'application/x-www-form-urlencoded' }
|
|
78
|
+
})).json();
|
|
79
|
+
if (!r.ok)
|
|
80
|
+
throw new Error(`Slack error: ${r.error}`);
|
|
81
|
+
return r;
|
|
82
|
+
}
|
|
83
|
+
export async function findSlackUser(name) {
|
|
84
|
+
const { members = [] } = await slackGet('users.list');
|
|
85
|
+
const n = name.toLowerCase();
|
|
86
|
+
const u = members.find((m) => m.real_name?.toLowerCase().includes(n) || m.name?.toLowerCase().includes(n) || m.profile?.display_name?.toLowerCase().includes(n));
|
|
87
|
+
return u ? { id: u.id, name: u.name, real_name: u.real_name || u.name, email: u.profile?.email } : null;
|
|
88
|
+
}
|
|
89
|
+
export async function searchSlackMsgs(userName, query, limit = 20) {
|
|
90
|
+
const user = await findSlackUser(userName);
|
|
91
|
+
if (!user)
|
|
92
|
+
throw new Error(`Slack user not found: ${userName}`);
|
|
93
|
+
const q = query ? `from:${user.name} ${query}` : `from:${user.name}`;
|
|
94
|
+
const { messages } = await slackGet('search.messages', { query: q, count: String(limit), sort: 'timestamp', sort_dir: 'desc' });
|
|
95
|
+
return (messages?.matches || []).map((m) => ({
|
|
96
|
+
ts: m.ts, text: m.text, user: m.username || user.name,
|
|
97
|
+
channel: m.channel?.name || m.channel?.id || 'unknown', permalink: m.permalink
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
async function hsPost(endpoint, body) {
|
|
101
|
+
if (!HUBSPOT_TOKEN)
|
|
102
|
+
throw new Error('HUBSPOT_ACCESS_TOKEN not set');
|
|
103
|
+
const res = await fetch(`https://api.hubapi.com${endpoint}`, {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: { 'Authorization': `Bearer ${HUBSPOT_TOKEN}`, 'Content-Type': 'application/json' },
|
|
106
|
+
body: JSON.stringify(body)
|
|
107
|
+
});
|
|
108
|
+
if (!res.ok)
|
|
109
|
+
throw new Error(`HubSpot error: ${res.status} - ${await res.text()}`);
|
|
110
|
+
return res.json();
|
|
111
|
+
}
|
|
112
|
+
async function hsGet(endpoint) {
|
|
113
|
+
if (!HUBSPOT_TOKEN)
|
|
114
|
+
throw new Error('HUBSPOT_ACCESS_TOKEN not set');
|
|
115
|
+
const res = await fetch(`https://api.hubapi.com${endpoint}`, {
|
|
116
|
+
headers: { 'Authorization': `Bearer ${HUBSPOT_TOKEN}`, 'Content-Type': 'application/json' }
|
|
117
|
+
});
|
|
118
|
+
if (!res.ok)
|
|
119
|
+
throw new Error(`HubSpot error: ${res.status} - ${await res.text()}`);
|
|
120
|
+
return res.json();
|
|
121
|
+
}
|
|
122
|
+
function mapTicket(t) {
|
|
123
|
+
const p = t.properties;
|
|
124
|
+
return {
|
|
125
|
+
id: t.id, subject: p.subject || 'No Subject', content: p.content,
|
|
126
|
+
status: p.hs_pipeline_stage || 'unknown', priority: p.hs_ticket_priority,
|
|
127
|
+
createdAt: p.createdate, updatedAt: p.hs_lastmodifieddate, owner: p.hubspot_owner_id
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
export async function getTickets(opts = {}) {
|
|
131
|
+
const filters = [];
|
|
132
|
+
if (opts.startDate)
|
|
133
|
+
filters.push({ propertyName: 'createdate', operator: 'GTE', value: new Date(opts.startDate).getTime() });
|
|
134
|
+
if (opts.endDate)
|
|
135
|
+
filters.push({ propertyName: 'createdate', operator: 'LTE', value: new Date(opts.endDate).getTime() });
|
|
136
|
+
if (opts.status)
|
|
137
|
+
filters.push({ propertyName: 'hs_pipeline_stage', operator: 'EQ', value: opts.status });
|
|
138
|
+
const body = {
|
|
139
|
+
properties: ['subject', 'content', 'hs_pipeline_stage', 'hs_ticket_priority', 'createdate', 'hs_lastmodifieddate'],
|
|
140
|
+
limit: opts.limit || 50, sorts: [{ propertyName: 'createdate', direction: 'DESCENDING' }]
|
|
141
|
+
};
|
|
142
|
+
if (filters.length)
|
|
143
|
+
body.filterGroups = [{ filters }];
|
|
144
|
+
const { results = [] } = await hsPost('/crm/v3/objects/tickets/search', body);
|
|
145
|
+
return results.map(mapTicket);
|
|
146
|
+
}
|
|
147
|
+
export async function getTicketById(id) {
|
|
148
|
+
const props = 'subject,content,hs_pipeline_stage,hs_ticket_priority,createdate,hs_lastmodifieddate';
|
|
149
|
+
const data = await hsGet(`/crm/v3/objects/tickets/${id}?properties=${props}`);
|
|
150
|
+
return data ? mapTicket(data) : null;
|
|
151
|
+
}
|
|
152
|
+
export async function getTicketsByUser(opts) {
|
|
153
|
+
const { results: owners = [] } = await hsGet('/crm/v3/owners');
|
|
154
|
+
const term = (opts.userName || opts.email || '').toLowerCase();
|
|
155
|
+
const matched = owners.filter((o) => {
|
|
156
|
+
const fn = (o.firstName || '').toLowerCase(), ln = (o.lastName || '').toLowerCase();
|
|
157
|
+
return fn.includes(term) || ln.includes(term) || `${fn} ${ln}`.includes(term) || (o.email || '').toLowerCase().includes(term);
|
|
158
|
+
});
|
|
159
|
+
if (!matched.length)
|
|
160
|
+
return [];
|
|
161
|
+
const body = {
|
|
162
|
+
properties: ['subject', 'content', 'hs_pipeline_stage', 'hs_ticket_priority', 'createdate', 'hs_lastmodifieddate', 'hubspot_owner_id'],
|
|
163
|
+
limit: opts.limit || 50, sorts: [{ propertyName: 'createdate', direction: 'DESCENDING' }],
|
|
164
|
+
filterGroups: [{ filters: [{ propertyName: 'hubspot_owner_id', operator: 'IN', values: matched.map((o) => o.id) }] }]
|
|
165
|
+
};
|
|
166
|
+
const { results = [] } = await hsPost('/crm/v3/objects/tickets/search', body);
|
|
167
|
+
const ownerMap = new Map(owners.map((o) => [o.id, `${o.firstName || ''} ${o.lastName || ''}`.trim() || o.email]));
|
|
168
|
+
return results.map((t) => ({ ...mapTicket(t), owner: ownerMap.get(t.properties.hubspot_owner_id) }));
|
|
169
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import dotenv from 'dotenv';
|
|
6
|
+
import * as api from './api.js';
|
|
7
|
+
dotenv.config();
|
|
8
|
+
const server = new McpServer({ name: 'user-activity-reporting', version: '0.0.3' });
|
|
9
|
+
const errMsg = (e) => e instanceof Error ? e.message : 'Unknown error';
|
|
10
|
+
const errResp = (msg) => ({ content: [{ type: 'text', text: `Error: ${msg}` }], isError: true });
|
|
11
|
+
const textResp = (text) => ({ content: [{ type: 'text', text }] });
|
|
12
|
+
server.tool('get_all_client_contacts', 'Query all client contacts with team members (POD, AA, AM, AE, PM, PA, AO).', {
|
|
13
|
+
month: z.string().optional().describe('Month in YYYY-MM format'),
|
|
14
|
+
}, async (args) => {
|
|
15
|
+
try {
|
|
16
|
+
const d = await api.getAllContacts(args.month);
|
|
17
|
+
const preview = d.clients.slice(0, 20);
|
|
18
|
+
const more = d.total > 20 ? `\n... and ${d.total - 20} more` : '';
|
|
19
|
+
return textResp(`# All Client Contacts\n\nMonth: ${d.month} | Total: ${d.total}\n\n\`\`\`json\n${JSON.stringify(preview, null, 2)}\n\`\`\`${more}`);
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
return errResp(errMsg(e));
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
server.tool('get_client_team_members', 'Query team members for a client. Returns POD, AA, AM, AE, PM, PA, AO.', {
|
|
26
|
+
client_name: z.string().describe('Client name (fuzzy match)'),
|
|
27
|
+
month: z.string().optional().describe('Month in YYYY-MM format'),
|
|
28
|
+
}, async (args) => {
|
|
29
|
+
try {
|
|
30
|
+
const d = await api.getContactByClient(args.client_name, args.month);
|
|
31
|
+
const team = { POD: d.pod || 'N/A', AA: d.aa || 'N/A', AM: d.am || 'N/A', AE: d.ae || 'N/A', PM: d.pm || 'N/A', PA: d.pa || 'N/A', AO: d.ao || 'N/A' };
|
|
32
|
+
return textResp(`# Team for "${d.client_name}"\n\n\`\`\`json\n${JSON.stringify({ client_id: d.client_id, client_name: d.client_name, month: d.month, team }, null, 2)}\n\`\`\``);
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
return errResp(errMsg(e));
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
server.tool('get_clients_by_pod', 'Query clients in a POD team.', {
|
|
39
|
+
pod: z.string().describe('POD name (fuzzy match)'),
|
|
40
|
+
month: z.string().optional().describe('Month in YYYY-MM format'),
|
|
41
|
+
}, async (args) => {
|
|
42
|
+
try {
|
|
43
|
+
const d = await api.getClientsByPod(args.pod, args.month);
|
|
44
|
+
return textResp(`# Clients in POD: ${d.pod}\n\nMonth: ${d.month} | Count: ${d.count}\n\n${d.client_names.map(c => `- ${c}`).join('\n')}`);
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
return errResp(errMsg(e));
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
server.tool('get_clients_by_name', 'Query clients managed by a person. Can filter by role.', {
|
|
51
|
+
name: z.string().describe('Person name'),
|
|
52
|
+
role: z.enum(['aa', 'am', 'ae', 'pm', 'pa', 'ao']).optional().describe('Role filter'),
|
|
53
|
+
month: z.string().optional().describe('Month in YYYY-MM format'),
|
|
54
|
+
}, async (args) => {
|
|
55
|
+
try {
|
|
56
|
+
if (args.role) {
|
|
57
|
+
const d = await api.getClientsByRole(args.role, args.name, args.month);
|
|
58
|
+
return textResp(`# Clients for ${d.role.toUpperCase()}: ${d.name}\n\nMonth: ${d.month} | Count: ${d.count}\n\n${d.client_names.map(c => `- ${c}`).join('\n')}`);
|
|
59
|
+
}
|
|
60
|
+
const d = await api.getClientsByName(args.name, args.month);
|
|
61
|
+
const lines = Object.entries(d.results).map(([r, cs]) => `**${r}:** ${cs.join(', ')}`);
|
|
62
|
+
return textResp(`# Clients for "${d.name}"\n\nMonth: ${d.month}\n\n${lines.join('\n') || 'No clients found'}`);
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
return errResp(errMsg(e));
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
server.tool('get_user_slack_history', 'Search Slack messages from a user.', {
|
|
69
|
+
user_name: z.string().describe('User name'),
|
|
70
|
+
query: z.string().optional().describe('Keyword filter'),
|
|
71
|
+
limit: z.number().optional().default(20).describe('Max results'),
|
|
72
|
+
}, async (args) => {
|
|
73
|
+
try {
|
|
74
|
+
const msgs = await api.searchSlackMsgs(args.user_name, args.query, args.limit || 20);
|
|
75
|
+
if (!msgs.length)
|
|
76
|
+
return textResp(`No Slack messages found for: ${args.user_name}`);
|
|
77
|
+
const fmt = msgs.map(m => ({ channel: m.channel, text: m.text.slice(0, 200) + (m.text.length > 200 ? '...' : ''), ts: new Date(parseFloat(m.ts) * 1000).toISOString(), link: m.permalink }));
|
|
78
|
+
return textResp(`# Slack Messages from ${args.user_name}\n\nFound ${msgs.length}\n\n\`\`\`json\n${JSON.stringify(fmt, null, 2)}\n\`\`\``);
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
return errResp(errMsg(e));
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
server.tool('get_hubspot_tickets', 'Query HubSpot tickets.', {
|
|
85
|
+
status: z.string().optional().describe('Status filter'),
|
|
86
|
+
start_date: z.string().optional().describe('Start date YYYY-MM-DD'),
|
|
87
|
+
end_date: z.string().optional().describe('End date YYYY-MM-DD'),
|
|
88
|
+
limit: z.number().optional().default(50).describe('Max results'),
|
|
89
|
+
}, async (args) => {
|
|
90
|
+
try {
|
|
91
|
+
const tickets = await api.getTickets({ status: args.status, startDate: args.start_date, endDate: args.end_date, limit: args.limit });
|
|
92
|
+
if (!tickets.length)
|
|
93
|
+
return textResp('No HubSpot tickets found');
|
|
94
|
+
const fmt = tickets.map(t => ({ id: t.id, subject: t.subject, status: t.status, priority: t.priority || 'N/A', created: t.createdAt }));
|
|
95
|
+
return textResp(`# HubSpot Tickets\n\nFound ${tickets.length}\n\n\`\`\`json\n${JSON.stringify(fmt, null, 2)}\n\`\`\``);
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
return errResp(errMsg(e));
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
server.tool('get_hubspot_ticket_detail', 'Get HubSpot ticket details.', {
|
|
102
|
+
ticket_id: z.string().describe('Ticket ID'),
|
|
103
|
+
}, async (args) => {
|
|
104
|
+
try {
|
|
105
|
+
const t = await api.getTicketById(args.ticket_id);
|
|
106
|
+
if (!t)
|
|
107
|
+
return textResp(`Ticket not found: ${args.ticket_id}`);
|
|
108
|
+
return textResp(`# ${t.subject}\n\n**ID:** ${t.id}\n**Status:** ${t.status}\n**Priority:** ${t.priority || 'N/A'}\n**Created:** ${t.createdAt}\n\n## Description\n\n${t.content || 'No content'}`);
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
return errResp(errMsg(e));
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
server.tool('get_hubspot_tickets_by_user', 'Query HubSpot tickets by user.', {
|
|
115
|
+
user_name: z.string().optional().describe('User name'),
|
|
116
|
+
email: z.string().optional().describe('Email'),
|
|
117
|
+
limit: z.number().optional().default(50).describe('Max results'),
|
|
118
|
+
}, async (args) => {
|
|
119
|
+
try {
|
|
120
|
+
if (!args.user_name && !args.email)
|
|
121
|
+
return errResp('Provide user_name or email');
|
|
122
|
+
const tickets = await api.getTicketsByUser({ userName: args.user_name, email: args.email, limit: args.limit });
|
|
123
|
+
if (!tickets.length)
|
|
124
|
+
return textResp(`No tickets found for: ${args.user_name || args.email}`);
|
|
125
|
+
const fmt = tickets.map(t => ({ id: t.id, subject: t.subject, status: t.status, created: t.createdAt }));
|
|
126
|
+
return textResp(`# Tickets for "${args.user_name || args.email}"\n\nFound ${tickets.length}\n\n\`\`\`json\n${JSON.stringify(fmt, null, 2)}\n\`\`\``);
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
return errResp(errMsg(e));
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
async function main() {
|
|
133
|
+
const transport = new StdioServerTransport();
|
|
134
|
+
await server.connect(transport);
|
|
135
|
+
console.error('User Activity Reporting MCP Server running...');
|
|
136
|
+
}
|
|
137
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@feedmob/user-activity-reporting",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "MCP server for querying client contacts, Slack messages, and HubSpot tickets",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"user-activity-reporting": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/anthropics/fm-mcp-servers.git"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mcp",
|
|
19
|
+
"model-context-protocol",
|
|
20
|
+
"user-activity",
|
|
21
|
+
"reporting"
|
|
22
|
+
],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"scripts": {
|
|
25
|
+
"dev": "tsx src/index.ts",
|
|
26
|
+
"inspect": "npx fastmcp inspect dist/index.js",
|
|
27
|
+
"build": "tsc",
|
|
28
|
+
"start": "node dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
32
|
+
"zod": "^3.24.2",
|
|
33
|
+
"jsonwebtoken": "^9.0.2",
|
|
34
|
+
"dotenv": "^16.4.5"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/jsonwebtoken": "^9.0.9",
|
|
38
|
+
"@types/node": "^22.13.10",
|
|
39
|
+
"tsx": "^4.19.3",
|
|
40
|
+
"typescript": "^5.8.2"
|
|
41
|
+
}
|
|
42
|
+
}
|