@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 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
+ }