@agifyai/leadify-mcp 2.0.0 → 3.2.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.
@@ -170,11 +170,13 @@ const ORG_ID_DESC = "REQUIRED Clerk organization ID targeting the org whose data
170
170
  "data room (read or write). Requires admin access for writes.";
171
171
  const DRY_RUN_DESC = "If true, validate the merged payload + return the preview WITHOUT writing. " +
172
172
  "Use to dry-fit a change before persisting.";
173
- async function fetchDataRoom(organizationId) {
174
- const params = organizationId
175
- ? new URLSearchParams({ organizationId })
176
- : undefined;
177
- const data = (await getClient().get("/api/data-room", params));
173
+ async function fetchDataRoom(organizationId, opts) {
174
+ const params = new URLSearchParams();
175
+ if (organizationId)
176
+ params.set("organizationId", organizationId);
177
+ if (opts?.includePersonas)
178
+ params.set("includePersonas", "true");
179
+ const data = (await getClient().get("/api/data-room", params.toString() ? params : undefined));
178
180
  return data ?? {};
179
181
  }
180
182
  function currentCompanyInfo(state) {
@@ -242,13 +244,26 @@ async function mergeAndPut(organizationId, dry_run, patcher, validationHint) {
242
244
  // ─── Tool registrations ────────────────────────────────────────────────────
243
245
  export function registerDataRoomTools(server) {
244
246
  // ── get_data_room ──────────────────────────────────────────────────────
245
- server.tool("get_data_room", "Retrieve the organization's full data room: company info (v2 schema), all documents, and " +
246
- "every persona defined for the workspace. Use this as the entry point when you need to " +
247
- "understand the company's context before writing campaigns or messages.", {
247
+ server.tool("get_data_room", "Retrieve the organization's data room: company info (v2 schema) and all documents. " +
248
+ "Use this as the entry point when you need to understand the company's context " +
249
+ "before writing campaigns or messages. " +
250
+ "By default, `personas[]` is NO LONGER included in the response (slimed down to " +
251
+ "keep the LLM context small — the data room itself was 158k chars). " +
252
+ "Pass `include_personas=true` to restore the legacy full payload (escape hatch). " +
253
+ "For per-persona queries, prefer the `list_personas` tool.", {
248
254
  organization_id: z.string().describe(ORG_ID_DESC),
249
- }, async ({ organization_id }) => {
255
+ include_personas: z
256
+ .boolean()
257
+ .optional()
258
+ .default(false)
259
+ .describe("If true, include the full `personas[]` array in the response (legacy " +
260
+ "behaviour, ~150k chars). If false (default), personas are omitted — use " +
261
+ "`list_personas` for per-persona queries."),
262
+ }, async ({ organization_id, include_personas }) => {
250
263
  try {
251
- const data = await fetchDataRoom(organization_id);
264
+ const data = await fetchDataRoom(organization_id, {
265
+ includePersonas: include_personas === true,
266
+ });
252
267
  return toolResult(data);
253
268
  }
254
269
  catch (error) {
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerFineTuningTools(server: McpServer): void;
@@ -0,0 +1,33 @@
1
+ import { z } from "zod";
2
+ import { getClient } from "../client.js";
3
+ import { toolResult, handleToolError } from "../types.js";
4
+ // Dedicated tool for the Fine Tuning page (separate from OutreachSettings).
5
+ // Both concepts live in the DB (FineTuning + OutreachSettings) and the API
6
+ // keeps them strictly separated: this endpoint NEVER touches OutreachSettings.
7
+ export function registerFineTuningTools(server) {
8
+ // ── get_fine_tuning ───────────────────────────────────────────────────
9
+ server.tool("get_fine_tuning", "Get the fine tuning configuration for a specific lead group. Returns exactly " +
10
+ "the 5 levers of the Fine Tuning page and nothing else: " +
11
+ "(1) nonNegotiableRules (Markdown), (2) pitfalls (Markdown), (3) structure (Markdown), " +
12
+ "(4) supportedLanguages (string[]), (5) examples (Markdown), plus fallbackLanguage. " +
13
+ "Read STRICTLY from the FineTuning model — zero OutreachSettings mixed in. " +
14
+ "Use this when editing the Fine Tuning page or when an agent needs the copywriting " +
15
+ "guardrails (rules + pitfalls + structure + examples + languages). " +
16
+ "DO NOT use get_outreach_settings for this: that endpoint returns 100× more payload " +
17
+ "and is for the Outreach agent preflight, not for fine tuning. " +
18
+ "Upsert-on-read: if no FineTuning row exists for the lead group, one is created " +
19
+ "with defaults, so the response shape is always stable.", {
20
+ lead_group_id: z
21
+ .string()
22
+ .describe("ID of the lead group whose fine tuning configuration to fetch. " +
23
+ "Resolve via list_organizations + get_leads if needed."),
24
+ }, async ({ lead_group_id }) => {
25
+ try {
26
+ const data = await getClient().get(`/api/lead-group/${encodeURIComponent(lead_group_id)}/fine-tuning`);
27
+ return toolResult(data);
28
+ }
29
+ catch (error) {
30
+ return handleToolError(error);
31
+ }
32
+ });
33
+ }
@@ -158,11 +158,26 @@ export function registerLeadGroupTools(server) {
158
158
  // ── get_lead_group_persona ─────────────────────────────────────────────
159
159
  server.tool("get_lead_group_persona", "Retrieve the persona assigned to a specific lead group, with group context. " +
160
160
  "Returns null persona if none is assigned. Use this to check what targeting " +
161
- "rules apply to a group before generating messages.", {
161
+ "rules apply to a group before generating messages. " +
162
+ "By default, `persona` is SLIM ({id, name, organizationId}) — about ~150 chars " +
163
+ "instead of 69k — just enough to resolve the org / link the persona to a lead group. " +
164
+ "Pass `view=full` to get the complete persona (legacy behaviour). " +
165
+ "Pass `view=outreach` to get the outreach-specific projection.", {
162
166
  lead_group_id: z.string().describe("Lead group ID."),
163
- }, async ({ lead_group_id }) => {
167
+ view: z
168
+ .enum(["slim", "outreach", "full"])
169
+ .optional()
170
+ .default("slim")
171
+ .describe("Persona projection. " +
172
+ "'slim' (default) = {id, name, organizationId} — fast, context-friendly. " +
173
+ "'outreach' = projection used by the outreach agent (messaging + targeting). " +
174
+ "'full' = complete persona payload (legacy 69k-char shape, escape hatch)."),
175
+ }, async ({ lead_group_id, view }) => {
164
176
  try {
165
- const data = await getClient().get(`/api/lead-group/${encodeURIComponent(lead_group_id)}/persona`);
177
+ const params = new URLSearchParams();
178
+ if (view)
179
+ params.set("view", view);
180
+ const data = await getClient().get(`/api/lead-group/${encodeURIComponent(lead_group_id)}/persona`, params);
166
181
  return toolResult(data);
167
182
  }
168
183
  catch (error) {
@@ -107,7 +107,11 @@ export function registerLeadTools(server) {
107
107
  "group membership, creation/update timestamps, related leads (if relations are " +
108
108
  "configured), and the group schema. Use this when you already have a lead ID " +
109
109
  "and need its complete information. For searching or browsing multiple leads, " +
110
- "use get_leads instead.", {
110
+ "use get_leads instead. " +
111
+ "Note: the group schema lives at `lead.group.schema` (no top-level `schema` " +
112
+ "field). System-level `select` field options (qualification_level, " +
113
+ "qualification_status, seniority, institution_type) are reconciled against the " +
114
+ "canonical registry, so all legitimate values are listed.", {
111
115
  id: z.string().describe("The unique ID of the lead to retrieve."),
112
116
  }, async ({ id }) => {
113
117
  try {
@@ -78,11 +78,24 @@ export function registerOutreachSettingsTools(server) {
78
78
  // ── get_outreach_settings ──────────────────────────────────────────────
79
79
  server.tool("get_outreach_settings", "Get the outreach settings (positioning, sequence design, rules, URLs, case studies) " +
80
80
  "for a specific lead group / campaign. The settings drive the [Leadify] Outreach agent " +
81
- "preflight. GET auto-creates a row with defaults on first read.", {
81
+ "preflight. GET auto-creates a row with defaults on first read. " +
82
+ "Pass `sections` (CSV) to slice the response and avoid saturating the LLM context " +
83
+ "when you only need one part (e.g. 'positioning,urls'). Allowed: " +
84
+ "positioning, sequenceDesign, rules, caseStudies, urls, connection.", {
82
85
  lead_group_id: z.string().describe("Lead group ID."),
83
- }, async ({ lead_group_id }) => {
86
+ sections: z
87
+ .string()
88
+ .optional()
89
+ .describe("Optional CSV of section names to return. " +
90
+ "Allowed values: positioning, sequenceDesign, rules, caseStudies, urls, connection. " +
91
+ "If omitted, the full row is returned (legacy behaviour). " +
92
+ "Use this to keep the LLM context small when you only need one section."),
93
+ }, async ({ lead_group_id, sections }) => {
84
94
  try {
85
- const data = await getClient().get(`/api/lead-group/${encodeURIComponent(lead_group_id)}/outreach-settings`);
95
+ const params = new URLSearchParams();
96
+ if (sections)
97
+ params.set("sections", sections);
98
+ const data = await getClient().get(`/api/lead-group/${encodeURIComponent(lead_group_id)}/outreach-settings`, params);
86
99
  return toolResult(data);
87
100
  }
88
101
  catch (error) {
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { getClient } from "../client.js";
3
- import { toolResult, handleToolError } from "../types.js";
3
+ import { toolResult, toolError, handleToolError } from "../types.js";
4
4
  export function registerPipelineTools(server) {
5
5
  // ── trigger_outreach ─────────────────────────────────────────────────────
6
6
  server.tool("trigger_outreach", "Generate the outreach message SEQUENCE for a lead via the Trigger.dev agent " +
@@ -15,8 +15,21 @@ export function registerPipelineTools(server) {
15
15
  "card_* sequence fields to lead.data). " +
16
16
  "The 'force' flag (default false) bypasses the 'already has a message?' guard — " +
17
17
  "useful for re-generating on an already-processed lead. " +
18
+ "REQUIRES a 'campaign_id': the pilot slug (e.g. 'agify-medtech-fr-v2') that selects " +
19
+ "which copywriting pilot the writer follows. It is NOT a DB id — it must match a pilot " +
20
+ "authored in the Trigger.dev repo (pilots/{campaign_id}.md + .json), and that pilot's " +
21
+ ".json is paired to the lead's group. The skill operator picks it; there is no default. " +
22
+ "The lead's `leadGroupId` is resolved server-side from the lead (one extra " +
23
+ "`get_lead` call); the caller only needs `lead_id`. " +
24
+ "The response is decorated with a Trigger.dev `traceUrl` deep-link when missing " +
25
+ "(https://cloud.trigger.dev/runs/{runId}), so you can always jump to the trace. " +
18
26
  "Waits up to 120s for the remote agent to complete.", {
19
27
  lead_id: z.string().describe("ID of the lead to generate a message for."),
28
+ campaign_id: z
29
+ .string()
30
+ .describe("Pilot slug driving the copywriting (e.g. 'agify-medtech-fr-v2'). NOT a DB campaign id. " +
31
+ "Must match a pilot file pair in the Trigger.dev repo whose .json is paired to this " +
32
+ "lead's group. The writer hard-fails if the pilot is missing."),
20
33
  dryrun: z
21
34
  .boolean()
22
35
  .optional()
@@ -30,13 +43,35 @@ export function registerPipelineTools(server) {
30
43
  .default(false)
31
44
  .describe("true = bypass the 'already has a message?' check so you can re-generate. " +
32
45
  "false (default) = normal behaviour: skip leads that already have card_message."),
33
- }, async ({ lead_id, dryrun, force }) => {
46
+ }, async ({ lead_id, campaign_id, dryrun, force }) => {
34
47
  try {
35
- const data = await getClient().post("/api/trigger-outreach", {
48
+ // Resolve leadGroupId server-side. The Trigger.dev task payload requires
49
+ // `leadGroupId`, but the lead_id alone is enough on the MCP side — we
50
+ // fetch the lead to extract groupId instead of forcing the caller (human
51
+ // or LLM) to pass it manually. One extra HTTP call, dwarfed by the
52
+ // 10–60s Trigger.dev wait that follows.
53
+ const leadParams = new URLSearchParams();
54
+ leadParams.set("id", lead_id);
55
+ const leadRes = (await getClient().get("/get-lead", leadParams));
56
+ const leadGroupId = leadRes?.lead?.groupId;
57
+ if (!leadGroupId) {
58
+ return toolError(`Cannot resolve leadGroupId for lead "${lead_id}". ` +
59
+ `The lead was not found, or it has no groupId. ` +
60
+ `Check that the lead exists via get_lead.`);
61
+ }
62
+ const data = (await getClient().post("/api/trigger-outreach", {
36
63
  leadId: lead_id,
64
+ leadGroupId,
65
+ campaignId: campaign_id,
37
66
  dryrun,
38
67
  force,
39
- });
68
+ }));
69
+ // Decorate the response with a Trigger.dev run deep-link when the
70
+ // backend didn't ship one. Applies to BOTH success and failure paths
71
+ // so the skill operator can always jump to the trace.
72
+ if (data?.runId && !data.traceUrl) {
73
+ data.traceUrl = `https://cloud.trigger.dev/runs/${data.runId}`;
74
+ }
40
75
  return toolResult(data);
41
76
  }
42
77
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agifyai/leadify-mcp",
3
- "version": "2.0.0",
3
+ "version": "3.2.0",
4
4
  "description": "MCP server for Leadify lead management API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",