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.
@@ -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;
@@ -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 = 1100; // ~1 req/s to avoid 429
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 res = await undiciFetch(url, {
31
- method: options.method ?? "GET",
32
- headers: authHeaders(token),
33
- body: options.body,
34
- });
35
- if (res.status === 429 || (res.status >= 500 && res.status < 600)) {
36
- throw new Error(`HTTP ${res.status}`);
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
- if (!res.ok) {
39
- const text = await res.text();
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
- /** Busca movimentações em janelas de 30 dias (limite da API) com paginação */
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
- const server = new McpServer({ name: "mcp-agendor", version: "1.0.0" });
16
- server.registerTool("funnels_list", { title: "List funnels", description: "Lista funis e etapas", inputSchema: z.object({}) }, () => funnelsList());
17
- server.registerTool("users_list", { title: "List users", description: "Lista usuarios", inputSchema: z.object({}) }, () => usersList());
18
- server.registerTool("deals_list", { title: "List deals", description: "Lista negocios", inputSchema: schemas.dealsListSchema }, (a) => dealsList(a));
19
- server.registerTool("deals_get", { title: "Get deal", description: "Negocio por ID", inputSchema: schemas.dealsGetSchema }, (a) => dealsGet(a));
20
- server.registerTool("deals_timeline", { title: "Deal timeline", description: "Movimentacoes e tarefas", inputSchema: schemas.dealsTimelineSchema }, (a) => dealsTimeline(a));
21
- server.registerTool("pipeline_overview", { title: "Pipeline overview", description: "Snapshot e distribuicao", inputSchema: schemas.pipelineOverviewSchema }, (a) => pipelineOverview(a));
22
- server.registerTool("pipeline_stage_metrics", { title: "Stage metrics", description: "Metricas por coluna", inputSchema: schemas.pipelineStageMetricsSchema }, (a) => pipelineStageMetrics(a));
23
- server.registerTool("pipeline_trends", { title: "Pipeline trends", description: "Novos ganhos perdidos por periodo", inputSchema: schemas.pipelineTrendsSchema }, (a) => pipelineTrends(a));
24
- server.registerTool("pipeline_aging", { title: "Pipeline aging", description: "Parados e ranking risco", inputSchema: schemas.pipelineAgingSchema }, (a) => pipelineAging(a));
25
- server.registerTool("pipeline_rep_performance", { title: "Rep performance", description: "Por responsavel", inputSchema: schemas.pipelineRepPerformanceSchema }, (a) => pipelineRepPerformance(a));
26
- server.registerTool("pipeline_report", { title: "Pipeline full report", description: "Relatório completo: leads por status, valor em análise (forecast), valores ganhos/perdidos, tempo médio por coluna", inputSchema: schemas.pipelineReportSchema }, (a) => pipelineReport(a));
27
- const transport = new StdioServerTransport();
28
- await server.connect(transport);
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
+ }
@@ -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.ZodOptional<z.ZodObject<{
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.ZodOptional<z.ZodObject<{
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.ZodOptional<z.ZodObject<{
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, {
@@ -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(true),
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({ withCustomFields: args.include_value }),
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() - 180);
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, 180);
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
- // mantém tempo_medio_por_coluna
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({ withCustomFields: true }),
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 deals = await client.getAllDeals({ withCustomFields: false });
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
- ? (await client.getFunnels()).find((f) => f.id === args.pipeline_id)
11
- : (await client.getFunnels())[0];
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-agendor",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "MCP server for Agendor CRM - pipeline metrics and commercial management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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 = 1100; // ~1 req/s to avoid 429
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 res = await undiciFetch(url, {
49
- method: options.method ?? "GET",
50
- headers: authHeaders(token),
51
- body: options.body,
52
- });
53
- if (res.status === 429 || (res.status >= 500 && res.status < 600)) {
54
- throw new Error(`HTTP ${res.status}`);
55
- }
56
- if (!res.ok) {
57
- const text = await res.text();
58
- throw new Error(`Agendor API ${res.status}: ${text.slice(0, 200)}`);
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
- /** Busca movimentações em janelas de 30 dias (limite da API) com paginação */
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
- const server = new McpServer({ name: "mcp-agendor", version: "1.0.0" });
18
+ type ToolResult = { content: Array<{ type: "text"; text: string }> };
17
19
 
18
- server.registerTool("funnels_list", { title: "List funnels", description: "Lista funis e etapas", inputSchema: z.object({}) }, () => funnelsList());
19
- server.registerTool("users_list", { title: "List users", description: "Lista usuarios", inputSchema: z.object({}) }, () => usersList());
20
- server.registerTool("deals_list", { title: "List deals", description: "Lista negocios", inputSchema: schemas.dealsListSchema }, (a) => dealsList(a));
21
- server.registerTool("deals_get", { title: "Get deal", description: "Negocio por ID", inputSchema: schemas.dealsGetSchema }, (a) => dealsGet(a));
22
- server.registerTool("deals_timeline", { title: "Deal timeline", description: "Movimentacoes e tarefas", inputSchema: schemas.dealsTimelineSchema }, (a) => dealsTimeline(a));
23
- server.registerTool("pipeline_overview", { title: "Pipeline overview", description: "Snapshot e distribuicao", inputSchema: schemas.pipelineOverviewSchema }, (a) => pipelineOverview(a));
24
- server.registerTool("pipeline_stage_metrics", { title: "Stage metrics", description: "Metricas por coluna", inputSchema: schemas.pipelineStageMetricsSchema }, (a) => pipelineStageMetrics(a));
25
- server.registerTool("pipeline_trends", { title: "Pipeline trends", description: "Novos ganhos perdidos por periodo", inputSchema: schemas.pipelineTrendsSchema }, (a) => pipelineTrends(a));
26
- server.registerTool("pipeline_aging", { title: "Pipeline aging", description: "Parados e ranking risco", inputSchema: schemas.pipelineAgingSchema }, (a) => pipelineAging(a));
27
- server.registerTool("pipeline_rep_performance", { title: "Rep performance", description: "Por responsavel", inputSchema: schemas.pipelineRepPerformanceSchema }, (a) => pipelineRepPerformance(a));
28
- server.registerTool("pipeline_report", { title: "Pipeline full report", description: "Relatório completo: leads por status, valor em análise (forecast), valores ganhos/perdidos, tempo médio por coluna", inputSchema: schemas.pipelineReportSchema }, (a) => pipelineReport(a));
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 transport = new StdioServerTransport();
31
- await server.connect(transport);
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
+ }
@@ -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
- .object({
6
- start: z.string().datetime().or(z.string().regex(/^\d{4}-\d{2}-\d{2}$/)),
7
- end: z.string().datetime().or(z.string().regex(/^\d{4}-\d{2}-\d{2}$/)),
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(true),
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({ withCustomFields: args.include_value }),
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() - 180);
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, 180);
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
- // mantém tempo_medio_por_coluna
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({ withCustomFields: true }),
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 deals = await client.getAllDeals({ withCustomFields: false });
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
- ? (await client.getFunnels()).find((f) => f.id === args.pipeline_id)
19
- : (await client.getFunnels())[0];
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))