linkedin-toolkit-mcp 2.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 (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -0
  3. package/dist/bridge.d.ts +95 -0
  4. package/dist/bridge.js +259 -0
  5. package/dist/bridge.js.map +1 -0
  6. package/dist/cli.d.ts +83 -0
  7. package/dist/cli.js +898 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/config.d.ts +47 -0
  10. package/dist/config.js +149 -0
  11. package/dist/config.js.map +1 -0
  12. package/dist/contract.d.ts +6770 -0
  13. package/dist/contract.js +926 -0
  14. package/dist/contract.js.map +1 -0
  15. package/dist/db.d.ts +64 -0
  16. package/dist/db.js +542 -0
  17. package/dist/db.js.map +1 -0
  18. package/dist/fake-data.d.ts +277 -0
  19. package/dist/fake-data.js +789 -0
  20. package/dist/fake-data.js.map +1 -0
  21. package/dist/fake-extension.d.ts +51 -0
  22. package/dist/fake-extension.js +151 -0
  23. package/dist/fake-extension.js.map +1 -0
  24. package/dist/gen.d.ts +5 -0
  25. package/dist/gen.js +22 -0
  26. package/dist/gen.js.map +1 -0
  27. package/dist/http.d.ts +27 -0
  28. package/dist/http.js +329 -0
  29. package/dist/http.js.map +1 -0
  30. package/dist/openapi.d.ts +30 -0
  31. package/dist/openapi.js +306 -0
  32. package/dist/openapi.js.map +1 -0
  33. package/dist/prompts.d.ts +9 -0
  34. package/dist/prompts.js +65 -0
  35. package/dist/prompts.js.map +1 -0
  36. package/dist/resources.d.ts +7 -0
  37. package/dist/resources.js +73 -0
  38. package/dist/resources.js.map +1 -0
  39. package/dist/server.d.ts +3 -0
  40. package/dist/server.js +38 -0
  41. package/dist/server.js.map +1 -0
  42. package/dist/toolkit.d.ts +82 -0
  43. package/dist/toolkit.js +171 -0
  44. package/dist/toolkit.js.map +1 -0
  45. package/dist/tools.d.ts +31 -0
  46. package/dist/tools.js +103 -0
  47. package/dist/tools.js.map +1 -0
  48. package/dist/webhooks.d.ts +52 -0
  49. package/dist/webhooks.js +112 -0
  50. package/dist/webhooks.js.map +1 -0
  51. package/openapi.json +8334 -0
  52. package/package.json +45 -0
  53. package/tools.json +1205 -0
@@ -0,0 +1,926 @@
1
+ /**
2
+ * The contract: a zod mirror of `docs/actions.md`.
3
+ *
4
+ * Everything else in this package is generated from or validated against this file:
5
+ * the MCP tool list, the OpenAPI document, the `/actions/{action}` router and the CLI.
6
+ * Action names, error codes, event names and shapes must match `docs/actions.md` exactly.
7
+ */
8
+ import { z } from 'zod';
9
+ /* ------------------------------------------------------------------ *
10
+ * Shared types
11
+ * ------------------------------------------------------------------ */
12
+ export const ExperienceSchema = z.object({
13
+ title: z.string(),
14
+ company: z.string(),
15
+ start: z.string().optional(),
16
+ end: z.string().optional(),
17
+ description: z.string().optional(),
18
+ });
19
+ export const EducationSchema = z.object({
20
+ school: z.string(),
21
+ degree: z.string().optional(),
22
+ field: z.string().optional(),
23
+ start: z.string().optional(),
24
+ end: z.string().optional(),
25
+ });
26
+ export const ProfileSchema = z
27
+ .object({
28
+ publicId: z.string(),
29
+ urn: z.string().optional(),
30
+ url: z.string(),
31
+ firstName: z.string(),
32
+ lastName: z.string(),
33
+ fullName: z.string(),
34
+ headline: z.string().optional(),
35
+ title: z.string().optional(),
36
+ company: z.string().optional(),
37
+ companyUrn: z.string().optional(),
38
+ location: z.string().optional(),
39
+ industry: z.string().optional(),
40
+ photoUrl: z.string().optional(),
41
+ photoDataUrl: z.string().optional(),
42
+ pageText: z.string().optional(),
43
+ skills: z.array(z.string()).optional(),
44
+ connectionDegree: z.union([z.literal(1), z.literal(2), z.literal(3)]).optional(),
45
+ experience: z.array(ExperienceSchema).optional(),
46
+ education: z.array(EducationSchema).optional(),
47
+ capturedAt: z.number(),
48
+ source: z.string().optional(),
49
+ })
50
+ .passthrough();
51
+ export const CompanySchema = z
52
+ .object({
53
+ universalName: z.string(),
54
+ urn: z.string().optional(),
55
+ name: z.string(),
56
+ url: z.string(),
57
+ industry: z.string().optional(),
58
+ size: z.string().optional(),
59
+ hq: z.string().optional(),
60
+ website: z.string().optional(),
61
+ description: z.string().optional(),
62
+ followerCount: z.number().optional(),
63
+ capturedAt: z.number(),
64
+ })
65
+ .passthrough();
66
+ export const EngagerSchema = ProfileSchema.extend({
67
+ reaction: z.string().optional(),
68
+ commentText: z.string().optional(),
69
+ engagedAt: z.number().optional(),
70
+ });
71
+ export const ThreadSchema = z
72
+ .object({
73
+ threadId: z.string(),
74
+ participants: z.array(z.object({ publicId: z.string(), fullName: z.string() })),
75
+ lastMessageAt: z.number(),
76
+ unread: z.boolean(),
77
+ snippet: z.string(),
78
+ sentiment: z.enum(['positive', 'neutral', 'negative']).optional(),
79
+ })
80
+ .passthrough();
81
+ export const MessageSchema = z
82
+ .object({
83
+ messageId: z.string(),
84
+ threadId: z.string(),
85
+ fromPublicId: z.string(),
86
+ body: z.string(),
87
+ sentAt: z.number(),
88
+ })
89
+ .passthrough();
90
+ export const ListSchema = z
91
+ .object({
92
+ listId: z.string(),
93
+ name: z.string(),
94
+ tags: z.array(z.string()),
95
+ createdAt: z.number(),
96
+ count: z.number(),
97
+ })
98
+ .passthrough();
99
+ export const ListMemberSchema = z
100
+ .object({
101
+ publicId: z.string(),
102
+ profile: ProfileSchema,
103
+ addedAt: z.number(),
104
+ tags: z.array(z.string()),
105
+ contactedBefore: z.boolean(),
106
+ signals: z.array(z.string()).optional(),
107
+ })
108
+ .passthrough();
109
+ export const STEP_TYPES = [
110
+ 'view',
111
+ 'follow',
112
+ 'invite',
113
+ 'message',
114
+ 'inmail',
115
+ 'like',
116
+ 'comment',
117
+ 'wait',
118
+ 'branch',
119
+ ];
120
+ export const StepSchema = z.lazy(() => z
121
+ .object({
122
+ type: z.enum(STEP_TYPES),
123
+ note: z.string().optional(),
124
+ body: z.string().optional(),
125
+ subject: z.string().optional(),
126
+ variants: z.array(z.string()).optional(),
127
+ waitMs: z.number().optional(),
128
+ branch: z
129
+ .object({
130
+ on: z.enum(['accepted', 'replied', 'notAcceptedAfterMs']),
131
+ ms: z.number().optional(),
132
+ then: z.array(StepSchema),
133
+ else: z.array(StepSchema),
134
+ })
135
+ .optional(),
136
+ })
137
+ .passthrough());
138
+ export const CampaignSchema = z
139
+ .object({
140
+ campaignId: z.string(),
141
+ name: z.string(),
142
+ steps: z.array(StepSchema),
143
+ status: z.enum(['active', 'paused', 'completed']),
144
+ createdAt: z.number(),
145
+ settings: z.object({ stopOnReply: z.boolean(), autopilot: z.boolean() }).passthrough(),
146
+ stats: z
147
+ .object({
148
+ enrolled: z.number(),
149
+ sent: z.number(),
150
+ accepted: z.number(),
151
+ replied: z.number(),
152
+ positive: z.number(),
153
+ byStep: z.record(z.string(), z.record(z.string(), z.number())),
154
+ })
155
+ .passthrough()
156
+ .optional(),
157
+ })
158
+ .passthrough();
159
+ export const QUEUE_ACTIONS = [
160
+ 'outreach.invite',
161
+ 'outreach.message',
162
+ 'outreach.inmail',
163
+ 'outreach.comment',
164
+ ];
165
+ export const QueueItemSchema = z
166
+ .object({
167
+ id: z.string(),
168
+ action: z.enum(QUEUE_ACTIONS),
169
+ params: z.record(z.string(), z.unknown()),
170
+ origin: z.enum(['popup', 'campaign', 'mcp', 'cli']),
171
+ profile: ProfileSchema.optional(),
172
+ createdAt: z.number(),
173
+ status: z.enum(['pending', 'approved', 'rejected', 'sent', 'failed']),
174
+ result: z.record(z.string(), z.unknown()).optional(),
175
+ })
176
+ .passthrough();
177
+ export const ResearchRowSchema = z
178
+ .object({
179
+ name: z.string().optional(),
180
+ linkedinUrl: z.string().optional(),
181
+ email: z.string().optional(),
182
+ domain: z.string().optional(),
183
+ company: z.string().optional(),
184
+ })
185
+ .passthrough();
186
+ export const ResolvedRowSchema = z
187
+ .object({
188
+ row: ResearchRowSchema,
189
+ kind: z.enum(['person', 'company', 'unresolved']),
190
+ publicId: z.string().optional(),
191
+ universalName: z.string().optional(),
192
+ confidence: z.number(),
193
+ candidates: z.array(ProfileSchema).optional(),
194
+ })
195
+ .passthrough();
196
+ export const PackSchema = z
197
+ .object({
198
+ row: ResearchRowSchema,
199
+ resolved: ResolvedRowSchema,
200
+ profile: ProfileSchema.optional(),
201
+ company: CompanySchema.optional(),
202
+ recentPosts: z
203
+ .array(z
204
+ .object({
205
+ url: z.string(),
206
+ text: z.string(),
207
+ likes: z.number().optional(),
208
+ comments: z.number().optional(),
209
+ postedAt: z.number().optional(),
210
+ })
211
+ .passthrough())
212
+ .optional(),
213
+ mutualConnections: z.number().optional(),
214
+ connectionStatus: z.enum(['connected', 'pending', 'none']).optional(),
215
+ signals: z.array(z.string()),
216
+ enrichment: z
217
+ .object({
218
+ email: z.string().optional(),
219
+ phone: z.string().optional(),
220
+ provider: z.string().optional(),
221
+ })
222
+ .passthrough()
223
+ .optional(),
224
+ markdown: z.string(),
225
+ csvRow: z.record(z.string(), z.string()),
226
+ })
227
+ .passthrough();
228
+ export const WriteResultSchema = z
229
+ .object({
230
+ status: z.enum(['sent', 'queued', 'dryRun']),
231
+ queueId: z.string().optional(),
232
+ wouldSend: z.record(z.string(), z.unknown()).optional(),
233
+ sentAt: z.number().optional(),
234
+ })
235
+ .passthrough();
236
+ export const RateLimitSchema = z
237
+ .object({
238
+ hourlyUsed: z.number(),
239
+ hourlyCap: z.number(),
240
+ dailyUsed: z.number(),
241
+ dailyCap: z.number(),
242
+ nextAllowedAt: z.number(),
243
+ })
244
+ .passthrough();
245
+ export const QUOTA_KINDS = ['invite', 'message', 'visit', 'search'];
246
+ /** One word per endpoint, from `status.get { verify: true }`. */
247
+ export const ENDPOINT_RESULTS = ['ok', 'failed', 'unverified', 'skipped'];
248
+ export const EndpointReportSchema = z.record(z.string(), z.enum(ENDPOINT_RESULTS));
249
+ export const StatusSchema = z
250
+ .object({
251
+ connected: z.literal(true),
252
+ extensionVersion: z.string(),
253
+ loggedIn: z.boolean(),
254
+ autopilot: z.boolean(),
255
+ businessHours: z.boolean(),
256
+ backoffUntil: z.number().optional(),
257
+ challenge: z.object({ detectedAt: z.number() }).optional(),
258
+ quotas: z.record(z.enum(QUOTA_KINDS), RateLimitSchema),
259
+ queue: z.object({ pending: z.number() }).passthrough(),
260
+ campaigns: z.object({ active: z.number(), paused: z.number() }).passthrough(),
261
+ // Present only when the call asked to verify.
262
+ endpoints: EndpointReportSchema.optional(),
263
+ clientVersionCaptured: z.string().optional(),
264
+ endpointsCapturedAt: z.string().optional(),
265
+ endpointErrors: z.record(z.string(), z.string()).optional(),
266
+ })
267
+ .passthrough();
268
+ export const AI_PROVIDERS = [
269
+ 'none',
270
+ 'anthropic',
271
+ 'openai',
272
+ 'gemini',
273
+ 'ollama',
274
+ 'openai-compatible',
275
+ ];
276
+ export const ACCOUNT_PRESETS = ['free', 'premium', 'salesnav', 'recruiter'];
277
+ export const ConfigSchema = z
278
+ .object({
279
+ minDelayMs: z.number(),
280
+ maxDelayMs: z.number(),
281
+ hourlyCap: z.number(),
282
+ dailyInviteCap: z.number(),
283
+ dailyMessageCap: z.number(),
284
+ dailyVisitCap: z.number(),
285
+ dailySearchCap: z.number(),
286
+ businessHoursOnly: z.boolean(),
287
+ businessStart: z.number(),
288
+ businessEnd: z.number(),
289
+ weekdaysOnly: z.boolean(),
290
+ autopilot: z.boolean(),
291
+ accountPreset: z.enum(ACCOUNT_PRESETS),
292
+ warmup: z
293
+ .object({
294
+ enabled: z.boolean(),
295
+ startedAt: z.number().optional(),
296
+ days: z.number(),
297
+ })
298
+ .passthrough(),
299
+ ai: z
300
+ .object({
301
+ provider: z.enum(AI_PROVIDERS),
302
+ model: z.string().optional(),
303
+ baseUrl: z.string().optional(),
304
+ apiKey: z.string().optional(),
305
+ })
306
+ .passthrough(),
307
+ bridge: z
308
+ .object({ enabled: z.boolean(), port: z.number(), token: z.string().optional() })
309
+ .passthrough(),
310
+ webhookUrl: z.string().optional(),
311
+ })
312
+ .passthrough();
313
+ /** `config.set` takes a partial Config; hard ceilings are clamped by the extension. */
314
+ export const PartialConfigSchema = ConfigSchema.partial();
315
+ /* ------------------------------------------------------------------ *
316
+ * Error codes and events
317
+ * ------------------------------------------------------------------ */
318
+ export const ERROR_CODES = [
319
+ 'EXTENSION_OFFLINE',
320
+ 'NOT_LOGGED_IN',
321
+ 'RATE_LIMITED',
322
+ 'CHALLENGE_DETECTED',
323
+ 'QUOTA_EXCEEDED',
324
+ 'OUTSIDE_BUSINESS_HOURS',
325
+ 'INVALID_PARAMS',
326
+ 'NOT_FOUND',
327
+ 'LINKEDIN_ERROR',
328
+ 'AI_NOT_CONFIGURED',
329
+ 'AI_ERROR',
330
+ 'UNAUTHORIZED',
331
+ 'INTERNAL',
332
+ ];
333
+ export const EVENTS = [
334
+ 'invite_accepted',
335
+ 'reply_received',
336
+ 'positive_reply',
337
+ 'campaign_step_done',
338
+ 'campaign_completed',
339
+ 'quota_hit',
340
+ 'challenge_detected',
341
+ 'queue_item_added',
342
+ 'queue_item_sent',
343
+ 'research_progress',
344
+ 'research_completed',
345
+ ];
346
+ /* ------------------------------------------------------------------ *
347
+ * Envelope
348
+ * ------------------------------------------------------------------ */
349
+ /**
350
+ * Who asked for a request, carried on the bridge frame so the extension can
351
+ * apply Copilot-mode approval to agent-originated writes. Optional: a frame
352
+ * without it is treated exactly as before.
353
+ */
354
+ export const REQUEST_ORIGINS = ['mcp', 'cli'];
355
+ export const ORIGIN_HEADER = 'x-linkedin-toolkit-origin';
356
+ export const ErrorShapeSchema = z.object({
357
+ code: z.enum(ERROR_CODES),
358
+ message: z.string(),
359
+ retryAfter: z.number().optional(),
360
+ howToFix: z.string().optional(),
361
+ });
362
+ export const EnvelopeSchema = z.union([
363
+ z.object({
364
+ id: z.string(),
365
+ ok: z.literal(true),
366
+ data: z.unknown(),
367
+ rateLimit: RateLimitSchema.optional(),
368
+ }),
369
+ z.object({ id: z.string(), ok: z.literal(false), error: ErrorShapeSchema }),
370
+ ]);
371
+ export const BridgeEventSchema = z.object({
372
+ event: z.enum(EVENTS),
373
+ payload: z.unknown(),
374
+ });
375
+ /* ------------------------------------------------------------------ *
376
+ * Params: one zod object per action
377
+ * ------------------------------------------------------------------ */
378
+ const Empty = z.object({});
379
+ const Pagination = { start: z.number().int().min(0).optional(), count: z.number().int().min(1).optional() };
380
+ export const PARAMS = {
381
+ 'status.get': z.object({
382
+ verify: z.boolean().optional(),
383
+ postUrl: z.string().optional(),
384
+ }),
385
+ 'config.get': Empty,
386
+ 'config.set': PartialConfigSchema,
387
+ 'search.people': z.object({
388
+ keywords: z.string(),
389
+ title: z.string().optional(),
390
+ company: z.string().optional(),
391
+ location: z.string().optional(),
392
+ source: z.enum(['search', 'salesnav', 'recruiter']).optional(),
393
+ start: z.number().int().min(0).optional(),
394
+ count: z.number().int().min(1).max(100).optional(),
395
+ }),
396
+ 'profile.get': z.object({
397
+ url: z.string().optional(),
398
+ publicId: z.string().optional(),
399
+ full: z.boolean().optional(),
400
+ }),
401
+ 'profile.export': z.object({ urls: z.array(z.string()), full: z.boolean().optional() }),
402
+ 'company.get': z.object({ url: z.string().optional(), universalName: z.string().optional() }),
403
+ 'company.employees': z.object({ universalName: z.string(), ...Pagination }),
404
+ 'post.engagers': z.object({
405
+ postUrl: z.string(),
406
+ kind: z.enum(['likes', 'comments', 'both']),
407
+ ...Pagination,
408
+ }),
409
+ 'group.members': z.object({ groupUrl: z.string(), ...Pagination }),
410
+ 'event.attendees': z.object({ eventUrl: z.string(), ...Pagination }),
411
+ 'network.connections': z.object({ ...Pagination }),
412
+ 'network.followers': z.object({ ...Pagination }),
413
+ 'network.status': z.object({ publicIds: z.array(z.string()) }),
414
+ 'network.unfollowCount': Empty,
415
+ 'network.unfollowAll': Empty,
416
+ 'outreach.view': z.object({ publicId: z.string() }),
417
+ 'outreach.follow': z.object({ publicId: z.string() }),
418
+ 'outreach.invite': z.object({ publicId: z.string(), note: z.string().optional() }),
419
+ 'outreach.message': z.object({ publicId: z.string(), body: z.string() }),
420
+ 'outreach.inmail': z.object({ publicId: z.string(), subject: z.string(), body: z.string() }),
421
+ 'outreach.like': z.object({ postUrl: z.string() }),
422
+ 'outreach.comment': z.object({ postUrl: z.string(), body: z.string() }),
423
+ 'inbox.threads': z.object({
424
+ since: z.number().optional(),
425
+ unreadOnly: z.boolean().optional(),
426
+ count: z.number().int().min(1).optional(),
427
+ }),
428
+ 'inbox.messages': z.object({ threadId: z.string(), since: z.number().optional() }),
429
+ 'inbox.export': z.object({ since: z.number().optional() }),
430
+ 'list.create': z.object({ name: z.string(), tags: z.array(z.string()).optional() }),
431
+ 'list.getAll': Empty,
432
+ 'list.get': z.object({ listId: z.string() }),
433
+ 'list.add': z.object({
434
+ listId: z.string(),
435
+ profiles: z.array(ProfileSchema).optional(),
436
+ publicIds: z.array(z.string()).optional(),
437
+ }),
438
+ 'list.remove': z.object({ listId: z.string(), publicIds: z.array(z.string()) }),
439
+ 'list.members': z.object({ listId: z.string(), ...Pagination }),
440
+ 'list.delete': z.object({ listId: z.string() }),
441
+ 'list.importCsv': z.object({ listId: z.string(), csv: z.string() }),
442
+ 'campaign.create': z.object({
443
+ name: z.string(),
444
+ steps: z.array(StepSchema),
445
+ listId: z.string().optional(),
446
+ publicIds: z.array(z.string()).optional(),
447
+ settings: z
448
+ .object({ stopOnReply: z.boolean().optional(), autopilot: z.boolean().optional() })
449
+ .passthrough()
450
+ .optional(),
451
+ }),
452
+ 'campaign.getAll': Empty,
453
+ 'campaign.get': z.object({ campaignId: z.string() }),
454
+ 'campaign.enroll': z.object({ campaignId: z.string(), publicIds: z.array(z.string()) }),
455
+ 'campaign.pause': z.object({ campaignId: z.string() }),
456
+ 'campaign.resume': z.object({ campaignId: z.string() }),
457
+ 'campaign.delete': z.object({ campaignId: z.string() }),
458
+ 'campaign.tick': Empty,
459
+ 'queue.list': z.object({
460
+ status: z.enum(['pending', 'approved', 'rejected', 'sent']).optional(),
461
+ }),
462
+ 'queue.approve': z.object({
463
+ ids: z.array(z.string()),
464
+ edits: z
465
+ .record(z.string(), z.object({ note: z.string().optional(), body: z.string().optional() }))
466
+ .optional(),
467
+ }),
468
+ 'queue.reject': z.object({ ids: z.array(z.string()) }),
469
+ 'ai.complete': z.object({
470
+ task: z.enum(['opener', 'summary', 'sentiment', 'comment', 'score']),
471
+ input: z.record(z.string(), z.unknown()),
472
+ }),
473
+ 'export.csv': z.object({
474
+ kind: z.enum(['profiles', 'list', 'campaign', 'inbox']),
475
+ id: z.string().optional(),
476
+ }),
477
+ 'research.resolve': z.object({ rows: z.array(ResearchRowSchema) }),
478
+ 'research.pack': z.object({
479
+ rows: z.array(ResearchRowSchema),
480
+ listName: z.string().optional(),
481
+ enrich: z.boolean().optional(),
482
+ full: z.boolean().optional(),
483
+ }),
484
+ 'research.get': z.object({ jobId: z.string() }),
485
+ 'sync.pull': z.object({ since: z.number().optional() }),
486
+ };
487
+ export const ACTIONS = Object.keys(PARAMS);
488
+ export function isAction(name) {
489
+ return Object.prototype.hasOwnProperty.call(PARAMS, name);
490
+ }
491
+ /* ------------------------------------------------------------------ *
492
+ * Results: one zod object per action (used for OpenAPI response schemas)
493
+ * ------------------------------------------------------------------ */
494
+ const ProfilePage = z.object({
495
+ profiles: z.array(ProfileSchema),
496
+ total: z.number().optional(),
497
+ nextStart: z.number().optional(),
498
+ });
499
+ export const RESULTS = {
500
+ 'status.get': StatusSchema,
501
+ 'config.get': ConfigSchema,
502
+ 'config.set': ConfigSchema,
503
+ 'search.people': ProfilePage,
504
+ 'profile.get': ProfileSchema,
505
+ 'profile.export': z.object({
506
+ profiles: z.array(ProfileSchema),
507
+ failed: z.array(z.object({ url: z.string(), error: z.string() })),
508
+ }),
509
+ 'company.get': CompanySchema,
510
+ 'company.employees': ProfilePage,
511
+ 'post.engagers': z.object({
512
+ engagers: z.array(EngagerSchema),
513
+ nextStart: z.number().optional(),
514
+ }),
515
+ 'group.members': ProfilePage,
516
+ 'event.attendees': ProfilePage,
517
+ 'network.connections': ProfilePage,
518
+ 'network.followers': ProfilePage,
519
+ 'network.status': z.object({
520
+ statuses: z.record(z.string(), z.enum(['connected', 'pending', 'none'])),
521
+ }),
522
+ 'network.unfollowCount': z.object({ count: z.number() }),
523
+ 'network.unfollowAll': z.object({ unfollowed: z.number() }),
524
+ 'outreach.view': WriteResultSchema,
525
+ 'outreach.follow': WriteResultSchema,
526
+ 'outreach.invite': WriteResultSchema,
527
+ 'outreach.message': WriteResultSchema,
528
+ 'outreach.inmail': WriteResultSchema,
529
+ 'outreach.like': WriteResultSchema,
530
+ 'outreach.comment': WriteResultSchema,
531
+ 'inbox.threads': z.object({ threads: z.array(ThreadSchema) }),
532
+ 'inbox.messages': z.object({ messages: z.array(MessageSchema) }),
533
+ 'inbox.export': z.object({ threads: z.array(ThreadSchema), messages: z.array(MessageSchema) }),
534
+ 'list.create': ListSchema,
535
+ 'list.getAll': z.object({ lists: z.array(ListSchema) }),
536
+ 'list.get': ListSchema,
537
+ 'list.add': z.object({ added: z.number(), duplicates: z.number() }),
538
+ 'list.remove': z.object({ removed: z.number() }),
539
+ 'list.members': z.object({ members: z.array(ListMemberSchema), total: z.number() }),
540
+ 'list.delete': z.object({ ok: z.literal(true) }),
541
+ 'list.importCsv': z.object({
542
+ added: z.number(),
543
+ duplicates: z.number(),
544
+ invalid: z.number(),
545
+ }),
546
+ 'campaign.create': CampaignSchema,
547
+ 'campaign.getAll': z.object({ campaigns: z.array(CampaignSchema) }),
548
+ 'campaign.get': CampaignSchema,
549
+ 'campaign.enroll': z.object({ enrolled: z.number(), skipped: z.number() }),
550
+ 'campaign.pause': CampaignSchema,
551
+ 'campaign.resume': CampaignSchema,
552
+ 'campaign.delete': CampaignSchema,
553
+ 'campaign.tick': z.object({ executed: z.number(), queued: z.number() }),
554
+ 'queue.list': z.object({ items: z.array(QueueItemSchema) }),
555
+ 'queue.approve': z.object({ approved: z.number() }),
556
+ 'queue.reject': z.object({ rejected: z.number() }),
557
+ 'ai.complete': z.object({
558
+ output: z.union([z.string(), z.record(z.string(), z.unknown())]),
559
+ provider: z.string(),
560
+ model: z.string().optional(),
561
+ }),
562
+ 'export.csv': z.object({ csv: z.string(), filename: z.string() }),
563
+ 'research.resolve': z.object({ resolved: z.array(ResolvedRowSchema) }),
564
+ 'research.pack': z.object({ jobId: z.string(), total: z.number(), etaMs: z.number().optional() }),
565
+ 'research.get': z.object({
566
+ jobId: z.string(),
567
+ status: z.string(),
568
+ done: z.number(),
569
+ total: z.number(),
570
+ packs: z.array(PackSchema),
571
+ }),
572
+ 'sync.pull': z.object({
573
+ profiles: z.array(ProfileSchema).optional(),
574
+ lists: z.array(ListSchema).optional(),
575
+ listMembers: z.array(z.record(z.string(), z.unknown())).optional(),
576
+ campaigns: z.array(CampaignSchema).optional(),
577
+ enrollments: z.array(z.record(z.string(), z.unknown())).optional(),
578
+ actions: z.array(z.record(z.string(), z.unknown())).optional(),
579
+ threads: z.array(ThreadSchema).optional(),
580
+ messages: z.array(MessageSchema).optional(),
581
+ events: z.array(z.record(z.string(), z.unknown())).optional(),
582
+ }),
583
+ };
584
+ /* ------------------------------------------------------------------ *
585
+ * Write actions
586
+ * ------------------------------------------------------------------ */
587
+ export const WRITE_ACTIONS = new Set([
588
+ 'config.set',
589
+ 'outreach.view',
590
+ 'outreach.follow',
591
+ 'outreach.invite',
592
+ 'outreach.message',
593
+ 'outreach.inmail',
594
+ 'outreach.like',
595
+ 'outreach.comment',
596
+ 'list.create',
597
+ 'list.add',
598
+ 'list.remove',
599
+ 'list.delete',
600
+ 'list.importCsv',
601
+ 'campaign.create',
602
+ 'campaign.enroll',
603
+ 'campaign.pause',
604
+ 'campaign.resume',
605
+ 'campaign.delete',
606
+ 'queue.approve',
607
+ 'queue.reject',
608
+ 'research.pack',
609
+ ]);
610
+ export function isWriteAction(action) {
611
+ return WRITE_ACTIONS.has(action);
612
+ }
613
+ /** Server-local tools that do not proxy a single bridge action. */
614
+ export const SQL_QUERY_PARAMS = z.object({
615
+ sql: z.string(),
616
+ params: z.array(z.union([z.string(), z.number(), z.null()])).optional(),
617
+ });
618
+ export const SYNC_PARAMS = z.object({ since: z.number().optional() });
619
+ /** `linkedin_endpoints_check` always verifies; the post is an optional probe. */
620
+ export const ENDPOINTS_CHECK_PARAMS = z.object({ postUrl: z.string().optional() });
621
+ export const TOOLS = [
622
+ {
623
+ name: 'linkedin_get_status',
624
+ action: 'status.get',
625
+ description: 'Check that the Chrome extension is connected and the user is logged in to LinkedIn. Call this first in any session and again after a rate-limit error; returns extension version, autopilot on/off, business-hours flag, per-quota usage (invite, message, visit, search), pending approval-queue size and campaign counts.',
626
+ write: false,
627
+ },
628
+ {
629
+ name: 'linkedin_endpoints_check',
630
+ action: 'status.get',
631
+ description: 'Self-test every LinkedIn endpoint the extension uses; run this first when a tool returns LINKEDIN_ERROR. One read-only call per endpoint reports ok, failed, unverified or skipped, plus the LinkedIn client version the endpoint table was captured against, so you can tell "LinkedIn moved" from "the toolkit is broken". The search and profile reads it makes count against the normal daily caps.',
632
+ write: false,
633
+ },
634
+ {
635
+ name: 'linkedin_search_people',
636
+ action: 'search.people',
637
+ description: 'Search LinkedIn people and return structured profiles. Use it to build a candidate or prospect list from keywords plus optional title, company and location filters. Returns up to 100 profiles per call with nextStart for paging; the extension caps search results at 1,000 per day.',
638
+ write: false,
639
+ },
640
+ {
641
+ name: 'linkedin_get_profile',
642
+ action: 'profile.get',
643
+ description: 'Fetch one profile by URL or publicId. Use it before writing an invite or message so the copy can reference real detail. Returns the Profile; with full=true it also captures the rendered page text, photo and experience/education, which costs one profile visit against the 500/day cap.',
644
+ write: false,
645
+ },
646
+ {
647
+ name: 'linkedin_export_profiles',
648
+ action: 'profile.export',
649
+ description: 'Fetch many profiles in one call from a list of LinkedIn URLs. Use it to hydrate a list you already have URLs for. Returns profiles plus a failed array of {url, error}; each profile counts against the 500 visits/day cap, so keep batches modest.',
650
+ write: false,
651
+ },
652
+ {
653
+ name: 'linkedin_get_company',
654
+ action: 'company.get',
655
+ description: 'Fetch a company page by URL or universalName. Use it for account research before outreach. Returns name, industry, size, HQ, website, description and follower count.',
656
+ write: false,
657
+ },
658
+ {
659
+ name: 'linkedin_get_company_employees',
660
+ action: 'company.employees',
661
+ description: 'List people who work at a company, by universalName. Use it for account-based sourcing once you know the company. Returns a page of profiles plus nextStart; results count against the daily search cap.',
662
+ write: false,
663
+ },
664
+ {
665
+ name: 'linkedin_get_post_engagers',
666
+ action: 'post.engagers',
667
+ description: 'List the people who liked or commented on a LinkedIn post. Use it to source warm leads who have shown intent. Returns engagers (a profile plus reaction or comment text) and nextStart.',
668
+ write: false,
669
+ },
670
+ {
671
+ name: 'linkedin_get_group_members',
672
+ action: 'group.members',
673
+ description: 'List members of a LinkedIn group you belong to. Use it for niche sourcing. Returns a page of profiles plus nextStart.',
674
+ write: false,
675
+ },
676
+ {
677
+ name: 'linkedin_get_event_attendees',
678
+ action: 'event.attendees',
679
+ description: 'List attendees of a LinkedIn event you can see. Use it to source people around a conference or webinar. Returns a page of profiles plus nextStart.',
680
+ write: false,
681
+ },
682
+ {
683
+ name: 'linkedin_get_connections',
684
+ action: 'network.connections',
685
+ description: "List the user's own first-degree connections. Use it to work an existing network rather than sending new invites. Returns a page of profiles plus nextStart.",
686
+ write: false,
687
+ },
688
+ {
689
+ name: 'linkedin_get_connection_status',
690
+ action: 'network.status',
691
+ description: 'Check whether the user is already connected to, or has a pending invite with, each of the given publicIds. Always call this before sending invites so you do not re-invite existing connections. Returns a map publicId to connected | pending | none.',
692
+ write: false,
693
+ },
694
+ {
695
+ name: 'linkedin_get_conversations',
696
+ action: 'inbox.threads',
697
+ description: 'List LinkedIn inbox threads, optionally only those since a timestamp or only unread. Use it to triage replies. Returns threads with participants, snippet, unread flag and sentiment when the extension has an AI provider configured.',
698
+ write: false,
699
+ },
700
+ {
701
+ name: 'linkedin_get_messages',
702
+ action: 'inbox.messages',
703
+ description: 'Fetch the messages in one thread by threadId. Use it after linkedin_get_conversations to read the full exchange before replying. Returns messages with sender publicId, body and sentAt.',
704
+ write: false,
705
+ },
706
+ {
707
+ name: 'linkedin_list_create',
708
+ action: 'list.create',
709
+ description: 'Create a named local list to hold prospects. Use it as the container for search results before enrolling them in a campaign. Returns the List with its listId. Lists live only in the local extension storage.',
710
+ write: true,
711
+ },
712
+ {
713
+ name: 'linkedin_list_get',
714
+ action: 'list.get',
715
+ description: 'Fetch one list by listId. Use it to confirm a list exists and how many members it holds. Returns the List record.',
716
+ write: false,
717
+ },
718
+ {
719
+ name: 'linkedin_list_all',
720
+ action: 'list.getAll',
721
+ description: 'List every local list. Use it to discover listIds before adding members or creating a campaign. Returns all List records with their member counts.',
722
+ write: false,
723
+ },
724
+ {
725
+ name: 'linkedin_list_add',
726
+ action: 'list.add',
727
+ description: 'Add profiles (or bare publicIds) to a list. Use it to save search or engager results for later outreach. Duplicates are skipped; returns {added, duplicates}.',
728
+ write: true,
729
+ },
730
+ {
731
+ name: 'linkedin_list_members',
732
+ action: 'list.members',
733
+ description: 'Page through the members of a list. Use it to read back what is in a list, including tags, whether each person was contacted before and any signals. Returns members plus total.',
734
+ write: false,
735
+ },
736
+ {
737
+ name: 'linkedin_view_profile',
738
+ action: 'outreach.view',
739
+ description: 'Visit a profile so the visit shows up in their "who viewed your profile". Use it as a light warm-up touch before an invite. This is a direct, metered action: it is paced and drawn from the visit bucket (500 visits/day) but never queued for approval, so the result status is "sent". Pass dry_run to preview.',
740
+ write: true,
741
+ },
742
+ {
743
+ name: 'linkedin_follow',
744
+ action: 'outreach.follow',
745
+ description: 'Follow a person without sending a connection invite. Use it when an invite would be too strong a first touch. This is a direct, metered action: it is paced and drawn from the visit bucket but never queued for approval, so the result status is "sent". Pass dry_run to preview.',
746
+ write: true,
747
+ },
748
+ {
749
+ name: 'linkedin_send_invite',
750
+ action: 'outreach.invite',
751
+ description: 'Send a connection invite, optionally with a note. Check linkedin_get_connection_status first. Hard cap 100 invites/day; in Copilot mode (the default) the invite is queued for human approval and the result status is "queued" rather than "sent". Pass dry_run to preview the exact payload.',
752
+ write: true,
753
+ },
754
+ {
755
+ name: 'linkedin_send_message',
756
+ action: 'outreach.message',
757
+ description: 'Send a direct message to a first-degree connection. Hard cap 150 messages/day; in Copilot mode it is queued for approval. Returns a WriteResult; pass dry_run to preview.',
758
+ write: true,
759
+ },
760
+ {
761
+ name: 'linkedin_send_inmail',
762
+ action: 'outreach.inmail',
763
+ description: 'Send an InMail with a subject line (requires Premium, Sales Navigator or Recruiter). Counts against the message cap and queues for approval in Copilot mode. Returns a WriteResult; pass dry_run to preview.',
764
+ write: true,
765
+ },
766
+ {
767
+ name: 'linkedin_like_post',
768
+ action: 'outreach.like',
769
+ description: 'Like a post by URL. Use it as a low-risk warm-up touch before inviting the author. This is a direct, metered action: it is paced and drawn from the visit bucket but never queued for approval, so the result status is "sent". Unlike a comment, a like carries no words of yours. Pass dry_run to preview.',
770
+ write: true,
771
+ },
772
+ {
773
+ name: 'linkedin_comment_post',
774
+ action: 'outreach.comment',
775
+ description: 'Comment on a post by URL. Use it for public engagement before outreach; comments are queued for approval in Copilot mode because they are visible to everyone. Returns a WriteResult; pass dry_run to preview.',
776
+ write: true,
777
+ },
778
+ {
779
+ name: 'linkedin_campaign_create',
780
+ action: 'campaign.create',
781
+ description: 'Create a multi-step outreach sequence (view, follow, invite, message, inmail, like, comment, wait, branch) over a list or explicit publicIds. Use it instead of firing individual writes when the touches should be spaced over days. Returns the Campaign; steps still obey every quota and the approval queue.',
782
+ write: true,
783
+ },
784
+ {
785
+ name: 'linkedin_campaign_get',
786
+ action: 'campaign.get',
787
+ description: 'Fetch one campaign by campaignId including its stats. Use it to report on enrolled, sent, accepted, replied and positive counts per step.',
788
+ write: false,
789
+ },
790
+ {
791
+ name: 'linkedin_campaign_list',
792
+ action: 'campaign.getAll',
793
+ description: 'List every campaign with its status. Use it to find campaignIds and see what is currently running or paused.',
794
+ write: false,
795
+ },
796
+ {
797
+ name: 'linkedin_campaign_enroll',
798
+ action: 'campaign.enroll',
799
+ description: 'Enroll publicIds into an existing campaign. Use it to top up a running sequence with newly sourced people. Already-enrolled people are skipped; returns {enrolled, skipped}.',
800
+ write: true,
801
+ },
802
+ {
803
+ name: 'linkedin_campaign_pause',
804
+ action: 'campaign.pause',
805
+ description: 'Pause a campaign so no further steps execute. Use it immediately if replies look negative or a challenge was detected. Returns the updated Campaign.',
806
+ write: true,
807
+ },
808
+ {
809
+ name: 'linkedin_campaign_resume',
810
+ action: 'campaign.resume',
811
+ description: 'Resume a paused campaign from where it stopped. Returns the updated Campaign.',
812
+ write: true,
813
+ },
814
+ {
815
+ name: 'linkedin_queue_list',
816
+ action: 'queue.list',
817
+ description: 'List items in the human-approval queue, optionally filtered by status. In Copilot mode every agent-originated write lands here first, so call this to show the user what is waiting. Returns queue items with their action, params and target profile.',
818
+ write: false,
819
+ },
820
+ {
821
+ name: 'linkedin_queue_approve',
822
+ action: 'queue.approve',
823
+ description: 'Approve queued writes by id so the extension sends them, optionally editing the note or body first. This works only when the user has turned Autopilot on: in the default Copilot mode approval is a human action and the extension answers UNAUTHORIZED, so show the queue with linkedin_queue_list and ask the user to approve in the popup. Returns {approved}.',
824
+ write: true,
825
+ },
826
+ {
827
+ name: 'linkedin_queue_reject',
828
+ action: 'queue.reject',
829
+ description: 'Reject queued writes by id so they are never sent. Like approving, this works only when the user has turned Autopilot on; in the default Copilot mode the extension answers UNAUTHORIZED and the user rejects in the popup. Returns {rejected}.',
830
+ write: true,
831
+ },
832
+ {
833
+ name: 'linkedin_research_pack',
834
+ action: 'research.pack',
835
+ description: 'Turn a list of rows (name, LinkedIn URL, email, domain or company) into full research packs: resolved profile, company, recent posts, mutual connections, connection status, signals and a ready-to-read markdown brief per row. Use it as the one-shot "research these people for me" tool. The server waits for the job to finish and returns the packs; if it takes longer than the research timeout it returns {jobId, status:"running"} and you should poll linkedin_research_get.',
836
+ write: true,
837
+ },
838
+ {
839
+ name: 'linkedin_research_get',
840
+ action: 'research.get',
841
+ description: 'Poll a research job by jobId. Use it after linkedin_research_pack returned status "running". Returns {jobId, status, done, total, packs} with the packs finished so far.',
842
+ write: false,
843
+ },
844
+ {
845
+ name: 'linkedin_research_resolve',
846
+ action: 'research.resolve',
847
+ description: 'Resolve messy rows (a name, an email, a company domain) to LinkedIn people or companies without doing the full research gather. Use it as a cheap first pass to check match quality before spending visits on linkedin_research_pack. Returns each row with kind, publicId or universalName, a confidence score and candidate profiles.',
848
+ write: false,
849
+ },
850
+ {
851
+ name: 'linkedin_query_sql',
852
+ action: null,
853
+ description: 'Run a read-only SQL query against the local SQLite mirror of everything the toolkit has captured (tables: profiles, companies, searches, search_results, lists, list_members, campaigns, enrollments, actions, conversations, messages, events, packs). Use it for counting, filtering and joining across past work instead of re-scraping LinkedIn. Only a single SELECT or WITH statement is allowed and at most 1,000 rows are returned.',
854
+ write: false,
855
+ },
856
+ {
857
+ name: 'linkedin_sync',
858
+ action: 'sync.pull',
859
+ description: 'Pull everything changed in the extension since the last sync into the local SQLite mirror. Call it before linkedin_query_sql so the database is current. Returns per-table row counts and the new sync timestamp.',
860
+ write: false,
861
+ },
862
+ ];
863
+ export const TOOL_NAMES = TOOLS.map((t) => t.name);
864
+ export function toolByName(name) {
865
+ return TOOLS.find((t) => t.name === name);
866
+ }
867
+ /** Input schema for a tool, including `dry_run` for write tools. */
868
+ export function toolInputSchema(tool) {
869
+ let base;
870
+ if (tool.name === 'linkedin_query_sql')
871
+ base = SQL_QUERY_PARAMS;
872
+ else if (tool.name === 'linkedin_sync')
873
+ base = SYNC_PARAMS;
874
+ else if (tool.name === 'linkedin_endpoints_check')
875
+ base = ENDPOINTS_CHECK_PARAMS;
876
+ else
877
+ base = PARAMS[tool.action];
878
+ return tool.write ? base.extend({ dry_run: z.boolean().optional() }) : base;
879
+ }
880
+ /* ------------------------------------------------------------------ *
881
+ * MCP resources and prompts
882
+ * ------------------------------------------------------------------ */
883
+ export const RESOURCES = [
884
+ {
885
+ name: 'status',
886
+ uri: 'linkedin://status',
887
+ description: 'Live connection, quota and queue status of the LinkedIn Toolkit extension.',
888
+ mimeType: 'application/json',
889
+ },
890
+ {
891
+ name: 'profile',
892
+ uriTemplate: 'linkedin://profile/{publicId}',
893
+ description: 'One LinkedIn profile by publicId, fetched live through the extension.',
894
+ mimeType: 'application/json',
895
+ },
896
+ {
897
+ name: 'list',
898
+ uriTemplate: 'linkedin://list/{listId}',
899
+ description: 'One local prospect list with its members.',
900
+ mimeType: 'application/json',
901
+ },
902
+ {
903
+ name: 'queue',
904
+ uri: 'linkedin://queue',
905
+ description: 'The pending human-approval queue.',
906
+ mimeType: 'application/json',
907
+ },
908
+ ];
909
+ export const PROMPTS = [
910
+ {
911
+ name: 'source-candidates',
912
+ description: 'Source and shortlist people on LinkedIn from a short brief, saving them to a list.',
913
+ args: { brief: z.string(), count: z.string().optional() },
914
+ },
915
+ {
916
+ name: 'write-opener',
917
+ description: 'Draft a short, specific connection-request opener for one person.',
918
+ args: { publicId: z.string(), tone: z.string().optional() },
919
+ },
920
+ {
921
+ name: 'triage-inbox',
922
+ description: 'Triage recent LinkedIn replies into positive, neutral and negative buckets.',
923
+ args: { since: z.string().optional() },
924
+ },
925
+ ];
926
+ //# sourceMappingURL=contract.js.map