@hasna/microservices 0.0.9 → 0.0.10

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.
@@ -0,0 +1,533 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { z } from "zod";
6
+ import {
7
+ createLead,
8
+ getLead,
9
+ listLeads,
10
+ updateLead,
11
+ deleteLead,
12
+ searchLeads,
13
+ bulkImportLeads,
14
+ exportLeads,
15
+ addActivity,
16
+ getActivities,
17
+ getLeadTimeline,
18
+ getLeadStats,
19
+ getPipeline,
20
+ deduplicateLeads,
21
+ mergeLeads,
22
+ } from "../db/leads.js";
23
+ import {
24
+ createList,
25
+ listLists,
26
+ getListMembers,
27
+ getSmartListMembers,
28
+ addToList,
29
+ removeFromList,
30
+ deleteList,
31
+ } from "../db/lists.js";
32
+ import { enrichLead, bulkEnrich } from "../lib/enrichment.js";
33
+ import { scoreLead, autoScoreAll, getScoreDistribution } from "../lib/scoring.js";
34
+
35
+ const server = new McpServer({
36
+ name: "microservice-leads",
37
+ version: "0.0.1",
38
+ });
39
+
40
+ // --- Lead CRUD ---
41
+
42
+ server.registerTool(
43
+ "create_lead",
44
+ {
45
+ title: "Create Lead",
46
+ description: "Create a new lead.",
47
+ inputSchema: {
48
+ name: z.string().optional(),
49
+ email: z.string().optional(),
50
+ phone: z.string().optional(),
51
+ company: z.string().optional(),
52
+ title: z.string().optional(),
53
+ website: z.string().optional(),
54
+ linkedin_url: z.string().optional(),
55
+ source: z.string().optional(),
56
+ tags: z.array(z.string()).optional(),
57
+ notes: z.string().optional(),
58
+ },
59
+ },
60
+ async (params) => {
61
+ const lead = createLead(params);
62
+ return { content: [{ type: "text", text: JSON.stringify(lead, null, 2) }] };
63
+ }
64
+ );
65
+
66
+ server.registerTool(
67
+ "get_lead",
68
+ {
69
+ title: "Get Lead",
70
+ description: "Get a lead by ID.",
71
+ inputSchema: { id: z.string() },
72
+ },
73
+ async ({ id }) => {
74
+ const lead = getLead(id);
75
+ if (!lead) {
76
+ return { content: [{ type: "text", text: `Lead '${id}' not found.` }], isError: true };
77
+ }
78
+ return { content: [{ type: "text", text: JSON.stringify(lead, null, 2) }] };
79
+ }
80
+ );
81
+
82
+ server.registerTool(
83
+ "list_leads",
84
+ {
85
+ title: "List Leads",
86
+ description: "List leads with optional filters.",
87
+ inputSchema: {
88
+ status: z.string().optional(),
89
+ source: z.string().optional(),
90
+ score_min: z.number().optional(),
91
+ score_max: z.number().optional(),
92
+ enriched: z.boolean().optional(),
93
+ limit: z.number().optional(),
94
+ offset: z.number().optional(),
95
+ },
96
+ },
97
+ async (params) => {
98
+ const leads = listLeads(params);
99
+ return {
100
+ content: [
101
+ { type: "text", text: JSON.stringify({ leads, count: leads.length }, null, 2) },
102
+ ],
103
+ };
104
+ }
105
+ );
106
+
107
+ server.registerTool(
108
+ "update_lead",
109
+ {
110
+ title: "Update Lead",
111
+ description: "Update an existing lead.",
112
+ inputSchema: {
113
+ id: z.string(),
114
+ name: z.string().optional(),
115
+ email: z.string().optional(),
116
+ phone: z.string().optional(),
117
+ company: z.string().optional(),
118
+ title: z.string().optional(),
119
+ website: z.string().optional(),
120
+ linkedin_url: z.string().optional(),
121
+ source: z.string().optional(),
122
+ status: z.string().optional(),
123
+ tags: z.array(z.string()).optional(),
124
+ notes: z.string().optional(),
125
+ },
126
+ },
127
+ async ({ id, ...input }) => {
128
+ const lead = updateLead(id, input);
129
+ if (!lead) {
130
+ return { content: [{ type: "text", text: `Lead '${id}' not found.` }], isError: true };
131
+ }
132
+ return { content: [{ type: "text", text: JSON.stringify(lead, null, 2) }] };
133
+ }
134
+ );
135
+
136
+ server.registerTool(
137
+ "delete_lead",
138
+ {
139
+ title: "Delete Lead",
140
+ description: "Delete a lead by ID.",
141
+ inputSchema: { id: z.string() },
142
+ },
143
+ async ({ id }) => {
144
+ const deleted = deleteLead(id);
145
+ return { content: [{ type: "text", text: JSON.stringify({ id, deleted }) }] };
146
+ }
147
+ );
148
+
149
+ server.registerTool(
150
+ "search_leads",
151
+ {
152
+ title: "Search Leads",
153
+ description: "Search leads by name, email, or company.",
154
+ inputSchema: { query: z.string() },
155
+ },
156
+ async ({ query }) => {
157
+ const results = searchLeads(query);
158
+ return {
159
+ content: [
160
+ { type: "text", text: JSON.stringify({ results, count: results.length }, null, 2) },
161
+ ],
162
+ };
163
+ }
164
+ );
165
+
166
+ // --- Import/Export ---
167
+
168
+ server.registerTool(
169
+ "bulk_import_leads",
170
+ {
171
+ title: "Bulk Import Leads",
172
+ description: "Import multiple leads at once with deduplication by email.",
173
+ inputSchema: {
174
+ leads: z.array(
175
+ z.object({
176
+ name: z.string().optional(),
177
+ email: z.string().optional(),
178
+ phone: z.string().optional(),
179
+ company: z.string().optional(),
180
+ title: z.string().optional(),
181
+ website: z.string().optional(),
182
+ linkedin_url: z.string().optional(),
183
+ source: z.string().optional(),
184
+ })
185
+ ),
186
+ },
187
+ },
188
+ async ({ leads }) => {
189
+ const result = bulkImportLeads(leads);
190
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
191
+ }
192
+ );
193
+
194
+ server.registerTool(
195
+ "export_leads",
196
+ {
197
+ title: "Export Leads",
198
+ description: "Export leads in CSV or JSON format.",
199
+ inputSchema: {
200
+ format: z.enum(["csv", "json"]).optional(),
201
+ status: z.string().optional(),
202
+ },
203
+ },
204
+ async ({ format, status }) => {
205
+ const output = exportLeads(format || "json", status ? { status } : undefined);
206
+ return { content: [{ type: "text", text: output }] };
207
+ }
208
+ );
209
+
210
+ // --- Enrichment ---
211
+
212
+ server.registerTool(
213
+ "enrich_lead",
214
+ {
215
+ title: "Enrich Lead",
216
+ description: "Enrich a lead with data from email/domain research.",
217
+ inputSchema: { id: z.string() },
218
+ },
219
+ async ({ id }) => {
220
+ const lead = enrichLead(id);
221
+ if (!lead) {
222
+ return { content: [{ type: "text", text: `Lead '${id}' not found.` }], isError: true };
223
+ }
224
+ return { content: [{ type: "text", text: JSON.stringify(lead, null, 2) }] };
225
+ }
226
+ );
227
+
228
+ server.registerTool(
229
+ "bulk_enrich_leads",
230
+ {
231
+ title: "Bulk Enrich Leads",
232
+ description: "Enrich multiple leads by their IDs.",
233
+ inputSchema: { lead_ids: z.array(z.string()) },
234
+ },
235
+ async ({ lead_ids }) => {
236
+ const result = bulkEnrich(lead_ids);
237
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
238
+ }
239
+ );
240
+
241
+ // --- Scoring ---
242
+
243
+ server.registerTool(
244
+ "score_lead",
245
+ {
246
+ title: "Score Lead",
247
+ description: "Score a lead (0-100) based on data completeness and engagement.",
248
+ inputSchema: { id: z.string() },
249
+ },
250
+ async ({ id }) => {
251
+ const result = scoreLead(id);
252
+ if (!result) {
253
+ return { content: [{ type: "text", text: `Lead '${id}' not found.` }], isError: true };
254
+ }
255
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
256
+ }
257
+ );
258
+
259
+ server.registerTool(
260
+ "auto_score_all",
261
+ {
262
+ title: "Auto Score All",
263
+ description: "Auto-score all leads with score=0.",
264
+ inputSchema: {},
265
+ },
266
+ async () => {
267
+ const result = autoScoreAll();
268
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
269
+ }
270
+ );
271
+
272
+ server.registerTool(
273
+ "get_score_distribution",
274
+ {
275
+ title: "Score Distribution",
276
+ description: "Get the distribution of lead scores across ranges.",
277
+ inputSchema: {},
278
+ },
279
+ async () => {
280
+ const distribution = getScoreDistribution();
281
+ return { content: [{ type: "text", text: JSON.stringify(distribution, null, 2) }] };
282
+ }
283
+ );
284
+
285
+ // --- Pipeline & Stats ---
286
+
287
+ server.registerTool(
288
+ "get_lead_stats",
289
+ {
290
+ title: "Lead Stats",
291
+ description: "Get lead statistics including totals, by status, by source, avg score, and conversion rate.",
292
+ inputSchema: {},
293
+ },
294
+ async () => {
295
+ const stats = getLeadStats();
296
+ return { content: [{ type: "text", text: JSON.stringify(stats, null, 2) }] };
297
+ }
298
+ );
299
+
300
+ server.registerTool(
301
+ "get_pipeline",
302
+ {
303
+ title: "Lead Pipeline",
304
+ description: "Get the lead pipeline funnel view.",
305
+ inputSchema: {},
306
+ },
307
+ async () => {
308
+ const pipeline = getPipeline();
309
+ return { content: [{ type: "text", text: JSON.stringify(pipeline, null, 2) }] };
310
+ }
311
+ );
312
+
313
+ // --- Activities ---
314
+
315
+ server.registerTool(
316
+ "add_activity",
317
+ {
318
+ title: "Add Activity",
319
+ description: "Add an activity to a lead.",
320
+ inputSchema: {
321
+ lead_id: z.string(),
322
+ type: z.enum(["email_sent", "email_opened", "call", "meeting", "note", "status_change", "score_change", "enriched"]),
323
+ description: z.string().optional(),
324
+ },
325
+ },
326
+ async ({ lead_id, type, description }) => {
327
+ const activity = addActivity(lead_id, type, description);
328
+ return { content: [{ type: "text", text: JSON.stringify(activity, null, 2) }] };
329
+ }
330
+ );
331
+
332
+ server.registerTool(
333
+ "get_activities",
334
+ {
335
+ title: "Get Activities",
336
+ description: "Get activities for a lead.",
337
+ inputSchema: {
338
+ lead_id: z.string(),
339
+ limit: z.number().optional(),
340
+ },
341
+ },
342
+ async ({ lead_id, limit }) => {
343
+ const activities = limit ? getActivities(lead_id, limit) : getActivities(lead_id);
344
+ return {
345
+ content: [
346
+ { type: "text", text: JSON.stringify({ activities, count: activities.length }, null, 2) },
347
+ ],
348
+ };
349
+ }
350
+ );
351
+
352
+ server.registerTool(
353
+ "get_lead_timeline",
354
+ {
355
+ title: "Lead Timeline",
356
+ description: "Get full activity timeline for a lead.",
357
+ inputSchema: { lead_id: z.string() },
358
+ },
359
+ async ({ lead_id }) => {
360
+ const timeline = getLeadTimeline(lead_id);
361
+ return {
362
+ content: [
363
+ { type: "text", text: JSON.stringify({ timeline, count: timeline.length }, null, 2) },
364
+ ],
365
+ };
366
+ }
367
+ );
368
+
369
+ // --- Dedup & Merge ---
370
+
371
+ server.registerTool(
372
+ "deduplicate_leads",
373
+ {
374
+ title: "Deduplicate Leads",
375
+ description: "Find duplicate leads by email.",
376
+ inputSchema: {},
377
+ },
378
+ async () => {
379
+ const pairs = deduplicateLeads();
380
+ return { content: [{ type: "text", text: JSON.stringify({ pairs, count: pairs.length }, null, 2) }] };
381
+ }
382
+ );
383
+
384
+ server.registerTool(
385
+ "merge_leads",
386
+ {
387
+ title: "Merge Leads",
388
+ description: "Merge two leads — keep the first, merge data from the second, delete the second.",
389
+ inputSchema: {
390
+ keep_id: z.string(),
391
+ merge_id: z.string(),
392
+ },
393
+ },
394
+ async ({ keep_id, merge_id }) => {
395
+ const result = mergeLeads(keep_id, merge_id);
396
+ if (!result) {
397
+ return { content: [{ type: "text", text: "One or both leads not found." }], isError: true };
398
+ }
399
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
400
+ }
401
+ );
402
+
403
+ // --- Convert ---
404
+
405
+ server.registerTool(
406
+ "convert_lead",
407
+ {
408
+ title: "Convert Lead",
409
+ description: "Mark a lead as converted.",
410
+ inputSchema: { id: z.string() },
411
+ },
412
+ async ({ id }) => {
413
+ const lead = updateLead(id, { status: "converted" });
414
+ if (!lead) {
415
+ return { content: [{ type: "text", text: `Lead '${id}' not found.` }], isError: true };
416
+ }
417
+ addActivity(id, "status_change", "Lead converted");
418
+ return { content: [{ type: "text", text: JSON.stringify(lead, null, 2) }] };
419
+ }
420
+ );
421
+
422
+ // --- Lists ---
423
+
424
+ server.registerTool(
425
+ "create_lead_list",
426
+ {
427
+ title: "Create Lead List",
428
+ description: "Create a lead list.",
429
+ inputSchema: {
430
+ name: z.string(),
431
+ description: z.string().optional(),
432
+ filter_query: z.string().optional(),
433
+ },
434
+ },
435
+ async (params) => {
436
+ const list = createList(params);
437
+ return { content: [{ type: "text", text: JSON.stringify(list, null, 2) }] };
438
+ }
439
+ );
440
+
441
+ server.registerTool(
442
+ "list_lead_lists",
443
+ {
444
+ title: "List Lead Lists",
445
+ description: "List all lead lists.",
446
+ inputSchema: {},
447
+ },
448
+ async () => {
449
+ const lists = listLists();
450
+ return {
451
+ content: [
452
+ { type: "text", text: JSON.stringify({ lists, count: lists.length }, null, 2) },
453
+ ],
454
+ };
455
+ }
456
+ );
457
+
458
+ server.registerTool(
459
+ "get_list_members",
460
+ {
461
+ title: "Get List Members",
462
+ description: "Get members of a lead list. For smart lists with filter_query, returns dynamically matched leads.",
463
+ inputSchema: {
464
+ list_id: z.string(),
465
+ smart: z.boolean().optional(),
466
+ },
467
+ },
468
+ async ({ list_id, smart }) => {
469
+ const members = smart ? getSmartListMembers(list_id) : getListMembers(list_id);
470
+ return {
471
+ content: [
472
+ { type: "text", text: JSON.stringify({ members, count: members.length }, null, 2) },
473
+ ],
474
+ };
475
+ }
476
+ );
477
+
478
+ server.registerTool(
479
+ "add_to_list",
480
+ {
481
+ title: "Add to List",
482
+ description: "Add a lead to a list.",
483
+ inputSchema: {
484
+ list_id: z.string(),
485
+ lead_id: z.string(),
486
+ },
487
+ },
488
+ async ({ list_id, lead_id }) => {
489
+ const added = addToList(list_id, lead_id);
490
+ return { content: [{ type: "text", text: JSON.stringify({ list_id, lead_id, added }) }] };
491
+ }
492
+ );
493
+
494
+ server.registerTool(
495
+ "remove_from_list",
496
+ {
497
+ title: "Remove from List",
498
+ description: "Remove a lead from a list.",
499
+ inputSchema: {
500
+ list_id: z.string(),
501
+ lead_id: z.string(),
502
+ },
503
+ },
504
+ async ({ list_id, lead_id }) => {
505
+ const removed = removeFromList(list_id, lead_id);
506
+ return { content: [{ type: "text", text: JSON.stringify({ list_id, lead_id, removed }) }] };
507
+ }
508
+ );
509
+
510
+ server.registerTool(
511
+ "delete_lead_list",
512
+ {
513
+ title: "Delete Lead List",
514
+ description: "Delete a lead list.",
515
+ inputSchema: { id: z.string() },
516
+ },
517
+ async ({ id }) => {
518
+ const deleted = deleteList(id);
519
+ return { content: [{ type: "text", text: JSON.stringify({ id, deleted }) }] };
520
+ }
521
+ );
522
+
523
+ // --- Start ---
524
+ async function main() {
525
+ const transport = new StdioServerTransport();
526
+ await server.connect(transport);
527
+ console.error("microservice-leads MCP server running on stdio");
528
+ }
529
+
530
+ main().catch((error) => {
531
+ console.error("Fatal error:", error);
532
+ process.exit(1);
533
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/microservices",
3
- "version": "0.0.9",
3
+ "version": "0.0.10",
4
4
  "description": "Mini business apps for AI agents - invoices, contacts, bookkeeping and more, each with its own SQLite database",
5
5
  "type": "module",
6
6
  "bin": {