notifyme-csm-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.
Files changed (2) hide show
  1. package/dist/index.js +541 -0
  2. package/package.json +41 -0
package/dist/index.js ADDED
@@ -0,0 +1,541 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * NotifyMe CSM — MCP Server
4
+ *
5
+ * Exposes CSM data and actions to LLMs via the Model Context Protocol (stdio).
6
+ *
7
+ * Configuration (env vars):
8
+ * CSM_API_TOKEN — personal bearer token from Settings → API Tokens
9
+ * CSM_BASE_URL — defaults to prod Cloud Run URL
10
+ *
11
+ * Tools (full feature set):
12
+ *
13
+ * Companies
14
+ * list_companies — browse/filter companies
15
+ * search_companies — search by name / domain / email
16
+ * get_company — full profile + recent notes/reminders/meetings/emails
17
+ * update_company — update priority / state / tags
18
+ *
19
+ * Per-company sub-resources
20
+ * list_notes — all notes for a company
21
+ * list_meetings — all meetings for a company
22
+ * list_emails — all sent emails for a company
23
+ * list_feature_requests — feature requests from a company
24
+ * get_activity — activity log for a company
25
+ *
26
+ * Meetings
27
+ * upcoming_meetings — upcoming calendar meetings
28
+ * log_meeting — record a meeting on a company
29
+ *
30
+ * Emails
31
+ * draft_email — AI-generate an email draft
32
+ * send_email — send email via Gmail and log it
33
+ *
34
+ * Feature Requests
35
+ * create_feature_request — log a feature request
36
+ *
37
+ * Reminders
38
+ * list_reminders — overdue + due-today follow-ups
39
+ * create_reminder — set a follow-up with due date
40
+ * complete_reminder — mark a reminder done
41
+ *
42
+ * Dashboard
43
+ * get_dashboard — at-risk merchants
44
+ * get_stats — full dashboard stats
45
+ *
46
+ * Kanban
47
+ * list_kanban_columns — list pipeline columns
48
+ * move_to_column — move a company to a pipeline column
49
+ */
50
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
51
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
52
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
53
+ const BASE_URL = process.env.CSM_BASE_URL ??
54
+ "https://notify-me-csm-44700630095.us-central1.run.app";
55
+ const TOKEN = process.env.CSM_API_TOKEN ?? "";
56
+ if (!TOKEN) {
57
+ console.error("[notifyme-mcp] CSM_API_TOKEN is not set. Exiting.");
58
+ process.exit(1);
59
+ }
60
+ // ── HTTP helper ────────────────────────────────────────────────────────────
61
+ async function api(method, path, body) {
62
+ const url = `${BASE_URL}/api/mcp${path}`;
63
+ const res = await fetch(url, {
64
+ method,
65
+ headers: {
66
+ Authorization: `Bearer ${TOKEN}`,
67
+ "Content-Type": "application/json",
68
+ },
69
+ body: body != null ? JSON.stringify(body) : undefined,
70
+ });
71
+ if (!res.ok) {
72
+ const text = await res.text().catch(() => res.statusText);
73
+ throw new Error(`CSM API error ${res.status}: ${text}`);
74
+ }
75
+ return res.json();
76
+ }
77
+ // ── Tool definitions ───────────────────────────────────────────────────────
78
+ const TOOLS = [
79
+ // ── Companies ────────────────────────────────────────────────────────────
80
+ {
81
+ name: "list_companies",
82
+ description: "Browse or filter all companies in the CSM. Supports optional filters for type and stage/state. Use search_companies for name/domain lookups.",
83
+ inputSchema: {
84
+ type: "object",
85
+ properties: {
86
+ type: {
87
+ type: "string",
88
+ enum: ["agency", "merchant", "app_partner", "technology_partner", "media", "investor", "other"],
89
+ description: "Filter by company type (optional)",
90
+ },
91
+ stage: { type: "string", description: "Filter by state/stage (optional, e.g. 'active', 'churned')" },
92
+ limit: { type: "number", description: "Max results (default 25, max 100)" },
93
+ },
94
+ required: [],
95
+ },
96
+ },
97
+ {
98
+ name: "search_companies",
99
+ description: "Search NotifyMe CSM companies (agencies, merchants, partners) by name, domain, or email. Always call this first to find the company id before calling get_company.",
100
+ inputSchema: {
101
+ type: "object",
102
+ properties: {
103
+ q: { type: "string", description: "Search query (name, domain, or email)" },
104
+ type: {
105
+ type: "string",
106
+ enum: ["agency", "merchant", "app_partner", "technology_partner", "media", "investor", "other"],
107
+ description: "Filter by company type (optional)",
108
+ },
109
+ limit: { type: "number", description: "Max results (default 10, max 50)" },
110
+ },
111
+ required: ["q"],
112
+ },
113
+ },
114
+ {
115
+ name: "get_company",
116
+ description: "Get the full profile for a company by its CSM id, including recent notes, open reminders, recent meetings, and recent emails. Use search_companies first to find the id.",
117
+ inputSchema: {
118
+ type: "object",
119
+ properties: {
120
+ id: { type: "number", description: "Company id (from search_companies or list_companies)" },
121
+ },
122
+ required: ["id"],
123
+ },
124
+ },
125
+ {
126
+ name: "update_company",
127
+ description: "Update a company's priority, state, or tags. All fields are optional — only pass what you want to change.",
128
+ inputSchema: {
129
+ type: "object",
130
+ properties: {
131
+ id: { type: "number", description: "Company id" },
132
+ priority: { type: "string", enum: ["p1", "p2", "p3"], description: "Priority tier" },
133
+ state: { type: "string", description: "State/stage (e.g. 'active', 'churned')" },
134
+ tags: { type: "array", items: { type: "string" }, description: "Tags array (replaces existing)" },
135
+ },
136
+ required: ["id"],
137
+ },
138
+ },
139
+ // ── Per-company sub-resources ─────────────────────────────────────────────
140
+ {
141
+ name: "list_notes",
142
+ description: "List all notes for a company. Use get_company for a quick snapshot; use this for deeper history.",
143
+ inputSchema: {
144
+ type: "object",
145
+ properties: {
146
+ companyId: { type: "number", description: "Company id" },
147
+ limit: { type: "number", description: "Max results (default 20, max 100)" },
148
+ },
149
+ required: ["companyId"],
150
+ },
151
+ },
152
+ {
153
+ name: "list_meetings",
154
+ description: "List meetings for a specific company, most recent first.",
155
+ inputSchema: {
156
+ type: "object",
157
+ properties: {
158
+ companyId: { type: "number", description: "Company id" },
159
+ limit: { type: "number", description: "Max results (default 10, max 50)" },
160
+ },
161
+ required: ["companyId"],
162
+ },
163
+ },
164
+ {
165
+ name: "list_emails",
166
+ description: "List emails sent to a company, most recent first.",
167
+ inputSchema: {
168
+ type: "object",
169
+ properties: {
170
+ companyId: { type: "number", description: "Company id" },
171
+ limit: { type: "number", description: "Max results (default 20, max 100)" },
172
+ },
173
+ required: ["companyId"],
174
+ },
175
+ },
176
+ {
177
+ name: "list_feature_requests",
178
+ description: "List feature requests submitted for a company.",
179
+ inputSchema: {
180
+ type: "object",
181
+ properties: {
182
+ companyId: { type: "number", description: "Company id" },
183
+ },
184
+ required: ["companyId"],
185
+ },
186
+ },
187
+ {
188
+ name: "get_activity",
189
+ description: "Get the activity log for a company (notes, meetings, emails, reminders).",
190
+ inputSchema: {
191
+ type: "object",
192
+ properties: {
193
+ companyId: { type: "number", description: "Company id" },
194
+ limit: { type: "number", description: "Max results (default 20, max 100)" },
195
+ },
196
+ required: ["companyId"],
197
+ },
198
+ },
199
+ // ── Notes ─────────────────────────────────────────────────────────────────
200
+ {
201
+ name: "add_note",
202
+ description: "Add a note to a company in the CSM. Use this after a meeting, call, or any interaction worth logging.",
203
+ inputSchema: {
204
+ type: "object",
205
+ properties: {
206
+ companyId: { type: "number", description: "Company id (from search_companies)" },
207
+ content: { type: "string", description: "Note content (plain text, up to 10,000 chars)" },
208
+ },
209
+ required: ["companyId", "content"],
210
+ },
211
+ },
212
+ // ── Meetings ──────────────────────────────────────────────────────────────
213
+ {
214
+ name: "upcoming_meetings",
215
+ description: "List upcoming calendar meetings (future scheduled meetings across all companies).",
216
+ inputSchema: {
217
+ type: "object",
218
+ properties: {
219
+ limit: { type: "number", description: "Max results (default 10, max 50)" },
220
+ },
221
+ required: [],
222
+ },
223
+ },
224
+ {
225
+ name: "log_meeting",
226
+ description: "Log a meeting on a company (past or future). Use this to record meetings that happened.",
227
+ inputSchema: {
228
+ type: "object",
229
+ properties: {
230
+ companyId: { type: "number", description: "Company id" },
231
+ title: { type: "string", description: "Meeting title/subject" },
232
+ scheduledAt: { type: "string", description: "ISO 8601 datetime e.g. 2026-06-01T14:00:00Z" },
233
+ summary: { type: "string", description: "Meeting summary or notes (optional)" },
234
+ duration: { type: "number", description: "Duration in minutes (optional)" },
235
+ },
236
+ required: ["companyId", "title", "scheduledAt"],
237
+ },
238
+ },
239
+ // ── Emails ────────────────────────────────────────────────────────────────
240
+ {
241
+ name: "draft_email",
242
+ description: "AI-generate a personalized email draft for a company. Returns subject + body. Use send_email to send it afterward.",
243
+ inputSchema: {
244
+ type: "object",
245
+ properties: {
246
+ companyId: { type: "number", description: "Company id" },
247
+ emailType: {
248
+ type: "string",
249
+ enum: ["follow_up", "onboarding", "roi_improvement", "expansion", "general"],
250
+ description: "Type of email to generate",
251
+ },
252
+ userPrompt: { type: "string", description: "Additional context or instructions for the AI (optional)" },
253
+ },
254
+ required: ["companyId", "emailType"],
255
+ },
256
+ },
257
+ {
258
+ name: "send_email",
259
+ description: "Send an email to a company via Gmail and log it in the CSM. Uses the company's contact email unless recipients is provided.",
260
+ inputSchema: {
261
+ type: "object",
262
+ properties: {
263
+ companyId: { type: "number", description: "Company id" },
264
+ subject: { type: "string", description: "Email subject" },
265
+ body: { type: "string", description: "Email body (plain text or HTML)" },
266
+ emailType: {
267
+ type: "string",
268
+ enum: ["follow_up", "onboarding", "roi_improvement", "expansion", "general"],
269
+ description: "Email category for logging (default: general)",
270
+ },
271
+ recipients: { type: "array", items: { type: "string" }, description: "Override recipient emails (optional)" },
272
+ },
273
+ required: ["companyId", "subject", "body"],
274
+ },
275
+ },
276
+ // ── Feature Requests ─────────────────────────────────────────────────────
277
+ {
278
+ name: "create_feature_request",
279
+ description: "Log a feature request from a company.",
280
+ inputSchema: {
281
+ type: "object",
282
+ properties: {
283
+ companyId: { type: "number", description: "Company id" },
284
+ title: { type: "string", description: "Short feature request title" },
285
+ description: { type: "string", description: "Detailed description (up to 5,000 chars)" },
286
+ priority: { type: "string", enum: ["low", "medium", "high"], description: "Priority (default: medium)" },
287
+ },
288
+ required: ["companyId", "title", "description"],
289
+ },
290
+ },
291
+ // ── Reminders ─────────────────────────────────────────────────────────────
292
+ {
293
+ name: "list_reminders",
294
+ description: "List the current user's overdue and due-today follow-up reminders. Useful for a morning briefing or checking what needs attention.",
295
+ inputSchema: {
296
+ type: "object",
297
+ properties: {},
298
+ required: [],
299
+ },
300
+ },
301
+ {
302
+ name: "create_reminder",
303
+ description: "Create a follow-up reminder for a company. The reminder will appear in the Priority Follow-ups widget on the dashboard.",
304
+ inputSchema: {
305
+ type: "object",
306
+ properties: {
307
+ companyId: { type: "number", description: "Company id" },
308
+ message: { type: "string", description: "Reminder message (what to do)" },
309
+ dueAt: { type: "string", description: "ISO 8601 datetime e.g. 2026-06-01T09:00:00Z" },
310
+ },
311
+ required: ["companyId", "message", "dueAt"],
312
+ },
313
+ },
314
+ {
315
+ name: "complete_reminder",
316
+ description: "Mark a reminder as completed. Use after you've handled a follow-up.",
317
+ inputSchema: {
318
+ type: "object",
319
+ properties: {
320
+ id: { type: "number", description: "Reminder id (from list_reminders)" },
321
+ },
322
+ required: ["id"],
323
+ },
324
+ },
325
+ // ── Dashboard ─────────────────────────────────────────────────────────────
326
+ {
327
+ name: "get_dashboard",
328
+ description: "Get at-risk merchants from the CSM dashboard — merchants whose activity or spend signals they may churn.",
329
+ inputSchema: {
330
+ type: "object",
331
+ properties: {},
332
+ required: [],
333
+ },
334
+ },
335
+ {
336
+ name: "get_stats",
337
+ description: "Get full dashboard stats: total companies, active merchants, at-risk count, open reminders, upcoming meetings, recent activity, and more.",
338
+ inputSchema: {
339
+ type: "object",
340
+ properties: {},
341
+ required: [],
342
+ },
343
+ },
344
+ // ── Kanban ────────────────────────────────────────────────────────────────
345
+ {
346
+ name: "list_kanban_columns",
347
+ description: "List all pipeline (Kanban) columns and their positions. Use move_to_column to move companies.",
348
+ inputSchema: {
349
+ type: "object",
350
+ properties: {},
351
+ required: [],
352
+ },
353
+ },
354
+ {
355
+ name: "move_to_column",
356
+ description: "Move a company to a specific pipeline (Kanban) column. Creates a card if none exists.",
357
+ inputSchema: {
358
+ type: "object",
359
+ properties: {
360
+ companyId: { type: "number", description: "Company id" },
361
+ columnId: { type: "number", description: "Target column id (from list_kanban_columns)" },
362
+ },
363
+ required: ["companyId", "columnId"],
364
+ },
365
+ },
366
+ ];
367
+ // ── Server setup ───────────────────────────────────────────────────────────
368
+ const server = new Server({ name: "notifyme-csm", version: "2.0.0" }, { capabilities: { tools: {} } });
369
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
370
+ tools: TOOLS.map((t) => ({
371
+ name: t.name,
372
+ description: t.description,
373
+ inputSchema: t.inputSchema,
374
+ })),
375
+ }));
376
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
377
+ const { name, arguments: args } = request.params;
378
+ try {
379
+ let data;
380
+ switch (name) {
381
+ // ── Companies ──────────────────────────────────────────────────────────
382
+ case "list_companies": {
383
+ const { type, stage, limit } = args;
384
+ const params = new URLSearchParams();
385
+ if (type)
386
+ params.set("type", type);
387
+ if (stage)
388
+ params.set("stage", stage);
389
+ if (limit)
390
+ params.set("limit", String(limit));
391
+ data = await api("GET", `/companies?${params}`);
392
+ break;
393
+ }
394
+ case "search_companies": {
395
+ const { q, type, limit } = args;
396
+ const params = new URLSearchParams({ q });
397
+ if (type)
398
+ params.set("type", type);
399
+ if (limit)
400
+ params.set("limit", String(limit));
401
+ data = await api("GET", `/companies/search?${params}`);
402
+ break;
403
+ }
404
+ case "get_company": {
405
+ const { id } = args;
406
+ data = await api("GET", `/companies/${id}`);
407
+ break;
408
+ }
409
+ case "update_company": {
410
+ const { id, ...updates } = args;
411
+ data = await api("PATCH", `/companies/${id}`, updates);
412
+ break;
413
+ }
414
+ // ── Per-company sub-resources ──────────────────────────────────────────
415
+ case "list_notes": {
416
+ const { companyId, limit } = args;
417
+ const params = new URLSearchParams();
418
+ if (limit)
419
+ params.set("limit", String(limit));
420
+ data = await api("GET", `/companies/${companyId}/notes?${params}`);
421
+ break;
422
+ }
423
+ case "list_meetings": {
424
+ const { companyId, limit } = args;
425
+ const params = new URLSearchParams();
426
+ if (limit)
427
+ params.set("limit", String(limit));
428
+ data = await api("GET", `/companies/${companyId}/meetings?${params}`);
429
+ break;
430
+ }
431
+ case "list_emails": {
432
+ const { companyId, limit } = args;
433
+ const params = new URLSearchParams();
434
+ if (limit)
435
+ params.set("limit", String(limit));
436
+ data = await api("GET", `/companies/${companyId}/emails?${params}`);
437
+ break;
438
+ }
439
+ case "list_feature_requests": {
440
+ const { companyId } = args;
441
+ data = await api("GET", `/companies/${companyId}/feature-requests`);
442
+ break;
443
+ }
444
+ case "get_activity": {
445
+ const { companyId, limit } = args;
446
+ const params = new URLSearchParams();
447
+ if (limit)
448
+ params.set("limit", String(limit));
449
+ data = await api("GET", `/companies/${companyId}/activity?${params}`);
450
+ break;
451
+ }
452
+ // ── Notes ──────────────────────────────────────────────────────────────
453
+ case "add_note": {
454
+ const { companyId, content } = args;
455
+ data = await api("POST", "/notes", { companyId, content });
456
+ break;
457
+ }
458
+ // ── Meetings ───────────────────────────────────────────────────────────
459
+ case "upcoming_meetings": {
460
+ const { limit } = args;
461
+ const params = new URLSearchParams();
462
+ if (limit)
463
+ params.set("limit", String(limit));
464
+ data = await api("GET", `/meetings/upcoming?${params}`);
465
+ break;
466
+ }
467
+ case "log_meeting": {
468
+ const { companyId, title, scheduledAt, summary, duration } = args;
469
+ data = await api("POST", "/meetings", { companyId, title, scheduledAt, summary, duration });
470
+ break;
471
+ }
472
+ // ── Emails ─────────────────────────────────────────────────────────────
473
+ case "draft_email": {
474
+ const { companyId, emailType, userPrompt } = args;
475
+ data = await api("POST", "/emails/draft", { companyId, emailType, userPrompt });
476
+ break;
477
+ }
478
+ case "send_email": {
479
+ const { companyId, subject, body, emailType, recipients } = args;
480
+ data = await api("POST", "/emails/send", { companyId, subject, body, emailType, recipients });
481
+ break;
482
+ }
483
+ // ── Feature Requests ───────────────────────────────────────────────────
484
+ case "create_feature_request": {
485
+ const { companyId, title, description, priority } = args;
486
+ data = await api("POST", "/feature-requests", { companyId, title, description, priority });
487
+ break;
488
+ }
489
+ // ── Reminders ──────────────────────────────────────────────────────────
490
+ case "list_reminders": {
491
+ data = await api("GET", "/reminders");
492
+ break;
493
+ }
494
+ case "create_reminder": {
495
+ const { companyId, message, dueAt } = args;
496
+ data = await api("POST", "/reminders", { companyId, message, dueAt });
497
+ break;
498
+ }
499
+ case "complete_reminder": {
500
+ const { id } = args;
501
+ data = await api("POST", `/reminders/${id}/complete`, {});
502
+ break;
503
+ }
504
+ // ── Dashboard ──────────────────────────────────────────────────────────
505
+ case "get_dashboard": {
506
+ data = await api("GET", "/dashboard");
507
+ break;
508
+ }
509
+ case "get_stats": {
510
+ data = await api("GET", "/stats");
511
+ break;
512
+ }
513
+ // ── Kanban ─────────────────────────────────────────────────────────────
514
+ case "list_kanban_columns": {
515
+ data = await api("GET", "/kanban/columns");
516
+ break;
517
+ }
518
+ case "move_to_column": {
519
+ const { companyId, columnId } = args;
520
+ data = await api("POST", "/kanban/move", { companyId, columnId });
521
+ break;
522
+ }
523
+ default:
524
+ throw new Error(`Unknown tool: ${name}`);
525
+ }
526
+ return {
527
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
528
+ };
529
+ }
530
+ catch (err) {
531
+ const message = err instanceof Error ? err.message : String(err);
532
+ return {
533
+ content: [{ type: "text", text: `Error: ${message}` }],
534
+ isError: true,
535
+ };
536
+ }
537
+ });
538
+ // ── Start ──────────────────────────────────────────────────────────────────
539
+ const transport = new StdioServerTransport();
540
+ await server.connect(transport);
541
+ console.error("[notifyme-mcp] MCP server running on stdio");
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "notifyme-csm-mcp",
3
+ "version": "1.0.0",
4
+ "description": "NotifyMe CSM — MCP server for LLM integrations (Claude Code, Cursor, etc.)",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "notifyme-csm-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "dev": "tsx src/index.ts",
19
+ "start": "node dist/index.js",
20
+ "prepublishOnly": "npm run build"
21
+ },
22
+ "dependencies": {
23
+ "@modelcontextprotocol/sdk": "^1.29.0",
24
+ "zod": "^3.25.76"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^22.19.19",
28
+ "tsx": "^4.22.3",
29
+ "typescript": "^5.9.3"
30
+ },
31
+ "engines": {
32
+ "node": ">=18"
33
+ },
34
+ "keywords": [
35
+ "mcp",
36
+ "claude",
37
+ "notifyme",
38
+ "csm"
39
+ ],
40
+ "license": "UNLICENSED"
41
+ }