@agifyai/leadify-mcp 8.0.0 → 8.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -179,6 +179,8 @@ Conséquences pratiques :
179
179
  | Tool | Description |
180
180
  |------|-------------|
181
181
  | `test_api_key` | Vérifier que la clé API configurée est valide (health check). |
182
+ | `list_lead_groups` | Lister les groupes accessibles d'une organisation explicitement sélectionnée, en vue compacte. |
183
+ | `get_lead_group` | Consulter les métadonnées compactes d'un groupe accessible par son ID. |
182
184
  | `add_leads` | Ajouter un ou plusieurs leads à un groupe. |
183
185
  | `get_leads` | Rechercher et lister des leads avec filtres, recherche et pagination. |
184
186
  | `get_lead` | Récupérer les détails complets d'un lead par son ID. |
@@ -192,6 +194,7 @@ Conséquences pratiques :
192
194
  | `delete_campaign_log` | Supprimer une entrée de log de campagne. |
193
195
  | `update_campaign_stats` | Mettre à jour les statistiques d'email d'une campagne pour un groupe de leads. |
194
196
  | `create_campaign` | Créer une nouvelle campagne rattachée à un groupe de leads (statut DRAFT). |
197
+ | `list_campaigns` | Lister compactement les campagnes d'une organisation explicitement sélectionnée et leur statut courant. |
195
198
  | `get_campaign` | Récupérer les détails d'une campagne et ses KPIs temps réel. |
196
199
  | `update_campaign_status` | Changer le statut d'une campagne (DRAFT, ACTIVE, PAUSED, COMPLETED). |
197
200
  | `export_campaign` | Exporter les statistiques complètes d'une campagne en CSV. |
