mcp-agendor 1.0.2 → 1.1.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.
- package/dist/agendor/client.d.ts +0 -1
- package/dist/agendor/client.js +27 -17
- package/dist/index.js +46 -14
- package/dist/schemas/inputs.d.ts +6 -6
- package/dist/schemas/inputs.js +6 -8
- package/dist/tools/pipeline-overview.js +4 -1
- package/dist/tools/pipeline-report.js +8 -5
- package/dist/tools/pipeline-trends.js +9 -3
- package/package.json +1 -1
- package/src/agendor/client.ts +27 -18
- package/src/index.ts +48 -14
- package/src/schemas/inputs.ts +8 -10
- package/src/tools/pipeline-overview.ts +3 -1
- package/src/tools/pipeline-report.ts +7 -5
- package/src/tools/pipeline-trends.ts +8 -3
package/dist/agendor/client.d.ts
CHANGED
|
@@ -30,7 +30,6 @@ export declare class AgendorClient {
|
|
|
30
30
|
page?: number;
|
|
31
31
|
per_page?: number;
|
|
32
32
|
}): Promise<unknown[]>;
|
|
33
|
-
/** Busca movimentações em janelas de 30 dias (limite da API) com paginação */
|
|
34
33
|
getAllDealsMovementsHistory(since: string, daysTotal?: number): Promise<unknown[]>;
|
|
35
34
|
getTasks(params?: {
|
|
36
35
|
deal_id?: number;
|
package/dist/agendor/client.js
CHANGED
|
@@ -7,7 +7,9 @@ import pRetry from "p-retry";
|
|
|
7
7
|
import { fetch as undiciFetch } from "undici";
|
|
8
8
|
const DEFAULT_BASE = "https://api.agendor.com.br/v3";
|
|
9
9
|
const PER_PAGE = 50;
|
|
10
|
-
const RATE_LIMIT_MIN_TIME =
|
|
10
|
+
const RATE_LIMIT_MIN_TIME = 400;
|
|
11
|
+
const REQUEST_TIMEOUT_MS = 15_000;
|
|
12
|
+
const MAX_PAGES = 20;
|
|
11
13
|
function getConfig() {
|
|
12
14
|
const token = process.env.AGENDOR_TOKEN;
|
|
13
15
|
const baseUrl = process.env.AGENDOR_BASE_URL ?? DEFAULT_BASE;
|
|
@@ -27,19 +29,27 @@ function authHeaders(token) {
|
|
|
27
29
|
const limiter = new Bottleneck({ minTime: RATE_LIMIT_MIN_TIME });
|
|
28
30
|
async function request(url, token, options = {}) {
|
|
29
31
|
const run = () => limiter.schedule(async () => {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
const ac = new AbortController();
|
|
33
|
+
const timer = setTimeout(() => ac.abort(), REQUEST_TIMEOUT_MS);
|
|
34
|
+
try {
|
|
35
|
+
const res = await undiciFetch(url, {
|
|
36
|
+
method: options.method ?? "GET",
|
|
37
|
+
headers: authHeaders(token),
|
|
38
|
+
body: options.body,
|
|
39
|
+
signal: ac.signal,
|
|
40
|
+
});
|
|
41
|
+
if (res.status === 429 || (res.status >= 500 && res.status < 600)) {
|
|
42
|
+
throw new Error(`HTTP ${res.status}`);
|
|
43
|
+
}
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
const text = await res.text();
|
|
46
|
+
throw new Error(`Agendor API ${res.status}: ${text.slice(0, 200)}`);
|
|
47
|
+
}
|
|
48
|
+
return res.json();
|
|
37
49
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
throw new Error(`Agendor API ${res.status}: ${text.slice(0, 200)}`);
|
|
50
|
+
finally {
|
|
51
|
+
clearTimeout(timer);
|
|
41
52
|
}
|
|
42
|
-
return res.json();
|
|
43
53
|
});
|
|
44
54
|
return pRetry(run, {
|
|
45
55
|
retries: 3,
|
|
@@ -146,7 +156,7 @@ export class AgendorClient {
|
|
|
146
156
|
for (;;) {
|
|
147
157
|
const list = await this.getDeals({ ...params, page, per_page: PER_PAGE });
|
|
148
158
|
all.push(...list);
|
|
149
|
-
if (list.length < PER_PAGE)
|
|
159
|
+
if (list.length < PER_PAGE || page >= MAX_PAGES)
|
|
150
160
|
break;
|
|
151
161
|
page += 1;
|
|
152
162
|
}
|
|
@@ -186,12 +196,12 @@ export class AgendorClient {
|
|
|
186
196
|
const data = await request(url, this.token);
|
|
187
197
|
return Array.isArray(data) ? data : (data.data ?? []);
|
|
188
198
|
}
|
|
189
|
-
catch {
|
|
199
|
+
catch (e) {
|
|
200
|
+
console.error(`[agendor] getDealsMovementsHistory failed: ${e instanceof Error ? e.message : e}`);
|
|
190
201
|
return [];
|
|
191
202
|
}
|
|
192
203
|
}
|
|
193
|
-
|
|
194
|
-
async getAllDealsMovementsHistory(since, daysTotal = 180) {
|
|
204
|
+
async getAllDealsMovementsHistory(since, daysTotal = 90) {
|
|
195
205
|
const all = [];
|
|
196
206
|
const windowDays = 30;
|
|
197
207
|
let sinceDate = new Date(since);
|
|
@@ -210,7 +220,7 @@ export class AgendorClient {
|
|
|
210
220
|
per_page: PER_PAGE,
|
|
211
221
|
});
|
|
212
222
|
all.push(...list);
|
|
213
|
-
if (list.length < PER_PAGE)
|
|
223
|
+
if (list.length < PER_PAGE || page >= MAX_PAGES)
|
|
214
224
|
break;
|
|
215
225
|
page += 1;
|
|
216
226
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
|
+
import { createServer } from "node:http";
|
|
4
6
|
import { z } from "zod";
|
|
5
7
|
import { funnelsList } from "./tools/funnels.js";
|
|
6
8
|
import { usersList } from "./tools/users.js";
|
|
@@ -12,17 +14,47 @@ import { pipelineAging } from "./tools/pipeline-aging.js";
|
|
|
12
14
|
import { pipelineRepPerformance } from "./tools/pipeline-rep-performance.js";
|
|
13
15
|
import { pipelineReport } from "./tools/pipeline-report.js";
|
|
14
16
|
import * as schemas from "./schemas/inputs.js";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
17
|
+
function safe(name, fn) {
|
|
18
|
+
return async (args) => {
|
|
19
|
+
try {
|
|
20
|
+
return await fn(args);
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
24
|
+
console.error(`[mcp-agendor] ${name} failed: ${msg}`);
|
|
25
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: msg }) }] };
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const server = new McpServer({ name: "mcp-agendor", version: "1.1.0" });
|
|
30
|
+
server.registerTool("funnels_list", { title: "List funnels", description: "Lista funis e etapas", inputSchema: z.object({}) }, safe("funnels_list", () => funnelsList()));
|
|
31
|
+
server.registerTool("users_list", { title: "List users", description: "Lista usuarios", inputSchema: z.object({}) }, safe("users_list", () => usersList()));
|
|
32
|
+
server.registerTool("deals_list", { title: "List deals", description: "Lista negocios", inputSchema: schemas.dealsListSchema }, safe("deals_list", (a) => dealsList(a)));
|
|
33
|
+
server.registerTool("deals_get", { title: "Get deal", description: "Negocio por ID", inputSchema: schemas.dealsGetSchema }, safe("deals_get", (a) => dealsGet(a)));
|
|
34
|
+
server.registerTool("deals_timeline", { title: "Deal timeline", description: "Movimentacoes e tarefas", inputSchema: schemas.dealsTimelineSchema }, safe("deals_timeline", (a) => dealsTimeline(a)));
|
|
35
|
+
server.registerTool("pipeline_overview", { title: "Pipeline overview", description: "Snapshot e distribuicao", inputSchema: schemas.pipelineOverviewSchema }, safe("pipeline_overview", (a) => pipelineOverview(a)));
|
|
36
|
+
server.registerTool("pipeline_stage_metrics", { title: "Stage metrics", description: "Metricas por coluna", inputSchema: schemas.pipelineStageMetricsSchema }, safe("pipeline_stage_metrics", (a) => pipelineStageMetrics(a)));
|
|
37
|
+
server.registerTool("pipeline_trends", { title: "Pipeline trends", description: "Novos ganhos perdidos por periodo", inputSchema: schemas.pipelineTrendsSchema }, safe("pipeline_trends", (a) => pipelineTrends(a)));
|
|
38
|
+
server.registerTool("pipeline_aging", { title: "Pipeline aging", description: "Parados e ranking risco", inputSchema: schemas.pipelineAgingSchema }, safe("pipeline_aging", (a) => pipelineAging(a)));
|
|
39
|
+
server.registerTool("pipeline_rep_performance", { title: "Rep performance", description: "Por responsavel", inputSchema: schemas.pipelineRepPerformanceSchema }, safe("pipeline_rep_performance", (a) => pipelineRepPerformance(a)));
|
|
40
|
+
server.registerTool("pipeline_report", { title: "Pipeline full report", description: "Relatorio completo: leads por status, valor em analise (forecast), valores ganhos/perdidos, tempo medio por coluna", inputSchema: schemas.pipelineReportSchema }, safe("pipeline_report", (a) => pipelineReport(a)));
|
|
41
|
+
const port = parseInt(process.env.PORT ?? "", 10);
|
|
42
|
+
if (port > 0) {
|
|
43
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
44
|
+
const httpServer = createServer((req, res) => {
|
|
45
|
+
if (req.url === "/health") {
|
|
46
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
47
|
+
res.end(JSON.stringify({ status: "ok" }));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
transport.handleRequest(req, res);
|
|
51
|
+
});
|
|
52
|
+
await server.connect(transport);
|
|
53
|
+
httpServer.listen(port, () => {
|
|
54
|
+
console.error(`[mcp-agendor] HTTP transport listening on port ${port}`);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
const transport = new StdioServerTransport();
|
|
59
|
+
await server.connect(transport);
|
|
60
|
+
}
|
package/dist/schemas/inputs.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export declare const pipelineIdSchema: z.ZodOptional<z.ZodNumber>;
|
|
3
|
-
export declare const rangeSchema: z.
|
|
3
|
+
export declare const rangeSchema: z.ZodObject<{
|
|
4
4
|
start: z.ZodUnion<[z.ZodString, z.ZodString]>;
|
|
5
5
|
end: z.ZodUnion<[z.ZodString, z.ZodString]>;
|
|
6
6
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -9,7 +9,7 @@ export declare const rangeSchema: z.ZodOptional<z.ZodObject<{
|
|
|
9
9
|
}, {
|
|
10
10
|
start: string;
|
|
11
11
|
end: string;
|
|
12
|
-
}
|
|
12
|
+
}>;
|
|
13
13
|
export declare const timezoneSchema: z.ZodDefault<z.ZodString>;
|
|
14
14
|
export declare const groupBySchema: z.ZodOptional<z.ZodEnum<["day", "week", "month", "quarter"]>>;
|
|
15
15
|
export declare const ownerIdSchema: z.ZodOptional<z.ZodNumber>;
|
|
@@ -81,7 +81,7 @@ export declare const pipelineStageMetricsSchema: z.ZodObject<{
|
|
|
81
81
|
}>;
|
|
82
82
|
export declare const pipelineTrendsSchema: z.ZodObject<{
|
|
83
83
|
pipeline_id: z.ZodOptional<z.ZodNumber>;
|
|
84
|
-
range: z.ZodOptional<z.
|
|
84
|
+
range: z.ZodOptional<z.ZodObject<{
|
|
85
85
|
start: z.ZodUnion<[z.ZodString, z.ZodString]>;
|
|
86
86
|
end: z.ZodUnion<[z.ZodString, z.ZodString]>;
|
|
87
87
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -90,7 +90,7 @@ export declare const pipelineTrendsSchema: z.ZodObject<{
|
|
|
90
90
|
}, {
|
|
91
91
|
start: string;
|
|
92
92
|
end: string;
|
|
93
|
-
}
|
|
93
|
+
}>>;
|
|
94
94
|
group_by: z.ZodOptional<z.ZodEnum<["day", "week", "month", "quarter"]>>;
|
|
95
95
|
owner_id: z.ZodOptional<z.ZodNumber>;
|
|
96
96
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -154,7 +154,7 @@ export declare const pipelineRepPerformanceSchema: z.ZodObject<{
|
|
|
154
154
|
export declare const pipelineReportSchema: z.ZodObject<{
|
|
155
155
|
pipeline_id: z.ZodOptional<z.ZodNumber>;
|
|
156
156
|
todos_os_funis: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
157
|
-
range: z.ZodOptional<z.
|
|
157
|
+
range: z.ZodOptional<z.ZodObject<{
|
|
158
158
|
start: z.ZodUnion<[z.ZodString, z.ZodString]>;
|
|
159
159
|
end: z.ZodUnion<[z.ZodString, z.ZodString]>;
|
|
160
160
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -163,7 +163,7 @@ export declare const pipelineReportSchema: z.ZodObject<{
|
|
|
163
163
|
}, {
|
|
164
164
|
start: string;
|
|
165
165
|
end: string;
|
|
166
|
-
}
|
|
166
|
+
}>>;
|
|
167
167
|
owner_id: z.ZodOptional<z.ZodNumber>;
|
|
168
168
|
include_tempo_medio: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
169
169
|
}, "strip", z.ZodTypeAny, {
|
package/dist/schemas/inputs.js
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export const pipelineIdSchema = z.number().int().positive().optional();
|
|
3
|
-
export const rangeSchema = z
|
|
4
|
-
.object({
|
|
3
|
+
export const rangeSchema = z.object({
|
|
5
4
|
start: z.string().datetime().or(z.string().regex(/^\d{4}-\d{2}-\d{2}$/)),
|
|
6
5
|
end: z.string().datetime().or(z.string().regex(/^\d{4}-\d{2}-\d{2}$/)),
|
|
7
|
-
})
|
|
8
|
-
.optional();
|
|
6
|
+
});
|
|
9
7
|
export const timezoneSchema = z.string().default("America/Sao_Paulo");
|
|
10
8
|
export const groupBySchema = z.enum(["day", "week", "month", "quarter"]).optional();
|
|
11
9
|
export const ownerIdSchema = z.number().int().positive().optional();
|
|
12
10
|
export const includeValueSchema = z.boolean().optional().default(true);
|
|
13
11
|
export const pipelineOverviewSchema = z.object({
|
|
14
12
|
pipeline_id: pipelineIdSchema,
|
|
15
|
-
range: rangeSchema,
|
|
13
|
+
range: rangeSchema.optional(),
|
|
16
14
|
timezone: timezoneSchema,
|
|
17
15
|
group_by: groupBySchema,
|
|
18
16
|
owner_id: ownerIdSchema,
|
|
@@ -20,7 +18,7 @@ export const pipelineOverviewSchema = z.object({
|
|
|
20
18
|
});
|
|
21
19
|
export const pipelineStageMetricsSchema = z.object({
|
|
22
20
|
pipeline_id: pipelineIdSchema,
|
|
23
|
-
range: rangeSchema,
|
|
21
|
+
range: rangeSchema.optional(),
|
|
24
22
|
owner_id: ownerIdSchema,
|
|
25
23
|
});
|
|
26
24
|
export const pipelineTrendsSchema = z.object({
|
|
@@ -36,7 +34,7 @@ export const pipelineAgingSchema = z.object({
|
|
|
36
34
|
});
|
|
37
35
|
export const pipelineRepPerformanceSchema = z.object({
|
|
38
36
|
pipeline_id: pipelineIdSchema,
|
|
39
|
-
range: rangeSchema,
|
|
37
|
+
range: rangeSchema.optional(),
|
|
40
38
|
owner_id: ownerIdSchema,
|
|
41
39
|
});
|
|
42
40
|
export const pipelineReportSchema = z.object({
|
|
@@ -44,7 +42,7 @@ export const pipelineReportSchema = z.object({
|
|
|
44
42
|
todos_os_funis: z.boolean().optional().default(true),
|
|
45
43
|
range: rangeSchema.optional(),
|
|
46
44
|
owner_id: ownerIdSchema,
|
|
47
|
-
include_tempo_medio: z.boolean().optional().default(
|
|
45
|
+
include_tempo_medio: z.boolean().optional().default(false),
|
|
48
46
|
});
|
|
49
47
|
export const dealsListSchema = z.object({
|
|
50
48
|
pipeline_id: pipelineIdSchema,
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { getAgendorClient } from "../agendor/client.js";
|
|
2
2
|
export async function pipelineOverview(args) {
|
|
3
3
|
const client = getAgendorClient();
|
|
4
|
+
const dealOpts = { withCustomFields: args.include_value };
|
|
5
|
+
if (args.range?.start)
|
|
6
|
+
dealOpts.created_after = args.range.start;
|
|
4
7
|
const [funnels, deals] = await Promise.all([
|
|
5
8
|
client.getFunnels(),
|
|
6
|
-
client.getAllDeals(
|
|
9
|
+
client.getAllDeals(dealOpts),
|
|
7
10
|
]);
|
|
8
11
|
const pipelineId = args.pipeline_id;
|
|
9
12
|
const funnel = pipelineId ? funnels.find((f) => f.id === pipelineId) : funnels[0];
|
|
@@ -120,10 +120,10 @@ async function buildFunnelReport(funnel, deals, args, client) {
|
|
|
120
120
|
}));
|
|
121
121
|
if (args.include_tempo_medio && stages.length > 0) {
|
|
122
122
|
const since = new Date();
|
|
123
|
-
since.setDate(since.getDate() -
|
|
123
|
+
since.setDate(since.getDate() - 90);
|
|
124
124
|
const sinceStr = since.toISOString().slice(0, 19) + "Z";
|
|
125
125
|
try {
|
|
126
|
-
const raw = await client.getAllDealsMovementsHistory(sinceStr,
|
|
126
|
+
const raw = await client.getAllDealsMovementsHistory(sinceStr, 90);
|
|
127
127
|
const arr = Array.isArray(raw) ? raw : [];
|
|
128
128
|
const stageDays = {};
|
|
129
129
|
for (const s of stages)
|
|
@@ -164,8 +164,8 @@ async function buildFunnelReport(funnel, deals, args, client) {
|
|
|
164
164
|
};
|
|
165
165
|
});
|
|
166
166
|
}
|
|
167
|
-
catch {
|
|
168
|
-
|
|
167
|
+
catch (e) {
|
|
168
|
+
console.error(`[agendor] tempo_medio fetch failed: ${e instanceof Error ? e.message : e}`);
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
const out = {
|
|
@@ -185,9 +185,12 @@ async function buildFunnelReport(funnel, deals, args, client) {
|
|
|
185
185
|
}
|
|
186
186
|
export async function pipelineReport(args) {
|
|
187
187
|
const client = getAgendorClient();
|
|
188
|
+
const dealOpts = { withCustomFields: true };
|
|
189
|
+
if (args.range?.start)
|
|
190
|
+
dealOpts.created_after = args.range.start;
|
|
188
191
|
const [funnels, deals] = await Promise.all([
|
|
189
192
|
client.getFunnels(),
|
|
190
|
-
client.getAllDeals(
|
|
193
|
+
client.getAllDeals(dealOpts),
|
|
191
194
|
]);
|
|
192
195
|
const pipelineId = args.pipeline_id;
|
|
193
196
|
const todosOsFunis = args.todos_os_funis ?? false;
|
|
@@ -5,10 +5,16 @@ function parseDate(s) {
|
|
|
5
5
|
}
|
|
6
6
|
export async function pipelineTrends(args) {
|
|
7
7
|
const client = getAgendorClient();
|
|
8
|
-
const
|
|
8
|
+
const dealOpts = { withCustomFields: false };
|
|
9
|
+
if (args.range?.start)
|
|
10
|
+
dealOpts.created_after = args.range.start;
|
|
11
|
+
const [deals, funnels] = await Promise.all([
|
|
12
|
+
client.getAllDeals(dealOpts),
|
|
13
|
+
client.getFunnels(),
|
|
14
|
+
]);
|
|
9
15
|
const funnel = args.pipeline_id
|
|
10
|
-
?
|
|
11
|
-
:
|
|
16
|
+
? funnels.find((f) => f.id === args.pipeline_id)
|
|
17
|
+
: funnels[0];
|
|
12
18
|
const stageIds = funnel ? new Set((funnel.stages ?? []).map((s) => s.id)) : new Set();
|
|
13
19
|
const filtered = funnel
|
|
14
20
|
? deals.filter((d) => d.deal_stage_id != null && stageIds.has(d.deal_stage_id))
|
package/package.json
CHANGED
package/src/agendor/client.ts
CHANGED
|
@@ -16,7 +16,9 @@ import type {
|
|
|
16
16
|
|
|
17
17
|
const DEFAULT_BASE = "https://api.agendor.com.br/v3";
|
|
18
18
|
const PER_PAGE = 50;
|
|
19
|
-
const RATE_LIMIT_MIN_TIME =
|
|
19
|
+
const RATE_LIMIT_MIN_TIME = 400;
|
|
20
|
+
const REQUEST_TIMEOUT_MS = 15_000;
|
|
21
|
+
const MAX_PAGES = 20;
|
|
20
22
|
|
|
21
23
|
function getConfig() {
|
|
22
24
|
const token = process.env.AGENDOR_TOKEN;
|
|
@@ -45,19 +47,26 @@ async function request<T>(
|
|
|
45
47
|
): Promise<T> {
|
|
46
48
|
const run = () =>
|
|
47
49
|
limiter.schedule(async () => {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
const ac = new AbortController();
|
|
51
|
+
const timer = setTimeout(() => ac.abort(), REQUEST_TIMEOUT_MS);
|
|
52
|
+
try {
|
|
53
|
+
const res = await undiciFetch(url, {
|
|
54
|
+
method: options.method ?? "GET",
|
|
55
|
+
headers: authHeaders(token),
|
|
56
|
+
body: options.body,
|
|
57
|
+
signal: ac.signal as AbortSignal,
|
|
58
|
+
});
|
|
59
|
+
if (res.status === 429 || (res.status >= 500 && res.status < 600)) {
|
|
60
|
+
throw new Error(`HTTP ${res.status}`);
|
|
61
|
+
}
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
const text = await res.text();
|
|
64
|
+
throw new Error(`Agendor API ${res.status}: ${text.slice(0, 200)}`);
|
|
65
|
+
}
|
|
66
|
+
return res.json() as Promise<T>;
|
|
67
|
+
} finally {
|
|
68
|
+
clearTimeout(timer);
|
|
59
69
|
}
|
|
60
|
-
return res.json() as Promise<T>;
|
|
61
70
|
});
|
|
62
71
|
|
|
63
72
|
return pRetry(run, {
|
|
@@ -174,7 +183,7 @@ export class AgendorClient {
|
|
|
174
183
|
for (;;) {
|
|
175
184
|
const list = await this.getDeals({ ...params, page, per_page: PER_PAGE });
|
|
176
185
|
all.push(...list);
|
|
177
|
-
if (list.length < PER_PAGE) break;
|
|
186
|
+
if (list.length < PER_PAGE || page >= MAX_PAGES) break;
|
|
178
187
|
page += 1;
|
|
179
188
|
}
|
|
180
189
|
return all;
|
|
@@ -219,13 +228,13 @@ export class AgendorClient {
|
|
|
219
228
|
try {
|
|
220
229
|
const data = await request<unknown[] | { data?: unknown[] }>(url, this.token);
|
|
221
230
|
return Array.isArray(data) ? data : (data.data ?? []);
|
|
222
|
-
} catch {
|
|
231
|
+
} catch (e) {
|
|
232
|
+
console.error(`[agendor] getDealsMovementsHistory failed: ${e instanceof Error ? e.message : e}`);
|
|
223
233
|
return [];
|
|
224
234
|
}
|
|
225
235
|
}
|
|
226
236
|
|
|
227
|
-
|
|
228
|
-
async getAllDealsMovementsHistory(since: string, daysTotal = 180): Promise<unknown[]> {
|
|
237
|
+
async getAllDealsMovementsHistory(since: string, daysTotal = 90): Promise<unknown[]> {
|
|
229
238
|
const all: unknown[] = [];
|
|
230
239
|
const windowDays = 30;
|
|
231
240
|
let sinceDate = new Date(since);
|
|
@@ -245,7 +254,7 @@ export class AgendorClient {
|
|
|
245
254
|
per_page: PER_PAGE,
|
|
246
255
|
});
|
|
247
256
|
all.push(...list);
|
|
248
|
-
if (list.length < PER_PAGE) break;
|
|
257
|
+
if (list.length < PER_PAGE || page >= MAX_PAGES) break;
|
|
249
258
|
page += 1;
|
|
250
259
|
}
|
|
251
260
|
sinceDate = windowEnd;
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
|
+
import { createServer } from "node:http";
|
|
4
6
|
import { z } from "zod";
|
|
5
7
|
import { funnelsList } from "./tools/funnels.js";
|
|
6
8
|
import { usersList } from "./tools/users.js";
|
|
@@ -13,19 +15,51 @@ import { pipelineRepPerformance } from "./tools/pipeline-rep-performance.js";
|
|
|
13
15
|
import { pipelineReport } from "./tools/pipeline-report.js";
|
|
14
16
|
import * as schemas from "./schemas/inputs.js";
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
type ToolResult = { content: Array<{ type: "text"; text: string }> };
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
function safe<T>(name: string, fn: (args: T) => Promise<ToolResult>): (args: T) => Promise<ToolResult> {
|
|
21
|
+
return async (args: T) => {
|
|
22
|
+
try {
|
|
23
|
+
return await fn(args);
|
|
24
|
+
} catch (e) {
|
|
25
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
26
|
+
console.error(`[mcp-agendor] ${name} failed: ${msg}`);
|
|
27
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ error: msg }) }] };
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
29
31
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
+
const server = new McpServer({ name: "mcp-agendor", version: "1.1.0" });
|
|
33
|
+
|
|
34
|
+
server.registerTool("funnels_list", { title: "List funnels", description: "Lista funis e etapas", inputSchema: z.object({}) }, safe("funnels_list", () => funnelsList()));
|
|
35
|
+
server.registerTool("users_list", { title: "List users", description: "Lista usuarios", inputSchema: z.object({}) }, safe("users_list", () => usersList()));
|
|
36
|
+
server.registerTool("deals_list", { title: "List deals", description: "Lista negocios", inputSchema: schemas.dealsListSchema }, safe("deals_list", (a) => dealsList(a)));
|
|
37
|
+
server.registerTool("deals_get", { title: "Get deal", description: "Negocio por ID", inputSchema: schemas.dealsGetSchema }, safe("deals_get", (a) => dealsGet(a)));
|
|
38
|
+
server.registerTool("deals_timeline", { title: "Deal timeline", description: "Movimentacoes e tarefas", inputSchema: schemas.dealsTimelineSchema }, safe("deals_timeline", (a) => dealsTimeline(a)));
|
|
39
|
+
server.registerTool("pipeline_overview", { title: "Pipeline overview", description: "Snapshot e distribuicao", inputSchema: schemas.pipelineOverviewSchema }, safe("pipeline_overview", (a) => pipelineOverview(a)));
|
|
40
|
+
server.registerTool("pipeline_stage_metrics", { title: "Stage metrics", description: "Metricas por coluna", inputSchema: schemas.pipelineStageMetricsSchema }, safe("pipeline_stage_metrics", (a) => pipelineStageMetrics(a)));
|
|
41
|
+
server.registerTool("pipeline_trends", { title: "Pipeline trends", description: "Novos ganhos perdidos por periodo", inputSchema: schemas.pipelineTrendsSchema }, safe("pipeline_trends", (a) => pipelineTrends(a)));
|
|
42
|
+
server.registerTool("pipeline_aging", { title: "Pipeline aging", description: "Parados e ranking risco", inputSchema: schemas.pipelineAgingSchema }, safe("pipeline_aging", (a) => pipelineAging(a)));
|
|
43
|
+
server.registerTool("pipeline_rep_performance", { title: "Rep performance", description: "Por responsavel", inputSchema: schemas.pipelineRepPerformanceSchema }, safe("pipeline_rep_performance", (a) => pipelineRepPerformance(a)));
|
|
44
|
+
server.registerTool("pipeline_report", { title: "Pipeline full report", description: "Relatorio completo: leads por status, valor em analise (forecast), valores ganhos/perdidos, tempo medio por coluna", inputSchema: schemas.pipelineReportSchema }, safe("pipeline_report", (a) => pipelineReport(a)));
|
|
45
|
+
|
|
46
|
+
const port = parseInt(process.env.PORT ?? "", 10);
|
|
47
|
+
|
|
48
|
+
if (port > 0) {
|
|
49
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
50
|
+
const httpServer = createServer((req, res) => {
|
|
51
|
+
if (req.url === "/health") {
|
|
52
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
53
|
+
res.end(JSON.stringify({ status: "ok" }));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
transport.handleRequest(req, res);
|
|
57
|
+
});
|
|
58
|
+
await server.connect(transport);
|
|
59
|
+
httpServer.listen(port, () => {
|
|
60
|
+
console.error(`[mcp-agendor] HTTP transport listening on port ${port}`);
|
|
61
|
+
});
|
|
62
|
+
} else {
|
|
63
|
+
const transport = new StdioServerTransport();
|
|
64
|
+
await server.connect(transport);
|
|
65
|
+
}
|
package/src/schemas/inputs.ts
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
3
|
export const pipelineIdSchema = z.number().int().positive().optional();
|
|
4
|
-
export const rangeSchema = z
|
|
5
|
-
.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
})
|
|
9
|
-
.optional();
|
|
4
|
+
export const rangeSchema = z.object({
|
|
5
|
+
start: z.string().datetime().or(z.string().regex(/^\d{4}-\d{2}-\d{2}$/)),
|
|
6
|
+
end: z.string().datetime().or(z.string().regex(/^\d{4}-\d{2}-\d{2}$/)),
|
|
7
|
+
});
|
|
10
8
|
export const timezoneSchema = z.string().default("America/Sao_Paulo");
|
|
11
9
|
export const groupBySchema = z.enum(["day", "week", "month", "quarter"]).optional();
|
|
12
10
|
export const ownerIdSchema = z.number().int().positive().optional();
|
|
@@ -14,7 +12,7 @@ export const includeValueSchema = z.boolean().optional().default(true);
|
|
|
14
12
|
|
|
15
13
|
export const pipelineOverviewSchema = z.object({
|
|
16
14
|
pipeline_id: pipelineIdSchema,
|
|
17
|
-
range: rangeSchema,
|
|
15
|
+
range: rangeSchema.optional(),
|
|
18
16
|
timezone: timezoneSchema,
|
|
19
17
|
group_by: groupBySchema,
|
|
20
18
|
owner_id: ownerIdSchema,
|
|
@@ -23,7 +21,7 @@ export const pipelineOverviewSchema = z.object({
|
|
|
23
21
|
|
|
24
22
|
export const pipelineStageMetricsSchema = z.object({
|
|
25
23
|
pipeline_id: pipelineIdSchema,
|
|
26
|
-
range: rangeSchema,
|
|
24
|
+
range: rangeSchema.optional(),
|
|
27
25
|
owner_id: ownerIdSchema,
|
|
28
26
|
});
|
|
29
27
|
|
|
@@ -42,7 +40,7 @@ export const pipelineAgingSchema = z.object({
|
|
|
42
40
|
|
|
43
41
|
export const pipelineRepPerformanceSchema = z.object({
|
|
44
42
|
pipeline_id: pipelineIdSchema,
|
|
45
|
-
range: rangeSchema,
|
|
43
|
+
range: rangeSchema.optional(),
|
|
46
44
|
owner_id: ownerIdSchema,
|
|
47
45
|
});
|
|
48
46
|
|
|
@@ -51,7 +49,7 @@ export const pipelineReportSchema = z.object({
|
|
|
51
49
|
todos_os_funis: z.boolean().optional().default(true),
|
|
52
50
|
range: rangeSchema.optional(),
|
|
53
51
|
owner_id: ownerIdSchema,
|
|
54
|
-
include_tempo_medio: z.boolean().optional().default(
|
|
52
|
+
include_tempo_medio: z.boolean().optional().default(false),
|
|
55
53
|
});
|
|
56
54
|
|
|
57
55
|
export const dealsListSchema = z.object({
|
|
@@ -7,9 +7,11 @@ export async function pipelineOverview(
|
|
|
7
7
|
args: Args
|
|
8
8
|
): Promise<{ content: Array<{ type: "text"; text: string }> }> {
|
|
9
9
|
const client = getAgendorClient();
|
|
10
|
+
const dealOpts: Record<string, unknown> = { withCustomFields: args.include_value };
|
|
11
|
+
if (args.range?.start) dealOpts.created_after = args.range.start;
|
|
10
12
|
const [funnels, deals] = await Promise.all([
|
|
11
13
|
client.getFunnels(),
|
|
12
|
-
client.getAllDeals(
|
|
14
|
+
client.getAllDeals(dealOpts),
|
|
13
15
|
]);
|
|
14
16
|
const pipelineId = args.pipeline_id;
|
|
15
17
|
const funnel = pipelineId ? funnels.find((f) => f.id === pipelineId) : funnels[0];
|
|
@@ -145,10 +145,10 @@ async function buildFunnelReport(
|
|
|
145
145
|
|
|
146
146
|
if (args.include_tempo_medio && stages.length > 0) {
|
|
147
147
|
const since = new Date();
|
|
148
|
-
since.setDate(since.getDate() -
|
|
148
|
+
since.setDate(since.getDate() - 90);
|
|
149
149
|
const sinceStr = since.toISOString().slice(0, 19) + "Z";
|
|
150
150
|
try {
|
|
151
|
-
const raw = await client.getAllDealsMovementsHistory(sinceStr,
|
|
151
|
+
const raw = await client.getAllDealsMovementsHistory(sinceStr, 90);
|
|
152
152
|
const arr = Array.isArray(raw) ? raw : [];
|
|
153
153
|
type Mov = {
|
|
154
154
|
deal?: { id?: number | string };
|
|
@@ -194,8 +194,8 @@ async function buildFunnelReport(
|
|
|
194
194
|
quantidade_negocios: days.length,
|
|
195
195
|
};
|
|
196
196
|
});
|
|
197
|
-
} catch {
|
|
198
|
-
|
|
197
|
+
} catch (e) {
|
|
198
|
+
console.error(`[agendor] tempo_medio fetch failed: ${e instanceof Error ? e.message : e}`);
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
201
|
|
|
@@ -218,9 +218,11 @@ export async function pipelineReport(
|
|
|
218
218
|
args: Args
|
|
219
219
|
): Promise<{ content: Array<{ type: "text"; text: string }> }> {
|
|
220
220
|
const client = getAgendorClient();
|
|
221
|
+
const dealOpts: Record<string, unknown> = { withCustomFields: true };
|
|
222
|
+
if (args.range?.start) dealOpts.created_after = args.range.start;
|
|
221
223
|
const [funnels, deals] = await Promise.all([
|
|
222
224
|
client.getFunnels(),
|
|
223
|
-
client.getAllDeals(
|
|
225
|
+
client.getAllDeals(dealOpts),
|
|
224
226
|
]);
|
|
225
227
|
const pipelineId = args.pipeline_id;
|
|
226
228
|
const todosOsFunis = args.todos_os_funis ?? false;
|
|
@@ -13,10 +13,15 @@ export async function pipelineTrends(
|
|
|
13
13
|
args: Args
|
|
14
14
|
): Promise<{ content: Array<{ type: "text"; text: string }> }> {
|
|
15
15
|
const client = getAgendorClient();
|
|
16
|
-
const
|
|
16
|
+
const dealOpts: Record<string, unknown> = { withCustomFields: false };
|
|
17
|
+
if (args.range?.start) dealOpts.created_after = args.range.start;
|
|
18
|
+
const [deals, funnels] = await Promise.all([
|
|
19
|
+
client.getAllDeals(dealOpts),
|
|
20
|
+
client.getFunnels(),
|
|
21
|
+
]);
|
|
17
22
|
const funnel = args.pipeline_id
|
|
18
|
-
?
|
|
19
|
-
:
|
|
23
|
+
? funnels.find((f) => f.id === args.pipeline_id)
|
|
24
|
+
: funnels[0];
|
|
20
25
|
const stageIds = funnel ? new Set((funnel.stages ?? []).map((s) => s.id)) : new Set<number>();
|
|
21
26
|
const filtered = funnel
|
|
22
27
|
? deals.filter((d) => d.deal_stage_id != null && stageIds.has(d.deal_stage_id))
|