Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/erp/bling/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ beforeEach(async () => {
});

describe("mcp-bling", () => {
it("should register 28 tools", async () => {
it("should register 34 tools", async () => {
const result = await listToolsHandler();
expect(result.tools).toHaveLength(28);
expect(result.tools).toHaveLength(34);
});

it("should call correct API endpoint for list_products", async () => {
Expand Down
166 changes: 166 additions & 0 deletions packages/erp/bling/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
* - list_categories, create_category
* Sales
* - list_orders, create_order
* Proposals
* - list_proposals, get_proposal, create_proposal
* - update_proposal, delete_proposal, update_proposal_status
* Purchasing
* - list_purchase_orders, create_purchase_order
* Contacts
Expand Down Expand Up @@ -528,6 +531,129 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
required: ["webhookId"],
},
},

// ---------------- Proposals ----------------
{
name: "list_proposals",
description: "List commercial proposals (propostas comerciais / orçamentos) in Bling",
inputSchema: {
type: "object",
properties: {
page: { type: "number", description: "Page number" },
limit: { type: "number", description: "Items per page" },
situacao: { type: "string", description: "Filter by status (e.g., Rascunho, Aguardando, Aprovado, Concluído)" },
expandContact: { type: "boolean", description: "If true, resolve contact names inline (default: false)" },
},
},
},
{
name: "get_proposal",
description: "Get a single commercial proposal by ID",
inputSchema: {
type: "object",
properties: {
proposalId: { type: "number", description: "Proposal ID" },
},
required: ["proposalId"],
},
},
{
name: "create_proposal",
description: "Create a commercial proposal (proposta comercial / orçamento)",
inputSchema: {
type: "object",
properties: {
contato: { type: "object", description: "Customer contact", properties: { id: { type: "number" } } },
itens: {
type: "array", description: "Proposal items",
items: {
type: "object",
properties: {
produto: { type: "object", properties: { id: { type: "number" } } },
quantidade: { type: "number" },
valor: { type: "number" },
desconto: { type: "number" },
},
},
},
parcelas: {
type: "array", description: "Payment installments",
items: {
type: "object",
properties: {
dataVencimento: { type: "string" },
valor: { type: "number" },
formaPagamento: { type: "object", properties: { id: { type: "number" } } },
},
},
},
observacoes: { type: "string", description: "Notes" },
desconto: { type: "number", description: "Discount" },
data: { type: "string", description: "Proposal date (YYYY-MM-DD)" },
},
required: ["itens", "parcelas"],
},
},
{
name: "update_proposal",
description: "Update an existing commercial proposal",
inputSchema: {
type: "object",
properties: {
proposalId: { type: "number", description: "Proposal ID" },
contato: { type: "object", description: "Customer contact", properties: { id: { type: "number" } } },
itens: {
type: "array", description: "Proposal items",
items: {
type: "object",
properties: {
produto: { type: "object", properties: { id: { type: "number" } } },
quantidade: { type: "number" },
valor: { type: "number" },
desconto: { type: "number" },
},
},
},
parcelas: {
type: "array", description: "Payment installments",
items: {
type: "object",
properties: {
dataVencimento: { type: "string" },
valor: { type: "number" },
formaPagamento: { type: "object", properties: { id: { type: "number" } } },
},
},
},
observacoes: { type: "string", description: "Notes" },
desconto: { type: "number", description: "Discount" },
},
required: ["proposalId"],
},
},
{
name: "delete_proposal",
description: "Delete a commercial proposal",
inputSchema: {
type: "object",
properties: {
proposalId: { type: "number", description: "Proposal ID" },
},
required: ["proposalId"],
},
},
{
name: "update_proposal_status",
description: "Update proposal status (e.g., Rascunho, Aguardando, Aprovado, Concluído, Cancelado)",
inputSchema: {
type: "object",
properties: {
proposalId: { type: "number", description: "Proposal ID" },
situacao: { type: "string", description: "New status (e.g., Rascunho, Aguardando, Aprovado, Concluído, Cancelado)" },
},
required: ["proposalId", "situacao"],
},
},
],
}));

Expand Down Expand Up @@ -688,6 +814,46 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
case "unsubscribe_webhook":
return text(await blingRequest("DELETE", `/notificacoes/${a.webhookId}`));

// ---- Proposals ----
case "list_proposals": {
const p = new URLSearchParams();
if (a.page) p.set("pagina", String(a.page));
if (a.limit) p.set("limite", String(a.limit));
if (a.situacao) p.set("situacao", String(a.situacao));
const result: any = await blingRequest("GET", `/propostas-comerciais?${p}`);
// Expand contact names if requested
if (a.expandContact === true && result?.data?.length > 0) {
const rawIds = result.data.map((prop: any) => prop.contato?.id).filter((id: any) => id != null);
const contactIds = Array.from(new Set(rawIds)) as number[];
const contacts: Record<number, string> = {};
for (const id of contactIds) {
try {
const c: any = await blingRequest("GET", `/contatos/${id}`);
if (c?.data?.nome) contacts[id] = c.data.nome;
} catch { /* skip failed contact lookups */ }
}
for (const prop of result.data) {
const cid = prop.contato?.id;
if (cid && contacts[cid]) prop.contato.nome = contacts[cid];
}
}
return text(result);
}
case "get_proposal":
return text(await blingRequest("GET", `/propostas-comerciais/${a.proposalId}`));
case "create_proposal":
return text(await blingRequest("POST", "/propostas-comerciais", a));
case "update_proposal": {
const { proposalId, ...rest } = a;
return text(await blingRequest("PUT", `/propostas-comerciais/${proposalId}`, rest));
}
case "delete_proposal":
return text(await blingRequest("DELETE", `/propostas-comerciais/${a.proposalId}`));
case "update_proposal_status": {
const { proposalId, situacao, ...rest } = a;
return text(await blingRequest("PATCH", `/propostas-comerciais/${proposalId}/situacoes`, { situacao }));
}

default:
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
}
Expand Down
Loading