package/dist/server.js CHANGED
@@ -16,7 +16,7 @@ import { registerContextWorkspaceTools } from "./tools/context_workspace.js";
16
16
  export function createServer() {
17
17
  const server = new McpServer({
18
18
  name: "leadify",
19
- version: "8.0.0",
19
+ version: "8.1.1",
20
20
  });
21
21
  registerAuthTools(server);
22
22
  registerOrganizationTools(server);
@@ -1,2 +1,5 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- export declare function registerCampaignTools(server: McpServer): void;
2
+ import { type LeadifyClient } from "../client.js";
3
+ type CampaignReadClient = Pick<LeadifyClient, "get">;
4
+ export declare function registerCampaignTools(server: McpServer, readClient?: CampaignReadClient): void;
5
+ export {};
@@ -1,7 +1,25 @@
1
1
  import { z } from "zod";
2
2
  import { getClient } from "../client.js";
3
3
  import { toolResult, handleToolError } from "../types.js";
4
- export function registerCampaignTools(server) {
4
+ const CAMPAIGN_STATUSES = ["DRAFT", "ACTIVE", "PAUSED", "COMPLETED"];
5
+ function assertCampaignBelongsToOrganization(data, organizationId) {
6
+ if (!data || typeof data !== "object") {
7
+ throw new Error("Leadify returned an invalid campaign response");
8
+ }
9
+ const campaign = data;
10
+ const leadGroup = campaign.leadGroup;
11
+ const actualOrganizationId = campaign.organizationId ?? (leadGroup && typeof leadGroup === "object"
12
+ ? leadGroup.organizationId
13
+ : undefined);
14
+ if (actualOrganizationId !== organizationId) {
15
+ throw new Error("Campaign does not belong to the explicitly selected organization");
16
+ }
17
+ if (!CAMPAIGN_STATUSES.includes(campaign.status)) {
18
+ throw new Error("Leadify returned a campaign with an invalid current status");
19
+ }
20
+ return campaign;
21
+ }
22
+ export function registerCampaignTools(server, readClient) {
5
23
  // ── update_campaign_stats ──────────────────────────────────────────────
6
24
  server.tool("update_campaign_stats", "Update email campaign statistics for a lead group. If the given campaignSlug " +
7
25
  "doesn't exist in the group's stats yet, it is created; otherwise provided fields " +
@@ -98,14 +116,63 @@ export function registerCampaignTools(server) {
98
116
  }
99
117
  });
100
118
  // ── get_campaign ───────────────────────────────────────────────────────
101
- server.tool("get_campaign", "Retrieve full details for a campaign by its ID, including real-time calculated KPIs " +
102
- "(invitations sent, accept rate, messages sent, emails sent, reply rate, golden signals). " +
103
- "Use this to check a campaign's current performance.", {
119
+ server.tool("get_campaign", "Retrieve a campaign chosen from list_campaigns, scoped to the explicitly selected " +
120
+ "organization. Includes its normalized current status (DRAFT, ACTIVE, PAUSED, or COMPLETED) " +
121
+ "and real-time KPIs. Never use campaign logs as the source of status.", {
104
122
  id: z.string().describe("Campaign ID."),
105
- }, async ({ id }) => {
123
+ organization_id: z.string().describe("Organization selected explicitly via discover_leadify_context or list_organizations."),
124
+ }, async ({ id, organization_id }) => {
106
125
  try {
107
- const data = await getClient().get(`/api/campaign/${encodeURIComponent(id)}`);
108
- return toolResult(data);
126
+ const data = await (readClient ?? getClient()).get(`/api/campaign/${encodeURIComponent(id)}`);
127
+ return toolResult(assertCampaignBelongsToOrganization(data, organization_id));
128
+ }
129
+ catch (error) {
130
+ return handleToolError(error);
131
+ }
132
+ });
133
+ // ── list_campaigns ─────────────────────────────────────────────────────
134
+ server.tool("list_campaigns", "List campaigns in one explicitly selected organization with compact current status metadata. " +
135
+ "Use this after discover_leadify_context to identify ACTIVE, DRAFT, PAUSED, or COMPLETED " +
136
+ "campaigns without reading historical logs.", {
137
+ organization_id: z.string().describe("Organization selected explicitly via discover_leadify_context or list_organizations."),
138
+ status: z.enum(CAMPAIGN_STATUSES).optional().describe("Optional current-status filter."),
139
+ limit: z.number().int().min(1).max(100).optional().default(50).describe("Maximum campaigns returned, bounded to 100."),
140
+ }, async ({ organization_id, status, limit }) => {
141
+ try {
142
+ const data = await (readClient ?? getClient()).get("/api/campaigns", new URLSearchParams({ organizationId: organization_id }));
143
+ if (!data || typeof data !== "object" || !Array.isArray(data.campaigns)) {
144
+ throw new Error("Leadify returned an invalid campaign list response");
145
+ }
146
+ const response = data;
147
+ if (response.organizationId !== organization_id) {
148
+ throw new Error("Leadify returned campaigns from a different organization");
149
+ }
150
+ const campaigns = response.campaigns.map((item) => {
151
+ if (!item || typeof item !== "object")
152
+ throw new Error("Leadify returned an invalid campaign");
153
+ const campaign = item;
154
+ if (!CAMPAIGN_STATUSES.includes(campaign.status)) {
155
+ throw new Error("Leadify returned a campaign with an invalid current status");
156
+ }
157
+ return {
158
+ id: campaign.id,
159
+ name: campaign.name,
160
+ status: campaign.status,
161
+ channel: campaign.channel,
162
+ leadGroupId: campaign.leadGroupId,
163
+ createdAt: campaign.createdAt,
164
+ updatedAt: campaign.updatedAt,
165
+ };
166
+ });
167
+ const filtered = status ? campaigns.filter((campaign) => campaign.status === status) : campaigns;
168
+ const statusCounts = Object.fromEntries(CAMPAIGN_STATUSES.map((value) => [value, campaigns.filter((campaign) => campaign.status === value).length]));
169
+ return toolResult({
170
+ organizationId: organization_id,
171
+ campaigns: filtered.slice(0, limit),
172
+ total: filtered.length,
173
+ truncated: filtered.length > limit,
174
+ statusCounts,
175
+ });
109
176
  }
110
177
  catch (error) {
111
178
  return handleToolError(error);
@@ -1,3 +1,5 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  export declare function assertLeadGroupsBelongToOrganization(data: unknown, organizationId: string): Array<Record<string, unknown>>;
3
+ /** Keep group reads navigable: never return schema, profiles, or lead data. */
4
+ export declare function compactLeadGroup(group: unknown): Record<string, unknown>;
3
5
  export declare function registerOrganizationTools(server: McpServer): void;
@@ -22,6 +22,28 @@ export function assertLeadGroupsBelongToOrganization(data, organizationId) {
22
22
  }
23
23
  return leadGroups;
24
24
  }
25
+ /** Keep group reads navigable: never return schema, profiles, or lead data. */
26
+ export function compactLeadGroup(group) {
27
+ const source = group && typeof group === "object" ? group : {};
28
+ const count = source._count && typeof source._count === "object"
29
+ ? source._count
30
+ : {};
31
+ return {
32
+ id: source.id,
33
+ name: source.name,
34
+ description: source.description ?? null,
35
+ organizationId: source.organizationId,
36
+ entityKind: source.entityKind ?? "GENERIC",
37
+ personaTypes: Array.isArray(source.personaTypes) ? source.personaTypes : [],
38
+ hasPersona: Boolean(source.personaId),
39
+ disabledTools: Array.isArray(source.disabledTools) ? source.disabledTools : [],
40
+ leadCount: typeof count.leads === "number" ? count.leads : 0,
41
+ published: source.published ?? false,
42
+ refreshEnabled: source.refreshEnabled ?? false,
43
+ refreshFrequencyDays: source.refreshFrequencyDays ?? null,
44
+ createdAt: source.createdAt ?? null,
45
+ };
46
+ }
25
47
  export function registerOrganizationTools(server) {
26
48
  // ── list_organizations ─────────────────────────────────────────────────
27
49
  server.tool("list_organizations", "List every organization accessible to the caller, returning each with id, name, and " +
@@ -64,6 +86,21 @@ export function registerOrganizationTools(server) {
64
86
  return handleToolError(error);
65
87
  }
66
88
  });
89
+ server.tool("get_lead_group", "Read the compact metadata of one accessible lead group by ID. Use this after list_lead_groups " +
90
+ "or discover_leadify_context to confirm its type and operational state. Returns only navigation " +
91
+ "metadata (no lead records, schema, company profile, or persona payload) and reports inaccessible " +
92
+ "groups as not found, without revealing their organization.", {
93
+ lead_group_id: z.string().describe("Lead group ID returned by Leadify discovery or another authorized Leadify response."),
94
+ }, async ({ lead_group_id }) => {
95
+ try {
96
+ const data = await getClient().get(`/api/lead-group/${encodeURIComponent(lead_group_id)}`);
97
+ const source = data && typeof data === "object" ? data : {};
98
+ return toolResult({ leadGroup: compactLeadGroup(source.leadGroup) });
99
+ }
100
+ catch (error) {
101
+ return handleToolError(error);
102
+ }
103
+ });
67
104
  server.tool("discover_leadify_context", "Safe zero-ID entry point for Leadify. With no organization_id, lists accessible organizations " +
68
105
  "and never chooses one when several are available. With an explicit organization_id, returns " +
69
106
  "a compact list of its lead groups. Use the returned lead_group_id with lead, campaign, persona " +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agifyai/leadify-mcp",
3
- "version": "8.0.0",
3
+ "version": "8.1.1",
4
4
  "description": "MCP server for Leadify lead management API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",