@mohammad_noman/saleem-dashboard-mcp 1.0.0
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 +136 -0
- package/package.json +26 -0
- package/saleem-dashboard-mcp.js +446 -0
package/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Saleem Dashboard MCP Server
|
|
2
|
+
|
|
3
|
+
This MCP server wraps the Saleem internal dashboard API routes as tools, so Claude can query live data and trigger agents directly from a conversation.
|
|
4
|
+
|
|
5
|
+
## Team goal
|
|
6
|
+
|
|
7
|
+
Use this MCP server from Claude Desktop without cloning this repository or using machine-specific paths.
|
|
8
|
+
|
|
9
|
+
## What it does
|
|
10
|
+
|
|
11
|
+
Exposes 8 tools to Claude:
|
|
12
|
+
|
|
13
|
+
| Tool | Description |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `get_pipeline_health` | Deals exceeding SLA thresholds, sorted by days stalled |
|
|
16
|
+
| `get_funnel_data` | Mixpanel consultation funnel conversion rates (last 30 days) |
|
|
17
|
+
| `get_kpi_status` | All team KPIs with on_track / at_risk / behind status |
|
|
18
|
+
| `get_urgent_items` | Unresolved urgent items, filterable by owner / priority |
|
|
19
|
+
| `get_deal_priorities` | Fatima's morning list: new leads, at-risk deals, today's follow-ups |
|
|
20
|
+
| `get_weekly_brief` | Latest compiled weekly brief from all agents |
|
|
21
|
+
| `run_agent` | Trigger a diagnostic agent and return its output |
|
|
22
|
+
| `create_urgent_item` | Create a new urgent item visible on the team dashboard |
|
|
23
|
+
|
|
24
|
+
## Running locally
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
cd mcp
|
|
28
|
+
npm install
|
|
29
|
+
node saleem-dashboard-mcp.js
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
You should see:
|
|
33
|
+
```
|
|
34
|
+
[mcp] Saleem Dashboard MCP server running
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Publish once, run anywhere
|
|
38
|
+
|
|
39
|
+
This package is configured as a public scoped package under your npm username, so teammates can install it through `npx` without private-package billing.
|
|
40
|
+
|
|
41
|
+
### 1) Rotate and set the service key
|
|
42
|
+
|
|
43
|
+
1. Generate a new `MCP_SERVICE_KEY`.
|
|
44
|
+
2. Set that key in Vercel project environment variables (Production and Preview).
|
|
45
|
+
3. Share the key only through your team secret manager.
|
|
46
|
+
|
|
47
|
+
### 2) Publish the MCP package
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
cd mcp
|
|
51
|
+
npm login
|
|
52
|
+
npm publish --access public
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
If you later decide to move to a private registry, the same package structure still works; only the publish command and teammate auth change.
|
|
56
|
+
|
|
57
|
+
### 3) Add the server in Claude Desktop (team config)
|
|
58
|
+
|
|
59
|
+
Open Claude Desktop config:
|
|
60
|
+
|
|
61
|
+
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
62
|
+
- Windows: `%APPDATA%\\Claude\\claude_desktop_config.json`
|
|
63
|
+
|
|
64
|
+
Use this block:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"mcpServers": {
|
|
69
|
+
"saleem-dashboard": {
|
|
70
|
+
"command": "npx",
|
|
71
|
+
"args": ["-y", "@mohammad_noman/saleem-dashboard-mcp@1.0.0"],
|
|
72
|
+
"env": {
|
|
73
|
+
"DASHBOARD_BASE_URL": "https://analytics-dashboard-five-gamma.vercel.app",
|
|
74
|
+
"MCP_SERVICE_KEY": "YOUR_ROTATED_KEY"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Notes:
|
|
82
|
+
|
|
83
|
+
- Pin the package version in `args` for stable behavior.
|
|
84
|
+
- Bump version and update the pinned version when releasing updates.
|
|
85
|
+
|
|
86
|
+
## Adding to Claude Desktop
|
|
87
|
+
|
|
88
|
+
Open `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows) and add:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"mcpServers": {
|
|
93
|
+
"saleem-dashboard": {
|
|
94
|
+
"command": "npx",
|
|
95
|
+
"args": ["-y", "@mohammad_noman/saleem-dashboard-mcp@1.0.0"],
|
|
96
|
+
"env": {
|
|
97
|
+
"DASHBOARD_BASE_URL": "https://analytics-dashboard-five-gamma.vercel.app",
|
|
98
|
+
"MCP_SERVICE_KEY": "YOUR_ROTATED_KEY"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Restart Claude Desktop after saving. You should see "saleem-dashboard" appear in the tools panel.
|
|
106
|
+
|
|
107
|
+
## Verifying it works
|
|
108
|
+
|
|
109
|
+
Run the smoke test (reads from the live dashboard):
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
node mcp/test.js
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
This calls `get_pipeline_health`, `get_kpi_status`, and `get_urgent_items` directly and prints the results.
|
|
116
|
+
|
|
117
|
+
## Required environment variables
|
|
118
|
+
|
|
119
|
+
| Variable | Where | Description |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| `DASHBOARD_BASE_URL` | MCP server env | Base URL of the deployed dashboard |
|
|
122
|
+
| `MCP_SERVICE_KEY` | MCP server env **and** Vercel env | Shared secret — must match on both sides |
|
|
123
|
+
|
|
124
|
+
Set `MCP_SERVICE_KEY` as a Vercel environment variable (Production + Preview) so the deployed API routes accept requests from the MCP server.
|
|
125
|
+
|
|
126
|
+
## Release checklist
|
|
127
|
+
|
|
128
|
+
1. Update code in this folder.
|
|
129
|
+
2. Bump version in `mcp/package.json`.
|
|
130
|
+
3. Run `npm pack --dry-run` from `mcp`.
|
|
131
|
+
4. Publish with `npm publish --access public`.
|
|
132
|
+
5. Share the new pinned version string with the team.
|
|
133
|
+
|
|
134
|
+
## How authentication works
|
|
135
|
+
|
|
136
|
+
The MCP server sends `x-mcp-key: <MCP_SERVICE_KEY>` on every request. API routes that normally require a session cookie check for this header first — if it matches, the session check is skipped and the request is treated as a `ceo`-role caller. The key is never exposed to the browser.
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mohammad_noman/saleem-dashboard-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for Saleem dashboard tools",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "saleem-dashboard-mcp.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"saleem-dashboard-mcp": "saleem-dashboard-mcp.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"saleem-dashboard-mcp.js",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"start": "node saleem-dashboard-mcp.js"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@modelcontextprotocol/sdk": "latest"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Saleem Dashboard MCP Server
|
|
3
|
+
// Wraps internal dashboard API routes as MCP tools so Claude can query
|
|
4
|
+
// live data and trigger agents directly from a conversation.
|
|
5
|
+
//
|
|
6
|
+
// Required env vars:
|
|
7
|
+
// DASHBOARD_BASE_URL — e.g. https://analytics-dashboard-five-gamma.vercel.app
|
|
8
|
+
// MCP_SERVICE_KEY — must match the value set in the dashboard's env
|
|
9
|
+
|
|
10
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
11
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
12
|
+
import {
|
|
13
|
+
CallToolRequestSchema,
|
|
14
|
+
ListToolsRequestSchema,
|
|
15
|
+
} from '@modelcontextprotocol/sdk/types.js'
|
|
16
|
+
|
|
17
|
+
const BASE_URL = (process.env.DASHBOARD_BASE_URL || '').replace(/\/$/, '')
|
|
18
|
+
const SERVICE_KEY = process.env.MCP_SERVICE_KEY || ''
|
|
19
|
+
|
|
20
|
+
if (!BASE_URL) console.error('[mcp] WARNING: DASHBOARD_BASE_URL is not set')
|
|
21
|
+
if (!SERVICE_KEY) console.error('[mcp] WARNING: MCP_SERVICE_KEY is not set')
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// HTTP helpers
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
async function dashFetch(path, { method = 'GET', body } = {}) {
|
|
28
|
+
const url = `${BASE_URL}${path}`
|
|
29
|
+
const opts = {
|
|
30
|
+
method,
|
|
31
|
+
headers: {
|
|
32
|
+
'x-mcp-key': SERVICE_KEY,
|
|
33
|
+
'Content-Type': 'application/json',
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
if (body !== undefined) opts.body = JSON.stringify(body)
|
|
37
|
+
const res = await fetch(url, opts)
|
|
38
|
+
return res.json()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// SLA config — mirrors agents/pipeline-health.js
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
const PIPELINE_CONFIG = {
|
|
46
|
+
'Telemedicine': { won: 'TeleConsult Completed', lost: 'Lost / Inactive' },
|
|
47
|
+
'Treatment': { won: 'Treatment Completed', lost: 'Lost / Inactive' },
|
|
48
|
+
'Doctor': { won: 'Live Doctor', lost: 'Lost Doctor' },
|
|
49
|
+
'Hospital or Clinic': { won: 'Live Partner B2B', lost: 'Lost provider' },
|
|
50
|
+
'Corporates': { won: 'Live', lost: 'Lost Corporates' },
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const SLA_DAYS = {
|
|
54
|
+
'New Deal': 2,
|
|
55
|
+
'Quote Proposed': 3,
|
|
56
|
+
'Payment Done': 1,
|
|
57
|
+
'Consultation Scheduled': 2,
|
|
58
|
+
'Consult Payment': 1,
|
|
59
|
+
'Treatment Quote': 5,
|
|
60
|
+
'Treatment Payment': 3,
|
|
61
|
+
'Treatment Scheduled': 7,
|
|
62
|
+
'Treatment in Progress': 14,
|
|
63
|
+
'Discovery': 5,
|
|
64
|
+
'Outreach': 3,
|
|
65
|
+
'Interest': 5,
|
|
66
|
+
'Agreement': 7,
|
|
67
|
+
'Readiness': 5,
|
|
68
|
+
'Contract Sent': 7,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Tool implementations
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
async function getPipelineHealth() {
|
|
76
|
+
const data = await dashFetch('/api/zoho/crm/deals')
|
|
77
|
+
if (!data.success) throw new Error(data.error || 'Failed to fetch deals')
|
|
78
|
+
|
|
79
|
+
const now = Date.now()
|
|
80
|
+
const stalled = []
|
|
81
|
+
|
|
82
|
+
for (const deal of data.deals) {
|
|
83
|
+
const cfg = PIPELINE_CONFIG[deal.pipeline]
|
|
84
|
+
if (!cfg) continue
|
|
85
|
+
if (deal.stage === cfg.won || deal.stage === cfg.lost) continue
|
|
86
|
+
|
|
87
|
+
const sla = SLA_DAYS[deal.stage]
|
|
88
|
+
if (sla === undefined) continue
|
|
89
|
+
|
|
90
|
+
const daysSince = Math.floor((now - new Date(deal.modified).getTime()) / 86_400_000)
|
|
91
|
+
if (daysSince > sla) {
|
|
92
|
+
stalled.push({
|
|
93
|
+
deal_name: deal.name,
|
|
94
|
+
pipeline: deal.pipeline,
|
|
95
|
+
stage: deal.stage,
|
|
96
|
+
days_stalled: daysSince,
|
|
97
|
+
sla_days: sla,
|
|
98
|
+
owner: deal.contact || 'Unknown',
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
stalled.sort((a, b) => b.days_stalled - a.days_stalled)
|
|
104
|
+
return { stalled_deals: stalled, total_breaches: stalled.length }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function getFunnelData() {
|
|
108
|
+
const data = await dashFetch('/api/mixpanel/funnels')
|
|
109
|
+
if (!data.success) {
|
|
110
|
+
if (data.error === 'MIXPANEL_NOT_CONFIGURED') {
|
|
111
|
+
return { error: 'Mixpanel is not configured on this dashboard instance.' }
|
|
112
|
+
}
|
|
113
|
+
throw new Error(data.error || 'Failed to fetch funnel data')
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
steps: data.steps,
|
|
117
|
+
overall_conversion: data.overall_conversion,
|
|
118
|
+
biggest_drop_off_step: data.biggest_drop_off_step,
|
|
119
|
+
period: data.period,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function getKpiStatus({ person } = {}) {
|
|
124
|
+
const period = new Date().toISOString().slice(0, 7)
|
|
125
|
+
const data = await dashFetch(`/api/kpi/targets?period=${period}`)
|
|
126
|
+
if (!data.success) throw new Error(data.error || 'Failed to fetch KPI targets')
|
|
127
|
+
|
|
128
|
+
let targets = data.targets || []
|
|
129
|
+
if (person) {
|
|
130
|
+
targets = targets.filter(t => t.person?.toLowerCase() === person.toLowerCase())
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const withStatus = targets.map(t => {
|
|
134
|
+
const pct = t.target_value > 0 ? (t.current_value / t.target_value) * 100 : 0
|
|
135
|
+
const status = pct >= 80 ? 'on_track' : pct >= 50 ? 'at_risk' : 'behind'
|
|
136
|
+
return {
|
|
137
|
+
person: t.person,
|
|
138
|
+
metric: t.metric,
|
|
139
|
+
current_value: t.current_value ?? null,
|
|
140
|
+
target_value: t.target_value,
|
|
141
|
+
pct_complete: +pct.toFixed(1),
|
|
142
|
+
status,
|
|
143
|
+
period: t.period,
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
const summary = {
|
|
148
|
+
on_track: withStatus.filter(k => k.status === 'on_track').length,
|
|
149
|
+
at_risk: withStatus.filter(k => k.status === 'at_risk').length,
|
|
150
|
+
behind: withStatus.filter(k => k.status === 'behind').length,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { kpis: withStatus, summary, period }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function getUrgentItems({ owner, priority } = {}) {
|
|
157
|
+
const data = await dashFetch('/api/urgent')
|
|
158
|
+
if (!data.success) throw new Error(data.error || 'Failed to fetch urgent items')
|
|
159
|
+
|
|
160
|
+
let items = data.items || []
|
|
161
|
+
if (owner) items = items.filter(i => i.owner?.toLowerCase() === owner.toLowerCase())
|
|
162
|
+
if (priority) items = items.filter(i => i.priority === priority)
|
|
163
|
+
|
|
164
|
+
return { items, total: items.length }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function getDealPriorities() {
|
|
168
|
+
const data = await dashFetch('/api/zoho/crm/deals')
|
|
169
|
+
if (!data.success) throw new Error(data.error || 'Failed to fetch deals')
|
|
170
|
+
|
|
171
|
+
const today = new Date().toISOString().slice(0, 10)
|
|
172
|
+
const now = Date.now()
|
|
173
|
+
|
|
174
|
+
const newLeads = []
|
|
175
|
+
const atRisk = []
|
|
176
|
+
const followUps = []
|
|
177
|
+
|
|
178
|
+
for (const deal of data.deals) {
|
|
179
|
+
const cfg = PIPELINE_CONFIG[deal.pipeline]
|
|
180
|
+
const isTerminal = cfg && (deal.stage === cfg.won || deal.stage === cfg.lost)
|
|
181
|
+
if (isTerminal) continue
|
|
182
|
+
|
|
183
|
+
if (deal.stage === 'New Deal') {
|
|
184
|
+
newLeads.push({
|
|
185
|
+
name: deal.name,
|
|
186
|
+
pipeline: deal.pipeline,
|
|
187
|
+
created: deal.created,
|
|
188
|
+
owner: deal.contact,
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const daysSinceMod = Math.floor((now - new Date(deal.modified).getTime()) / 86_400_000)
|
|
193
|
+
if (daysSinceMod >= 3) {
|
|
194
|
+
atRisk.push({
|
|
195
|
+
name: deal.name,
|
|
196
|
+
pipeline: deal.pipeline,
|
|
197
|
+
stage: deal.stage,
|
|
198
|
+
days_inactive: daysSinceMod,
|
|
199
|
+
owner: deal.contact,
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (deal.follow_up && deal.follow_up <= today) {
|
|
204
|
+
followUps.push({
|
|
205
|
+
name: deal.name,
|
|
206
|
+
pipeline: deal.pipeline,
|
|
207
|
+
stage: deal.stage,
|
|
208
|
+
follow_up: deal.follow_up,
|
|
209
|
+
owner: deal.contact,
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
newLeads.sort((a, b) => new Date(a.created) - new Date(b.created))
|
|
215
|
+
atRisk.sort((a, b) => b.days_inactive - a.days_inactive)
|
|
216
|
+
followUps.sort((a, b) => a.follow_up.localeCompare(b.follow_up))
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
new_leads: newLeads,
|
|
220
|
+
at_risk: atRisk,
|
|
221
|
+
follow_ups: followUps,
|
|
222
|
+
counts: {
|
|
223
|
+
new_leads: newLeads.length,
|
|
224
|
+
at_risk: atRisk.length,
|
|
225
|
+
follow_ups: followUps.length,
|
|
226
|
+
},
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function getWeeklyBrief() {
|
|
231
|
+
const data = await dashFetch('/api/agents/brief')
|
|
232
|
+
if (!data.success) throw new Error(data.error || 'Failed to fetch weekly brief')
|
|
233
|
+
return { brief: data.brief }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const AGENT_ROUTES = {
|
|
237
|
+
'pipeline-health': '/api/agents/pipeline-health/run',
|
|
238
|
+
'funnel-diagnostic': '/api/agents/funnel-diagnostic/run',
|
|
239
|
+
'task-tracker': '/api/agents/task-tracker/run',
|
|
240
|
+
'department-bottleneck': '/api/agents/department-bottleneck/run',
|
|
241
|
+
'follow-up-generator': '/api/agents/follow-up-generator/run',
|
|
242
|
+
'lead-source-attribution': '/api/agents/lead-source-attribution/run',
|
|
243
|
+
'weekly-brief-compiler': '/api/agents/weekly-brief-compiler/run',
|
|
244
|
+
'provider-list-builder': '/api/agents/provider-list-builder/run',
|
|
245
|
+
'corporate-list-builder': '/api/agents/corporate-list-builder/run',
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function runAgent({ agent_name }) {
|
|
249
|
+
const route = AGENT_ROUTES[agent_name]
|
|
250
|
+
if (!route) {
|
|
251
|
+
const valid = Object.keys(AGENT_ROUTES).join(', ')
|
|
252
|
+
throw new Error(`Unknown agent "${agent_name}". Valid agents: ${valid}`)
|
|
253
|
+
}
|
|
254
|
+
const data = await dashFetch(route)
|
|
255
|
+
if (data.error && !data.success) throw new Error(data.error)
|
|
256
|
+
return data
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function createUrgentItem({ title, description, owner, priority, due_date }) {
|
|
260
|
+
if (!title?.trim()) throw new Error('title is required')
|
|
261
|
+
if (!owner?.trim()) throw new Error('owner is required')
|
|
262
|
+
if (!priority) throw new Error('priority is required')
|
|
263
|
+
|
|
264
|
+
const VALID_PRIORITIES = ['critical', 'high', 'medium', 'low']
|
|
265
|
+
if (!VALID_PRIORITIES.includes(priority)) {
|
|
266
|
+
throw new Error(`priority must be one of: ${VALID_PRIORITIES.join(', ')}`)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const data = await dashFetch('/api/urgent', {
|
|
270
|
+
method: 'POST',
|
|
271
|
+
body: {
|
|
272
|
+
title: title.trim(),
|
|
273
|
+
description: description?.trim() || null,
|
|
274
|
+
owner: owner.trim(),
|
|
275
|
+
priority,
|
|
276
|
+
due_at: due_date || null,
|
|
277
|
+
},
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
if (!data.success) throw new Error(data.error || 'Failed to create urgent item')
|
|
281
|
+
return { created: true, item: data.item }
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
// Tool definitions
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
const TOOLS = [
|
|
289
|
+
{
|
|
290
|
+
name: 'get_pipeline_health',
|
|
291
|
+
description: 'Get all deals currently exceeding SLA thresholds. Returns stalled deals by pipeline with days stalled and owner.',
|
|
292
|
+
inputSchema: {
|
|
293
|
+
type: 'object',
|
|
294
|
+
properties: {},
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
name: 'get_funnel_data',
|
|
299
|
+
description: 'Get the current Mixpanel consultation funnel conversion rates for the last 30 days.',
|
|
300
|
+
inputSchema: {
|
|
301
|
+
type: 'object',
|
|
302
|
+
properties: {},
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
name: 'get_kpi_status',
|
|
307
|
+
description: 'Get all team KPI targets and their current values for the current month. Shows who is on track and who is behind.',
|
|
308
|
+
inputSchema: {
|
|
309
|
+
type: 'object',
|
|
310
|
+
properties: {
|
|
311
|
+
person: {
|
|
312
|
+
type: 'string',
|
|
313
|
+
description: 'Filter to one person\'s KPIs (optional)',
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
name: 'get_urgent_items',
|
|
320
|
+
description: 'Get all unresolved urgent items across the team. Optionally filter by owner or priority.',
|
|
321
|
+
inputSchema: {
|
|
322
|
+
type: 'object',
|
|
323
|
+
properties: {
|
|
324
|
+
owner: {
|
|
325
|
+
type: 'string',
|
|
326
|
+
description: 'Filter to one person (optional)',
|
|
327
|
+
},
|
|
328
|
+
priority: {
|
|
329
|
+
type: 'string',
|
|
330
|
+
enum: ['critical', 'high', 'medium', 'low'],
|
|
331
|
+
description: 'Filter to a specific priority level (optional)',
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
name: 'get_deal_priorities',
|
|
338
|
+
description: "Get Fatima's morning priority list: new unresponded leads, at-risk deals with 3+ days no activity, and follow-ups due today.",
|
|
339
|
+
inputSchema: {
|
|
340
|
+
type: 'object',
|
|
341
|
+
properties: {},
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
name: 'get_weekly_brief',
|
|
346
|
+
description: 'Get the latest compiled weekly brief showing summaries from all agents that ran this week.',
|
|
347
|
+
inputSchema: {
|
|
348
|
+
type: 'object',
|
|
349
|
+
properties: {},
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
name: 'run_agent',
|
|
354
|
+
description: 'Trigger a specific diagnostic agent to run now and return its output. Only use when fresh data is needed.',
|
|
355
|
+
inputSchema: {
|
|
356
|
+
type: 'object',
|
|
357
|
+
properties: {
|
|
358
|
+
agent_name: {
|
|
359
|
+
type: 'string',
|
|
360
|
+
enum: Object.keys(AGENT_ROUTES),
|
|
361
|
+
description: 'The agent to run',
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
required: ['agent_name'],
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
name: 'create_urgent_item',
|
|
369
|
+
description: 'Create a new urgent item visible on the team dashboard. Use when something needs immediate attention from a specific person.',
|
|
370
|
+
inputSchema: {
|
|
371
|
+
type: 'object',
|
|
372
|
+
properties: {
|
|
373
|
+
title: {
|
|
374
|
+
type: 'string',
|
|
375
|
+
description: 'Short title for the urgent item',
|
|
376
|
+
},
|
|
377
|
+
description: {
|
|
378
|
+
type: 'string',
|
|
379
|
+
description: 'Longer description (optional)',
|
|
380
|
+
},
|
|
381
|
+
owner: {
|
|
382
|
+
type: 'string',
|
|
383
|
+
description: 'Team member name who owns this item',
|
|
384
|
+
},
|
|
385
|
+
priority: {
|
|
386
|
+
type: 'string',
|
|
387
|
+
enum: ['critical', 'high', 'medium', 'low'],
|
|
388
|
+
description: 'Urgency level',
|
|
389
|
+
},
|
|
390
|
+
due_date: {
|
|
391
|
+
type: 'string',
|
|
392
|
+
description: 'ISO date string e.g. 2026-04-25 (optional)',
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
required: ['title', 'owner', 'priority'],
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
]
|
|
399
|
+
|
|
400
|
+
// ---------------------------------------------------------------------------
|
|
401
|
+
// Dispatch
|
|
402
|
+
// ---------------------------------------------------------------------------
|
|
403
|
+
|
|
404
|
+
async function dispatch(name, args) {
|
|
405
|
+
switch (name) {
|
|
406
|
+
case 'get_pipeline_health': return getPipelineHealth()
|
|
407
|
+
case 'get_funnel_data': return getFunnelData()
|
|
408
|
+
case 'get_kpi_status': return getKpiStatus(args)
|
|
409
|
+
case 'get_urgent_items': return getUrgentItems(args)
|
|
410
|
+
case 'get_deal_priorities': return getDealPriorities()
|
|
411
|
+
case 'get_weekly_brief': return getWeeklyBrief()
|
|
412
|
+
case 'run_agent': return runAgent(args)
|
|
413
|
+
case 'create_urgent_item': return createUrgentItem(args)
|
|
414
|
+
default: throw new Error(`Unknown tool: ${name}`)
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ---------------------------------------------------------------------------
|
|
419
|
+
// MCP Server
|
|
420
|
+
// ---------------------------------------------------------------------------
|
|
421
|
+
|
|
422
|
+
const server = new Server(
|
|
423
|
+
{ name: 'saleem-dashboard', version: '1.0.0' },
|
|
424
|
+
{ capabilities: { tools: {} } }
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }))
|
|
428
|
+
|
|
429
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
430
|
+
const { name, arguments: args } = request.params
|
|
431
|
+
try {
|
|
432
|
+
const result = await dispatch(name, args || {})
|
|
433
|
+
return {
|
|
434
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
435
|
+
}
|
|
436
|
+
} catch (err) {
|
|
437
|
+
return {
|
|
438
|
+
content: [{ type: 'text', text: `Error: ${err.message}` }],
|
|
439
|
+
isError: true,
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
const transport = new StdioServerTransport()
|
|
445
|
+
await server.connect(transport)
|
|
446
|
+
console.error('[mcp] Saleem Dashboard MCP server running')
|