@quantyapp/quanty-mcp-server 1.0.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/README.md +65 -0
- package/dist/core/analysisService.d.ts +35 -0
- package/dist/core/analysisService.d.ts.map +1 -0
- package/dist/core/analysisService.js +151 -0
- package/dist/core/auth.d.ts +19 -0
- package/dist/core/auth.d.ts.map +1 -0
- package/dist/core/auth.js +42 -0
- package/dist/core/bankService.d.ts +45 -0
- package/dist/core/bankService.d.ts.map +1 -0
- package/dist/core/bankService.js +105 -0
- package/dist/core/budgetService.d.ts +84 -0
- package/dist/core/budgetService.d.ts.map +1 -0
- package/dist/core/budgetService.js +170 -0
- package/dist/core/supabase.d.ts +5 -0
- package/dist/core/supabase.d.ts.map +1 -0
- package/dist/core/supabase.js +12 -0
- package/dist/mcp/index.d.ts +3 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +464 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Quanty MCP Server
|
|
2
|
+
|
|
3
|
+
Servidor MCP que conecta o Claude Desktop ao Quanty.
|
|
4
|
+
|
|
5
|
+
## Instalação
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd quanty-mcp-server
|
|
9
|
+
npm install
|
|
10
|
+
npm run build
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Configuração no Claude Desktop
|
|
14
|
+
|
|
15
|
+
1. Gere seu token no Quanty: **Minha Conta → Conexão MCP → Gerar Token**
|
|
16
|
+
|
|
17
|
+
2. Edite o arquivo `claude_desktop_config.json`:
|
|
18
|
+
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
19
|
+
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
20
|
+
|
|
21
|
+
3. Adicione a configuração:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"mcpServers": {
|
|
26
|
+
"quanty": {
|
|
27
|
+
"command": "node",
|
|
28
|
+
"args": ["C:/Users/romar/OneDrive/Documentos/B2Builder/Quanty/quanty-mcp-server/dist/mcp/index.js"],
|
|
29
|
+
"env": {
|
|
30
|
+
"QUANTY_MCP_TOKEN": "seu-token-aqui",
|
|
31
|
+
"SUPABASE_URL": "https://aimquvsavkffqgctnzym.supabase.co",
|
|
32
|
+
"SUPABASE_SERVICE_ROLE_KEY": "sua-service-key-aqui"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
4. Reinicie o Claude Desktop
|
|
40
|
+
|
|
41
|
+
## Tools Disponíveis
|
|
42
|
+
|
|
43
|
+
### Leitura
|
|
44
|
+
- `quanty_listar_orcamentos` - Lista orçamentos recentes
|
|
45
|
+
- `quanty_abrir_orcamento` - Detalhes de um orçamento
|
|
46
|
+
- `quanty_listar_bancos` - Lista bancos de composição
|
|
47
|
+
- `quanty_buscar_insumo` - Busca insumos em banco
|
|
48
|
+
|
|
49
|
+
### Escrita (com confirmação)
|
|
50
|
+
- `quanty_preparar_orcamento` - Prepara criação
|
|
51
|
+
- `quanty_preparar_item` - Prepara adição de item
|
|
52
|
+
- `quanty_executar` - Confirma operação
|
|
53
|
+
|
|
54
|
+
### Análise
|
|
55
|
+
- `quanty_orcamentos_similares` - Encontra similares
|
|
56
|
+
- `quanty_sugerir_itens` - Sugere itens faltantes
|
|
57
|
+
- `quanty_custo_medio` - Custo médio por unidade
|
|
58
|
+
|
|
59
|
+
## Exemplo de Uso
|
|
60
|
+
|
|
61
|
+
No Claude Desktop, diga:
|
|
62
|
+
- "Liste meus orçamentos no Quanty"
|
|
63
|
+
- "Crie um orçamento chamado 'Ponte Rio Novo'"
|
|
64
|
+
- "Busque 'concreto fck 30' no banco SINAPI"
|
|
65
|
+
- "Sugira itens que podem estar faltando no orçamento X"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { AuthContext } from './auth.js';
|
|
2
|
+
export interface SimilarBudget {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
similarity: number;
|
|
6
|
+
commonItems: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface SuggestedItem {
|
|
9
|
+
description: string;
|
|
10
|
+
unit: string;
|
|
11
|
+
avgUnitCost: number;
|
|
12
|
+
frequency: number;
|
|
13
|
+
reason: string;
|
|
14
|
+
}
|
|
15
|
+
export interface CostMetric {
|
|
16
|
+
driver: string;
|
|
17
|
+
avgCostPerUnit: number;
|
|
18
|
+
minCost: number;
|
|
19
|
+
maxCost: number;
|
|
20
|
+
sampleSize: number;
|
|
21
|
+
p90Cost: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Finds similar budgets based on item descriptions
|
|
25
|
+
*/
|
|
26
|
+
export declare function findSimilarBudgets(auth: AuthContext, budgetId: string): Promise<SimilarBudget[]>;
|
|
27
|
+
/**
|
|
28
|
+
* Suggests items that might be missing based on similar budgets
|
|
29
|
+
*/
|
|
30
|
+
export declare function suggestMissingItems(auth: AuthContext, budgetId: string): Promise<SuggestedItem[]>;
|
|
31
|
+
/**
|
|
32
|
+
* Calculates average cost per unit (e.g., R$/m³ de concreto)
|
|
33
|
+
*/
|
|
34
|
+
export declare function calculateCostPerUnit(auth: AuthContext, category: string, driver: string): Promise<CostMetric>;
|
|
35
|
+
//# sourceMappingURL=analysisService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analysisService.d.ts","sourceRoot":"","sources":["../../src/core/analysisService.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAGxC,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CA+CtG;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CA0DvG;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CACtC,IAAI,EAAE,WAAW,EACjB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACf,OAAO,CAAC,UAAU,CAAC,CAmDrB"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { supabase } from './supabase.js';
|
|
2
|
+
/**
|
|
3
|
+
* Finds similar budgets based on item descriptions
|
|
4
|
+
*/
|
|
5
|
+
export async function findSimilarBudgets(auth, budgetId) {
|
|
6
|
+
// Get target budget
|
|
7
|
+
const { data: targetBudget, error: targetError } = await supabase
|
|
8
|
+
.from('budgets')
|
|
9
|
+
.select('data')
|
|
10
|
+
.eq('id', budgetId)
|
|
11
|
+
.single();
|
|
12
|
+
if (targetError)
|
|
13
|
+
throw new Error(`Failed to get budget: ${targetError.message}`);
|
|
14
|
+
const targetRows = targetBudget?.data?.rows || [];
|
|
15
|
+
const targetDescriptions = new Set(targetRows.map(r => r.description.toLowerCase()));
|
|
16
|
+
// Get all other budgets from tenant
|
|
17
|
+
const { data: allBudgets, error: allError } = await supabase
|
|
18
|
+
.from('budgets')
|
|
19
|
+
.select('id, title, data')
|
|
20
|
+
.eq('tenant_id', auth.tenantId)
|
|
21
|
+
.eq('deleted', false)
|
|
22
|
+
.neq('id', budgetId);
|
|
23
|
+
if (allError)
|
|
24
|
+
throw new Error(`Failed to list budgets: ${allError.message}`);
|
|
25
|
+
const results = [];
|
|
26
|
+
for (const budget of allBudgets || []) {
|
|
27
|
+
const rows = budget.data?.rows || [];
|
|
28
|
+
const commonItems = [];
|
|
29
|
+
for (const row of rows) {
|
|
30
|
+
if (targetDescriptions.has(row.description.toLowerCase())) {
|
|
31
|
+
commonItems.push(row.description);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (commonItems.length > 0) {
|
|
35
|
+
const similarity = Math.round((commonItems.length / Math.max(targetDescriptions.size, rows.length)) * 100);
|
|
36
|
+
results.push({
|
|
37
|
+
id: budget.id,
|
|
38
|
+
title: budget.title,
|
|
39
|
+
similarity,
|
|
40
|
+
commonItems: commonItems.slice(0, 5) // Top 5 common items
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return results.sort((a, b) => b.similarity - a.similarity).slice(0, 10);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Suggests items that might be missing based on similar budgets
|
|
48
|
+
*/
|
|
49
|
+
export async function suggestMissingItems(auth, budgetId) {
|
|
50
|
+
// Get target budget
|
|
51
|
+
const { data: targetBudget, error: targetError } = await supabase
|
|
52
|
+
.from('budgets')
|
|
53
|
+
.select('data')
|
|
54
|
+
.eq('id', budgetId)
|
|
55
|
+
.single();
|
|
56
|
+
if (targetError)
|
|
57
|
+
throw new Error(`Failed to get budget: ${targetError.message}`);
|
|
58
|
+
const targetRows = targetBudget?.data?.rows || [];
|
|
59
|
+
const targetDescriptions = new Set(targetRows.map(r => r.description.toLowerCase()));
|
|
60
|
+
// Get all other budgets
|
|
61
|
+
const { data: allBudgets, error: allError } = await supabase
|
|
62
|
+
.from('budgets')
|
|
63
|
+
.select('data')
|
|
64
|
+
.eq('tenant_id', auth.tenantId)
|
|
65
|
+
.eq('deleted', false)
|
|
66
|
+
.neq('id', budgetId);
|
|
67
|
+
if (allError)
|
|
68
|
+
throw new Error(`Failed to list budgets: ${allError.message}`);
|
|
69
|
+
// Count frequency of items not in target
|
|
70
|
+
const itemFrequency = new Map();
|
|
71
|
+
for (const budget of allBudgets || []) {
|
|
72
|
+
const rows = budget.data?.rows || [];
|
|
73
|
+
for (const row of rows) {
|
|
74
|
+
const descLower = row.description.toLowerCase();
|
|
75
|
+
if (!targetDescriptions.has(descLower)) {
|
|
76
|
+
const existing = itemFrequency.get(descLower) || { count: 0, units: [], costs: [] };
|
|
77
|
+
existing.count++;
|
|
78
|
+
if (row.unit)
|
|
79
|
+
existing.units.push(row.unit);
|
|
80
|
+
if (row.unitCost > 0)
|
|
81
|
+
existing.costs.push(row.unitCost);
|
|
82
|
+
itemFrequency.set(descLower, existing);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Convert to suggestions (items that appear in at least 2 budgets)
|
|
87
|
+
const suggestions = [];
|
|
88
|
+
for (const [desc, data] of itemFrequency) {
|
|
89
|
+
if (data.count >= 2) {
|
|
90
|
+
const avgCost = data.costs.length > 0
|
|
91
|
+
? data.costs.reduce((a, b) => a + b, 0) / data.costs.length
|
|
92
|
+
: 0;
|
|
93
|
+
suggestions.push({
|
|
94
|
+
description: desc,
|
|
95
|
+
unit: data.units[0] || 'un',
|
|
96
|
+
avgUnitCost: avgCost,
|
|
97
|
+
frequency: data.count,
|
|
98
|
+
reason: `Aparece em ${data.count} orçamentos similares`
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return suggestions.sort((a, b) => b.frequency - a.frequency).slice(0, 15);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Calculates average cost per unit (e.g., R$/m³ de concreto)
|
|
106
|
+
*/
|
|
107
|
+
export async function calculateCostPerUnit(auth, category, driver) {
|
|
108
|
+
// Get all budgets
|
|
109
|
+
const { data: allBudgets, error } = await supabase
|
|
110
|
+
.from('budgets')
|
|
111
|
+
.select('data')
|
|
112
|
+
.eq('tenant_id', auth.tenantId)
|
|
113
|
+
.eq('deleted', false);
|
|
114
|
+
if (error)
|
|
115
|
+
throw new Error(`Failed to list budgets: ${error.message}`);
|
|
116
|
+
const driverLower = driver.toLowerCase();
|
|
117
|
+
const ratios = [];
|
|
118
|
+
for (const budget of allBudgets || []) {
|
|
119
|
+
const rows = budget.data?.rows || [];
|
|
120
|
+
// Find driver item
|
|
121
|
+
const driverItem = rows.find(r => r.description.toLowerCase().includes(driverLower));
|
|
122
|
+
if (!driverItem || driverItem.quantity <= 0)
|
|
123
|
+
continue;
|
|
124
|
+
// Calculate total cost of budget
|
|
125
|
+
const totalCost = rows.reduce((sum, r) => sum + (r.quantity * r.unitCost), 0);
|
|
126
|
+
// Calculate cost per unit of driver
|
|
127
|
+
const costPerUnit = totalCost / driverItem.quantity;
|
|
128
|
+
ratios.push(costPerUnit);
|
|
129
|
+
}
|
|
130
|
+
if (ratios.length === 0) {
|
|
131
|
+
return {
|
|
132
|
+
driver,
|
|
133
|
+
avgCostPerUnit: 0,
|
|
134
|
+
minCost: 0,
|
|
135
|
+
maxCost: 0,
|
|
136
|
+
sampleSize: 0,
|
|
137
|
+
p90Cost: 0
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
ratios.sort((a, b) => a - b);
|
|
141
|
+
const avg = ratios.reduce((a, b) => a + b, 0) / ratios.length;
|
|
142
|
+
const p90Index = Math.floor(ratios.length * 0.9);
|
|
143
|
+
return {
|
|
144
|
+
driver,
|
|
145
|
+
avgCostPerUnit: Math.round(avg * 100) / 100,
|
|
146
|
+
minCost: Math.round(ratios[0] * 100) / 100,
|
|
147
|
+
maxCost: Math.round(ratios[ratios.length - 1] * 100) / 100,
|
|
148
|
+
sampleSize: ratios.length,
|
|
149
|
+
p90Cost: Math.round(ratios[p90Index] * 100) / 100
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface AuthContext {
|
|
2
|
+
userId: string;
|
|
3
|
+
tenantId: string;
|
|
4
|
+
userName: string;
|
|
5
|
+
userEmail: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Validates MCP token and returns user context
|
|
9
|
+
*/
|
|
10
|
+
export declare function validateToken(token: string): Promise<AuthContext>;
|
|
11
|
+
/**
|
|
12
|
+
* Gets token from environment
|
|
13
|
+
*/
|
|
14
|
+
export declare function getTokenFromEnv(): string;
|
|
15
|
+
/**
|
|
16
|
+
* Initializes auth context from env token
|
|
17
|
+
*/
|
|
18
|
+
export declare function initAuth(): Promise<AuthContext>;
|
|
19
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/core/auth.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,WAAW;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACrB;AAID;;GAEG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAwBvE;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAMxC;AAED;;GAEG;AACH,wBAAsB,QAAQ,IAAI,OAAO,CAAC,WAAW,CAAC,CAGrD"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { supabase } from './supabase.js';
|
|
2
|
+
let cachedAuth = null;
|
|
3
|
+
/**
|
|
4
|
+
* Validates MCP token and returns user context
|
|
5
|
+
*/
|
|
6
|
+
export async function validateToken(token) {
|
|
7
|
+
if (cachedAuth) {
|
|
8
|
+
return cachedAuth;
|
|
9
|
+
}
|
|
10
|
+
const { data, error } = await supabase.rpc('validate_mcp_token', { p_token: token });
|
|
11
|
+
if (error) {
|
|
12
|
+
throw new Error(`Token validation failed: ${error.message}`);
|
|
13
|
+
}
|
|
14
|
+
if (!data || data.length === 0) {
|
|
15
|
+
throw new Error('Invalid or expired MCP token');
|
|
16
|
+
}
|
|
17
|
+
const user = data[0];
|
|
18
|
+
cachedAuth = {
|
|
19
|
+
userId: user.user_id,
|
|
20
|
+
tenantId: user.tenant_id,
|
|
21
|
+
userName: user.user_name || 'Unknown',
|
|
22
|
+
userEmail: user.user_email || 'unknown@example.com'
|
|
23
|
+
};
|
|
24
|
+
return cachedAuth;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Gets token from environment
|
|
28
|
+
*/
|
|
29
|
+
export function getTokenFromEnv() {
|
|
30
|
+
const token = process.env.QUANTY_MCP_TOKEN;
|
|
31
|
+
if (!token) {
|
|
32
|
+
throw new Error('QUANTY_MCP_TOKEN environment variable not set');
|
|
33
|
+
}
|
|
34
|
+
return token;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Initializes auth context from env token
|
|
38
|
+
*/
|
|
39
|
+
export async function initAuth() {
|
|
40
|
+
const token = getTokenFromEnv();
|
|
41
|
+
return validateToken(token);
|
|
42
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { AuthContext } from './auth.js';
|
|
2
|
+
export interface BankSummary {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
author: string;
|
|
7
|
+
region?: string;
|
|
8
|
+
visibility: 'public' | 'private' | 'subscription';
|
|
9
|
+
categoryCount: number;
|
|
10
|
+
}
|
|
11
|
+
export interface BankItem {
|
|
12
|
+
id: string;
|
|
13
|
+
code: string;
|
|
14
|
+
description: string;
|
|
15
|
+
unit: string;
|
|
16
|
+
unitCost: number;
|
|
17
|
+
itemType?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface BankCategory {
|
|
20
|
+
id: string;
|
|
21
|
+
code?: string;
|
|
22
|
+
name: string;
|
|
23
|
+
itemCount: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Lists available composition banks for the tenant
|
|
27
|
+
*/
|
|
28
|
+
export declare function listBanks(auth: AuthContext): Promise<BankSummary[]>;
|
|
29
|
+
/**
|
|
30
|
+
* Lists categories in a bank
|
|
31
|
+
*/
|
|
32
|
+
export declare function getBankCategories(bankId: string): Promise<BankCategory[]>;
|
|
33
|
+
/**
|
|
34
|
+
* Searches items in a bank by text
|
|
35
|
+
*/
|
|
36
|
+
export declare function searchBankItems(bankId: string, query: string, limit?: number): Promise<BankItem[]>;
|
|
37
|
+
/**
|
|
38
|
+
* Prepares importing an item from bank to budget
|
|
39
|
+
*/
|
|
40
|
+
export declare function prepareImportItem(budgetId: string, bankItem: BankItem): {
|
|
41
|
+
action: string;
|
|
42
|
+
preview: string;
|
|
43
|
+
data: any;
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=bankService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bankService.d.ts","sourceRoot":"","sources":["../../src/core/bankService.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAExC,MAAM,WAAW,WAAW;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,QAAQ,GAAG,SAAS,GAAG,cAAc,CAAC;IAClD,aAAa,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,QAAQ;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAmBzE;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAgB/E;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,GAAE,MAAW,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CA0C5G;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC7B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,QAAQ,GACnB;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,GAAG,CAAA;CAAE,CAchD"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { supabase } from './supabase.js';
|
|
2
|
+
/**
|
|
3
|
+
* Lists available composition banks for the tenant
|
|
4
|
+
*/
|
|
5
|
+
export async function listBanks(auth) {
|
|
6
|
+
// Get public banks, tenant's private banks, and subscribed banks
|
|
7
|
+
const { data, error } = await supabase
|
|
8
|
+
.from('composition_banks')
|
|
9
|
+
.select('id, title, description, author_name, region, visibility, categories, tenant_id')
|
|
10
|
+
.eq('deleted', false)
|
|
11
|
+
.or(`visibility.eq.public,tenant_id.eq.${auth.tenantId}`);
|
|
12
|
+
if (error)
|
|
13
|
+
throw new Error(`Failed to list banks: ${error.message}`);
|
|
14
|
+
return (data || []).map(b => ({
|
|
15
|
+
id: b.id,
|
|
16
|
+
title: b.title,
|
|
17
|
+
description: b.description,
|
|
18
|
+
author: b.author_name || 'Unknown',
|
|
19
|
+
region: b.region,
|
|
20
|
+
visibility: b.visibility,
|
|
21
|
+
categoryCount: (b.categories || []).length
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Lists categories in a bank
|
|
26
|
+
*/
|
|
27
|
+
export async function getBankCategories(bankId) {
|
|
28
|
+
const { data, error } = await supabase
|
|
29
|
+
.from('composition_banks')
|
|
30
|
+
.select('categories')
|
|
31
|
+
.eq('id', bankId)
|
|
32
|
+
.single();
|
|
33
|
+
if (error)
|
|
34
|
+
throw new Error(`Failed to get bank: ${error.message}`);
|
|
35
|
+
const categories = data?.categories || [];
|
|
36
|
+
return categories.map((c) => ({
|
|
37
|
+
id: c.id,
|
|
38
|
+
code: c.code,
|
|
39
|
+
name: c.name,
|
|
40
|
+
itemCount: (c.items || []).length + (c.children || []).reduce((sum, child) => sum + (child.items || []).length, 0)
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Searches items in a bank by text
|
|
45
|
+
*/
|
|
46
|
+
export async function searchBankItems(bankId, query, limit = 20) {
|
|
47
|
+
const { data, error } = await supabase
|
|
48
|
+
.from('composition_banks')
|
|
49
|
+
.select('categories')
|
|
50
|
+
.eq('id', bankId)
|
|
51
|
+
.single();
|
|
52
|
+
if (error)
|
|
53
|
+
throw new Error(`Failed to search bank: ${error.message}`);
|
|
54
|
+
const results = [];
|
|
55
|
+
const queryLower = query.toLowerCase();
|
|
56
|
+
function searchInItems(items) {
|
|
57
|
+
for (const item of items) {
|
|
58
|
+
if (item.description?.toLowerCase().includes(queryLower) ||
|
|
59
|
+
item.code?.toLowerCase().includes(queryLower)) {
|
|
60
|
+
results.push({
|
|
61
|
+
id: item.id,
|
|
62
|
+
code: item.code,
|
|
63
|
+
description: item.description,
|
|
64
|
+
unit: item.unit,
|
|
65
|
+
unitCost: item.unitCost || 0,
|
|
66
|
+
itemType: item.itemType
|
|
67
|
+
});
|
|
68
|
+
if (results.length >= limit)
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (item.children)
|
|
72
|
+
searchInItems(item.children);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function searchInCategories(categories) {
|
|
76
|
+
for (const cat of categories) {
|
|
77
|
+
if (cat.items)
|
|
78
|
+
searchInItems(cat.items);
|
|
79
|
+
if (cat.children)
|
|
80
|
+
searchInCategories(cat.children);
|
|
81
|
+
if (results.length >= limit)
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
searchInCategories(data?.categories || []);
|
|
86
|
+
return results;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Prepares importing an item from bank to budget
|
|
90
|
+
*/
|
|
91
|
+
export function prepareImportItem(budgetId, bankItem) {
|
|
92
|
+
return {
|
|
93
|
+
action: 'import_item',
|
|
94
|
+
preview: `Importar "${bankItem.description}" (${bankItem.unit}) do banco para o orçamento`,
|
|
95
|
+
data: {
|
|
96
|
+
budgetId,
|
|
97
|
+
item: {
|
|
98
|
+
description: bankItem.description,
|
|
99
|
+
unit: bankItem.unit,
|
|
100
|
+
quantity: 0, // User will fill in quantity
|
|
101
|
+
unitCost: bankItem.unitCost
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { AuthContext } from './auth.js';
|
|
2
|
+
export interface BudgetRow {
|
|
3
|
+
id: string;
|
|
4
|
+
level: number;
|
|
5
|
+
code: string;
|
|
6
|
+
description: string;
|
|
7
|
+
unit: string;
|
|
8
|
+
quantity: number;
|
|
9
|
+
unitCost: number;
|
|
10
|
+
isComposition?: boolean;
|
|
11
|
+
bdiType?: 'Principal' | 'Diferenciado';
|
|
12
|
+
}
|
|
13
|
+
export interface Budget {
|
|
14
|
+
id: string;
|
|
15
|
+
title: string;
|
|
16
|
+
createdAt: string;
|
|
17
|
+
lastModified: string;
|
|
18
|
+
creator: string;
|
|
19
|
+
shared: boolean;
|
|
20
|
+
projectId?: string;
|
|
21
|
+
rows: BudgetRow[];
|
|
22
|
+
bdiPrincipal: number;
|
|
23
|
+
bdiDiferenciado: number;
|
|
24
|
+
}
|
|
25
|
+
export interface BudgetSummary {
|
|
26
|
+
id: string;
|
|
27
|
+
title: string;
|
|
28
|
+
createdAt: string;
|
|
29
|
+
lastModified: string;
|
|
30
|
+
creator: string;
|
|
31
|
+
shared: boolean;
|
|
32
|
+
itemCount: number;
|
|
33
|
+
totalValue: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Lists recent budgets for the authenticated user's tenant or user
|
|
37
|
+
*/
|
|
38
|
+
export declare function listBudgets(auth: AuthContext, limit?: number): Promise<BudgetSummary[]>;
|
|
39
|
+
/**
|
|
40
|
+
* Gets full budget details
|
|
41
|
+
*/
|
|
42
|
+
export declare function getBudget(auth: AuthContext, budgetId: string): Promise<Budget | null>;
|
|
43
|
+
/**
|
|
44
|
+
* Creates a new budget (returns preview for confirmation)
|
|
45
|
+
*/
|
|
46
|
+
export declare function prepareCreateBudget(auth: AuthContext, title: string): {
|
|
47
|
+
action: string;
|
|
48
|
+
preview: string;
|
|
49
|
+
data: any;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Executes budget creation
|
|
53
|
+
*/
|
|
54
|
+
export declare function executeCreateBudget(auth: AuthContext, data: any): Promise<string>;
|
|
55
|
+
/**
|
|
56
|
+
* Prepares adding an item to a budget
|
|
57
|
+
*/
|
|
58
|
+
export declare function prepareAddItem(budgetId: string, item: {
|
|
59
|
+
description: string;
|
|
60
|
+
unit: string;
|
|
61
|
+
quantity: number;
|
|
62
|
+
unitCost: number;
|
|
63
|
+
}): {
|
|
64
|
+
action: string;
|
|
65
|
+
preview: string;
|
|
66
|
+
data: any;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Executes adding an item
|
|
70
|
+
*/
|
|
71
|
+
export declare function executeAddItem(auth: AuthContext, data: any): Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Prepares item deletion
|
|
74
|
+
*/
|
|
75
|
+
export declare function prepareDeleteItem(budgetId: string, itemId: string, itemDescription: string): {
|
|
76
|
+
action: string;
|
|
77
|
+
preview: string;
|
|
78
|
+
data: any;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Executes item deletion
|
|
82
|
+
*/
|
|
83
|
+
export declare function executeDeleteItem(auth: AuthContext, data: any): Promise<void>;
|
|
84
|
+
//# sourceMappingURL=budgetService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"budgetService.d.ts","sourceRoot":"","sources":["../../src/core/budgetService.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAExC,MAAM,WAAW,SAAS;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,WAAW,GAAG,cAAc,CAAC;CAC1C;AAED,MAAM,WAAW,MAAM;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,SAAS,EAAE,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,GAAE,MAAW,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CA0BjG;AAED;;GAEG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAyB3F;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,GAAG,CAAA;CAAE,CAMpH;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,CAgCvF;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC1B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAChF;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,GAAG,CAAA;CAAE,CAOhD;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CA0BhF;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,GAAG,CAAA;CAAE,CAM3I;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAenF"}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { supabase } from './supabase.js';
|
|
2
|
+
/**
|
|
3
|
+
* Lists recent budgets for the authenticated user's tenant or user
|
|
4
|
+
*/
|
|
5
|
+
export async function listBudgets(auth, limit = 10) {
|
|
6
|
+
// Query budgets by tenant_id OR user_id (for legacy budgets with null tenant_id)
|
|
7
|
+
const { data, error } = await supabase
|
|
8
|
+
.from('budgets')
|
|
9
|
+
.select('id, title, created_at, last_modified, creator, shared, data, tenant_id, user_id')
|
|
10
|
+
.or(`tenant_id.eq.${auth.tenantId},user_id.eq.${auth.userId}`)
|
|
11
|
+
.eq('deleted', false)
|
|
12
|
+
.order('last_modified', { ascending: false })
|
|
13
|
+
.limit(limit);
|
|
14
|
+
if (error)
|
|
15
|
+
throw new Error(`Failed to list budgets: ${error.message}`);
|
|
16
|
+
return (data || []).map(b => {
|
|
17
|
+
const rows = b.data?.rows || [];
|
|
18
|
+
const totalValue = rows.reduce((sum, r) => sum + (r.quantity || 0) * (r.unitCost || 0), 0);
|
|
19
|
+
return {
|
|
20
|
+
id: b.id,
|
|
21
|
+
title: b.title,
|
|
22
|
+
createdAt: b.created_at,
|
|
23
|
+
lastModified: b.last_modified,
|
|
24
|
+
creator: b.creator,
|
|
25
|
+
shared: b.shared,
|
|
26
|
+
itemCount: rows.length,
|
|
27
|
+
totalValue
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Gets full budget details
|
|
33
|
+
*/
|
|
34
|
+
export async function getBudget(auth, budgetId) {
|
|
35
|
+
const { data, error } = await supabase
|
|
36
|
+
.from('budgets')
|
|
37
|
+
.select('*')
|
|
38
|
+
.eq('id', budgetId)
|
|
39
|
+
.or(`tenant_id.eq.${auth.tenantId},user_id.eq.${auth.userId}`)
|
|
40
|
+
.single();
|
|
41
|
+
if (error) {
|
|
42
|
+
if (error.code === 'PGRST116')
|
|
43
|
+
return null; // Not found
|
|
44
|
+
throw new Error(`Failed to get budget: ${error.message}`);
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
id: data.id,
|
|
48
|
+
title: data.title,
|
|
49
|
+
createdAt: data.created_at,
|
|
50
|
+
lastModified: data.last_modified,
|
|
51
|
+
creator: data.creator,
|
|
52
|
+
shared: data.shared,
|
|
53
|
+
projectId: data.project_id,
|
|
54
|
+
rows: data.data?.rows || [],
|
|
55
|
+
bdiPrincipal: data.data?.bdiPrincipal || 0,
|
|
56
|
+
bdiDiferenciado: data.data?.bdiDiferenciado || 0
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Creates a new budget (returns preview for confirmation)
|
|
61
|
+
*/
|
|
62
|
+
export function prepareCreateBudget(auth, title) {
|
|
63
|
+
return {
|
|
64
|
+
action: 'create_budget',
|
|
65
|
+
preview: `Criar orçamento "${title}"`,
|
|
66
|
+
data: { title, tenantId: auth.tenantId, userId: auth.userId, creator: auth.userName }
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Executes budget creation
|
|
71
|
+
*/
|
|
72
|
+
export async function executeCreateBudget(auth, data) {
|
|
73
|
+
const now = new Date().toISOString();
|
|
74
|
+
const budgetData = {
|
|
75
|
+
title: data.title,
|
|
76
|
+
tenant_id: data.tenantId,
|
|
77
|
+
user_id: data.userId,
|
|
78
|
+
creator: data.creator,
|
|
79
|
+
created_at: now,
|
|
80
|
+
last_modified: now,
|
|
81
|
+
shared: false,
|
|
82
|
+
deleted: false,
|
|
83
|
+
data: {
|
|
84
|
+
title: data.title,
|
|
85
|
+
rows: [],
|
|
86
|
+
bdi: 0,
|
|
87
|
+
bdiItems: [],
|
|
88
|
+
bdiPrincipal: 0,
|
|
89
|
+
bdiDiferenciado: 0,
|
|
90
|
+
bdiItemsPrincipal: [],
|
|
91
|
+
bdiItemsDiferenciado: [],
|
|
92
|
+
abcClassifications: {}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const { data: result, error } = await supabase
|
|
96
|
+
.from('budgets')
|
|
97
|
+
.insert(budgetData)
|
|
98
|
+
.select('id')
|
|
99
|
+
.single();
|
|
100
|
+
if (error)
|
|
101
|
+
throw new Error(`Failed to create budget: ${error.message}`);
|
|
102
|
+
return result.id;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Prepares adding an item to a budget
|
|
106
|
+
*/
|
|
107
|
+
export function prepareAddItem(budgetId, item) {
|
|
108
|
+
const total = item.quantity * item.unitCost;
|
|
109
|
+
return {
|
|
110
|
+
action: 'add_item',
|
|
111
|
+
preview: `Adicionar "${item.description}" (${item.quantity} ${item.unit} x R$ ${item.unitCost.toFixed(2)} = R$ ${total.toFixed(2)})`,
|
|
112
|
+
data: { budgetId, item }
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Executes adding an item
|
|
117
|
+
*/
|
|
118
|
+
export async function executeAddItem(auth, data) {
|
|
119
|
+
const budget = await getBudget(auth, data.budgetId);
|
|
120
|
+
if (!budget)
|
|
121
|
+
throw new Error('Budget not found');
|
|
122
|
+
const newRow = {
|
|
123
|
+
id: crypto.randomUUID(),
|
|
124
|
+
level: 0,
|
|
125
|
+
code: '',
|
|
126
|
+
description: data.item.description,
|
|
127
|
+
unit: data.item.unit,
|
|
128
|
+
quantity: data.item.quantity,
|
|
129
|
+
unitCost: data.item.unitCost,
|
|
130
|
+
isComposition: false
|
|
131
|
+
};
|
|
132
|
+
const updatedRows = [...budget.rows, newRow];
|
|
133
|
+
const { error } = await supabase
|
|
134
|
+
.from('budgets')
|
|
135
|
+
.update({
|
|
136
|
+
data: { ...budget, rows: updatedRows },
|
|
137
|
+
last_modified: new Date().toISOString()
|
|
138
|
+
})
|
|
139
|
+
.eq('id', data.budgetId);
|
|
140
|
+
if (error)
|
|
141
|
+
throw new Error(`Failed to add item: ${error.message}`);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Prepares item deletion
|
|
145
|
+
*/
|
|
146
|
+
export function prepareDeleteItem(budgetId, itemId, itemDescription) {
|
|
147
|
+
return {
|
|
148
|
+
action: 'delete_item',
|
|
149
|
+
preview: `Excluir item "${itemDescription}"`,
|
|
150
|
+
data: { budgetId, itemId }
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Executes item deletion
|
|
155
|
+
*/
|
|
156
|
+
export async function executeDeleteItem(auth, data) {
|
|
157
|
+
const budget = await getBudget(auth, data.budgetId);
|
|
158
|
+
if (!budget)
|
|
159
|
+
throw new Error('Budget not found');
|
|
160
|
+
const updatedRows = budget.rows.filter(r => r.id !== data.itemId);
|
|
161
|
+
const { error } = await supabase
|
|
162
|
+
.from('budgets')
|
|
163
|
+
.update({
|
|
164
|
+
data: { ...budget, rows: updatedRows },
|
|
165
|
+
last_modified: new Date().toISOString()
|
|
166
|
+
})
|
|
167
|
+
.eq('id', data.budgetId);
|
|
168
|
+
if (error)
|
|
169
|
+
throw new Error(`Failed to delete item: ${error.message}`);
|
|
170
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
declare const supabaseUrl = "https://aimquvsavkffqgctnzym.supabase.co";
|
|
3
|
+
export declare const supabase: import("@supabase/supabase-js").SupabaseClient<any, "public", "public", any, any>;
|
|
4
|
+
export { supabaseUrl };
|
|
5
|
+
//# sourceMappingURL=supabase.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"supabase.d.ts","sourceRoot":"","sources":["../../src/core/supabase.ts"],"names":[],"mappings":"AACA,OAAO,eAAe,CAAC;AAGvB,QAAA,MAAM,WAAW,6CAA6C,CAAC;AAG/D,eAAO,MAAM,QAAQ,mFAKnB,CAAC;AAEH,OAAO,EAAE,WAAW,EAAE,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createClient } from '@supabase/supabase-js';
|
|
2
|
+
import 'dotenv/config';
|
|
3
|
+
// Credenciais embutidas - usuário final só precisa do token MCP
|
|
4
|
+
const supabaseUrl = 'https://aimquvsavkffqgctnzym.supabase.co';
|
|
5
|
+
const supabaseServiceKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImFpbXF1dnNhdmtmZnFnY3RuenltIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc2NDYzOTU3NSwiZXhwIjoyMDgwMjE1NTc1fQ.rFiSeLqIuU8SYKOzr1yLTAKgrxI2SagUyDNk1M4ouAs';
|
|
6
|
+
export const supabase = createClient(supabaseUrl, supabaseServiceKey, {
|
|
7
|
+
auth: {
|
|
8
|
+
autoRefreshToken: false,
|
|
9
|
+
persistSession: false
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
export { supabaseUrl };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":";AAUA,OAAO,eAAe,CAAC"}
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import 'dotenv/config';
|
|
6
|
+
import { initAuth } from '../core/auth.js';
|
|
7
|
+
import * as budgetService from '../core/budgetService.js';
|
|
8
|
+
import * as bankService from '../core/bankService.js';
|
|
9
|
+
import * as analysisService from '../core/analysisService.js';
|
|
10
|
+
// Pending operations for confirmation workflow
|
|
11
|
+
const pendingOperations = new Map();
|
|
12
|
+
function generatePendingId() {
|
|
13
|
+
return `pending_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
14
|
+
}
|
|
15
|
+
// ============================================================
|
|
16
|
+
// MANUAL DO QUANTY - Este conteúdo é lido automaticamente pela IA
|
|
17
|
+
// ============================================================
|
|
18
|
+
const QUANTY_MANUAL = `
|
|
19
|
+
# Manual do Quanty - Sistema de Orçamentos de Engenharia
|
|
20
|
+
|
|
21
|
+
## O que é o Quanty?
|
|
22
|
+
O Quanty é um sistema de orçamentação de engenharia que permite criar, editar e analisar orçamentos de obras.
|
|
23
|
+
É usado por engenheiros civis, orçamentistas e empresas de construção.
|
|
24
|
+
|
|
25
|
+
## Conceitos Importantes
|
|
26
|
+
|
|
27
|
+
### 1. ORÇAMENTO
|
|
28
|
+
Um orçamento contém uma lista de ITENS que representam serviços ou materiais necessários para uma obra.
|
|
29
|
+
Cada orçamento tem:
|
|
30
|
+
- Título
|
|
31
|
+
- BDI Principal (percentual de bonificação aplicado sobre o custo)
|
|
32
|
+
- BDI Diferenciado (para itens específicos como equipamentos)
|
|
33
|
+
- Lista de Itens
|
|
34
|
+
|
|
35
|
+
### 2. ITENS
|
|
36
|
+
Cada item de um orçamento pode ser:
|
|
37
|
+
|
|
38
|
+
**A) ITEM SIMPLES**: Um insumo básico (ex: "Cimento Portland 50kg")
|
|
39
|
+
- Tem descrição, unidade, quantidade e custo unitário
|
|
40
|
+
- O custo total = quantidade × custo unitário
|
|
41
|
+
|
|
42
|
+
**B) COMPOSIÇÃO**: Um serviço composto por vários insumos (ex: "Concretagem de pilar")
|
|
43
|
+
- Identificado por isComposition=true
|
|
44
|
+
- Contém uma lista de INSUMOS (children) que compõem o serviço
|
|
45
|
+
- O custo unitário é calculado pela SOMA dos custos dos insumos
|
|
46
|
+
- Exemplo: "Concretagem" pode ter insumos: concreto, forma, ferragem, mão de obra
|
|
47
|
+
|
|
48
|
+
### 3. BANCOS DE COMPOSIÇÃO
|
|
49
|
+
São bases de dados com preços de referência:
|
|
50
|
+
- **SINAPI**: Sistema Nacional de Pesquisa de Custos e Índices da Construção Civil (Caixa Econômica)
|
|
51
|
+
- **SICRO**: Sistema de Custos de Obras Rodoviárias (DNIT)
|
|
52
|
+
- **Bancos próprios**: Composições criadas pela empresa
|
|
53
|
+
|
|
54
|
+
### 4. BDI (Bonificações e Despesas Indiretas)
|
|
55
|
+
Percentual aplicado sobre o custo direto para cobrir:
|
|
56
|
+
- Administração central
|
|
57
|
+
- Lucro
|
|
58
|
+
- Impostos
|
|
59
|
+
- Seguros
|
|
60
|
+
- Risco
|
|
61
|
+
|
|
62
|
+
Preço de Venda = Custo Direto × (1 + BDI%)
|
|
63
|
+
|
|
64
|
+
### 5. HIERARQUIA (NÍVEIS)
|
|
65
|
+
Itens podem ser organizados em níveis hierárquicos:
|
|
66
|
+
- Nível 0: Item principal (título ou agrupador)
|
|
67
|
+
- Nível 1: Subitens
|
|
68
|
+
- Nível 2+: Sub-subitens
|
|
69
|
+
|
|
70
|
+
## Terminologia do Setor
|
|
71
|
+
|
|
72
|
+
- **Insumo**: Material ou recurso básico (cimento, areia, mão de obra)
|
|
73
|
+
- **Composição**: Serviço que combina vários insumos
|
|
74
|
+
- **Custo Unitário**: Preço por unidade (R$/m³, R$/kg, R$/h)
|
|
75
|
+
- **Quantidade**: Volume de serviço (ex: 500 m³ de concreto)
|
|
76
|
+
- **EAP**: Estrutura Analítica do Projeto (organização hierárquica)
|
|
77
|
+
- **Curva ABC**: Classificação de itens por impacto no custo total
|
|
78
|
+
|
|
79
|
+
## Dicas para o Assistente de IA
|
|
80
|
+
|
|
81
|
+
1. Quando o usuário perguntar sobre CUSTOS ZERADOS em composições, verifique se os INSUMOS (children) estão presentes
|
|
82
|
+
2. Para COMPARAR preços, use a função de custo médio ou orçamentos similares
|
|
83
|
+
3. Ao CRIAR itens, sempre pergunte a quantidade e verifique a unidade correta
|
|
84
|
+
4. COMPOSIÇÕES devem ser importadas dos bancos (SINAPI/SICRO) para manter a estrutura
|
|
85
|
+
5. O BDI padrão do mercado fica entre 20% e 30%
|
|
86
|
+
`;
|
|
87
|
+
// Tool definitions with detailed descriptions
|
|
88
|
+
const TOOLS = [
|
|
89
|
+
// === READ TOOLS ===
|
|
90
|
+
{
|
|
91
|
+
name: 'quanty_listar_orcamentos',
|
|
92
|
+
description: `Lista os orçamentos recentes do usuário no Quanty.
|
|
93
|
+
Retorna: título, data de criação/modificação, valor total estimado e quantidade de itens.
|
|
94
|
+
Use esta função para descobrir quais orçamentos existem antes de abrir um específico.`,
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: 'object',
|
|
97
|
+
properties: {
|
|
98
|
+
limite: { type: 'number', description: 'Quantidade máxima de orçamentos (padrão: 10)' }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'quanty_abrir_orcamento',
|
|
104
|
+
description: `Abre um orçamento e retorna todos os detalhes incluindo lista de ITENS.
|
|
105
|
+
Cada item pode ser:
|
|
106
|
+
- ITEM SIMPLES: tem descrição, unidade, quantidade, custo_unitario
|
|
107
|
+
- COMPOSIÇÃO (is_composicao=true): serviço composto por insumos. O custo é calculado pela soma dos insumos.
|
|
108
|
+
Se o custo de uma composição aparecer zerado, verifique os insumos (children) da composição.
|
|
109
|
+
Use quanty_ver_composicao para ver os insumos de uma composição específica.`,
|
|
110
|
+
inputSchema: {
|
|
111
|
+
type: 'object',
|
|
112
|
+
properties: {
|
|
113
|
+
id: { type: 'string', description: 'ID do orçamento (obtido via quanty_listar_orcamentos)' }
|
|
114
|
+
},
|
|
115
|
+
required: ['id']
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: 'quanty_listar_bancos',
|
|
120
|
+
description: `Lista os bancos de composição disponíveis para buscar preços de referência.
|
|
121
|
+
Bancos comuns: SINAPI (Caixa), SICRO (DNIT), ou bancos próprios da empresa.
|
|
122
|
+
Use esta função antes de buscar insumos para saber qual banco usar.`,
|
|
123
|
+
inputSchema: { type: 'object', properties: {} }
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: 'quanty_buscar_insumo',
|
|
127
|
+
description: `Busca insumos ou composições em um banco de preços de referência.
|
|
128
|
+
Retorna código, descrição, unidade e custo unitário.
|
|
129
|
+
O campo 'tipo' indica se é INSUMO (material básico) ou COMPOSIÇÃO (serviço composto).
|
|
130
|
+
Para importar uma composição para um orçamento, o sistema traz toda a estrutura de insumos.`,
|
|
131
|
+
inputSchema: {
|
|
132
|
+
type: 'object',
|
|
133
|
+
properties: {
|
|
134
|
+
banco_id: { type: 'string', description: 'ID do banco (obtido via quanty_listar_bancos)' },
|
|
135
|
+
busca: { type: 'string', description: 'Texto para buscar (ex: "concreto fck 30", "forma metálica")' }
|
|
136
|
+
},
|
|
137
|
+
required: ['banco_id', 'busca']
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
// === WRITE TOOLS (with confirmation) ===
|
|
141
|
+
{
|
|
142
|
+
name: 'quanty_preparar_orcamento',
|
|
143
|
+
description: `Prepara a criação de um NOVO orçamento vazio.
|
|
144
|
+
Retorna um preview e pending_id. Use quanty_executar para confirmar.
|
|
145
|
+
O orçamento será criado com BDI zerado (pode ser ajustado depois).`,
|
|
146
|
+
inputSchema: {
|
|
147
|
+
type: 'object',
|
|
148
|
+
properties: {
|
|
149
|
+
titulo: { type: 'string', description: 'Título descritivo do orçamento (ex: "Reforma Escola Municipal")' }
|
|
150
|
+
},
|
|
151
|
+
required: ['titulo']
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: 'quanty_preparar_item',
|
|
156
|
+
description: `Prepara a adição de um item SIMPLES ao orçamento.
|
|
157
|
+
Para adicionar COMPOSIÇÕES (serviços com insumos), use quanty_buscar_insumo + quanty_importar_composicao.
|
|
158
|
+
Retorna preview e pending_id. Use quanty_executar para confirmar.`,
|
|
159
|
+
inputSchema: {
|
|
160
|
+
type: 'object',
|
|
161
|
+
properties: {
|
|
162
|
+
orcamento_id: { type: 'string', description: 'ID do orçamento destino' },
|
|
163
|
+
descricao: { type: 'string', description: 'Descrição do item (ex: "Demolição manual de alvenaria")' },
|
|
164
|
+
unidade: { type: 'string', description: 'Unidade de medida (m², m³, m, kg, un, h, vb)' },
|
|
165
|
+
quantidade: { type: 'number', description: 'Quantidade do serviço' },
|
|
166
|
+
custo_unitario: { type: 'number', description: 'Custo unitário em R$ (sem BDI)' }
|
|
167
|
+
},
|
|
168
|
+
required: ['orcamento_id', 'descricao', 'unidade', 'quantidade', 'custo_unitario']
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: 'quanty_executar',
|
|
173
|
+
description: `Confirma e executa uma operação pendente (criar orçamento, adicionar item, etc).
|
|
174
|
+
Após executar, a operação é concluída e o pending_id não pode mais ser usado.
|
|
175
|
+
Operações expiram após 5 minutos se não confirmadas.`,
|
|
176
|
+
inputSchema: {
|
|
177
|
+
type: 'object',
|
|
178
|
+
properties: {
|
|
179
|
+
pending_id: { type: 'string', description: 'ID da operação pendente (retornado por quanty_preparar_*)' }
|
|
180
|
+
},
|
|
181
|
+
required: ['pending_id']
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
// === ANALYSIS TOOLS ===
|
|
185
|
+
{
|
|
186
|
+
name: 'quanty_orcamentos_similares',
|
|
187
|
+
description: `Encontra orçamentos SIMILARES baseado nos itens em comum.
|
|
188
|
+
Útil para: benchmarking, verificar se está faltando algo, comparar preços.
|
|
189
|
+
Retorna lista de orçamentos ordenados por % de similaridade.`,
|
|
190
|
+
inputSchema: {
|
|
191
|
+
type: 'object',
|
|
192
|
+
properties: {
|
|
193
|
+
orcamento_id: { type: 'string', description: 'ID do orçamento de referência' }
|
|
194
|
+
},
|
|
195
|
+
required: ['orcamento_id']
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: 'quanty_sugerir_itens',
|
|
200
|
+
description: `Sugere itens que podem estar FALTANDO no orçamento.
|
|
201
|
+
Analisa orçamentos similares e identifica itens frequentes que não estão presentes.
|
|
202
|
+
Útil para evitar esquecimentos em obras similares.`,
|
|
203
|
+
inputSchema: {
|
|
204
|
+
type: 'object',
|
|
205
|
+
properties: {
|
|
206
|
+
orcamento_id: { type: 'string', description: 'ID do orçamento a analisar' }
|
|
207
|
+
},
|
|
208
|
+
required: ['orcamento_id']
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: 'quanty_custo_medio',
|
|
213
|
+
description: `Calcula o CUSTO MÉDIO por unidade baseado em orçamentos históricos.
|
|
214
|
+
Ex: "Qual o custo médio do m³ de concreto nos meus orçamentos?"
|
|
215
|
+
Retorna média, mínimo, máximo e percentil 90 (para detecção de outliers).`,
|
|
216
|
+
inputSchema: {
|
|
217
|
+
type: 'object',
|
|
218
|
+
properties: {
|
|
219
|
+
driver: { type: 'string', description: 'Item a analisar (ex: "concreto", "forma", "aço", "escavação")' },
|
|
220
|
+
categoria: { type: 'string', description: 'Categoria opcional para filtrar (ex: "fundação", "estrutura")' }
|
|
221
|
+
},
|
|
222
|
+
required: ['driver']
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
];
|
|
226
|
+
async function handleToolCall(auth, name, args) {
|
|
227
|
+
switch (name) {
|
|
228
|
+
// === READ ===
|
|
229
|
+
case 'quanty_listar_orcamentos': {
|
|
230
|
+
const budgets = await budgetService.listBudgets(auth, args.limite || 10);
|
|
231
|
+
return {
|
|
232
|
+
_info: 'Use quanty_abrir_orcamento com o ID para ver detalhes completos',
|
|
233
|
+
total: budgets.length,
|
|
234
|
+
orcamentos: budgets.map(b => ({
|
|
235
|
+
id: b.id,
|
|
236
|
+
titulo: b.title,
|
|
237
|
+
criado_em: b.createdAt,
|
|
238
|
+
modificado_em: b.lastModified,
|
|
239
|
+
criador: b.creator,
|
|
240
|
+
compartilhado: b.shared,
|
|
241
|
+
qtd_itens: b.itemCount,
|
|
242
|
+
valor_total: `R$ ${b.totalValue.toFixed(2)}`
|
|
243
|
+
}))
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
case 'quanty_abrir_orcamento': {
|
|
247
|
+
const budget = await budgetService.getBudget(auth, args.id);
|
|
248
|
+
if (!budget)
|
|
249
|
+
return { erro: 'Orçamento não encontrado' };
|
|
250
|
+
const totalValue = budget.rows.reduce((sum, r) => sum + (r.quantity * r.unitCost), 0);
|
|
251
|
+
return {
|
|
252
|
+
id: budget.id,
|
|
253
|
+
titulo: budget.title,
|
|
254
|
+
bdi_principal: `${budget.bdiPrincipal}%`,
|
|
255
|
+
bdi_diferenciado: `${budget.bdiDiferenciado}%`,
|
|
256
|
+
valor_total: `R$ ${totalValue.toFixed(2)}`,
|
|
257
|
+
_info_itens: 'Itens com is_composicao=true são COMPOSIÇÕES que possuem insumos internos',
|
|
258
|
+
itens: budget.rows.map(r => ({
|
|
259
|
+
id: r.id,
|
|
260
|
+
nivel: r.level,
|
|
261
|
+
codigo: r.code,
|
|
262
|
+
descricao: r.description,
|
|
263
|
+
unidade: r.unit,
|
|
264
|
+
quantidade: r.quantity,
|
|
265
|
+
custo_unitario: r.unitCost,
|
|
266
|
+
total: r.quantity * r.unitCost,
|
|
267
|
+
is_composicao: r.isComposition || false,
|
|
268
|
+
bdi_tipo: r.bdiType || 'Principal'
|
|
269
|
+
}))
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
case 'quanty_listar_bancos': {
|
|
273
|
+
const banks = await bankService.listBanks(auth);
|
|
274
|
+
return {
|
|
275
|
+
_info: 'Use quanty_buscar_insumo com o ID do banco para buscar itens',
|
|
276
|
+
total: banks.length,
|
|
277
|
+
bancos: banks.map(b => ({
|
|
278
|
+
id: b.id,
|
|
279
|
+
titulo: b.title,
|
|
280
|
+
descricao: b.description,
|
|
281
|
+
autor: b.author,
|
|
282
|
+
regiao: b.region,
|
|
283
|
+
visibilidade: b.visibility,
|
|
284
|
+
qtd_categorias: b.categoryCount
|
|
285
|
+
}))
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
case 'quanty_buscar_insumo': {
|
|
289
|
+
const items = await bankService.searchBankItems(args.banco_id, args.busca);
|
|
290
|
+
return {
|
|
291
|
+
_info: 'Itens do tipo COMPOSIÇÃO possuem insumos internos que definem o custo',
|
|
292
|
+
total: items.length,
|
|
293
|
+
resultados: items.map(i => ({
|
|
294
|
+
id: i.id,
|
|
295
|
+
codigo: i.code,
|
|
296
|
+
descricao: i.description,
|
|
297
|
+
unidade: i.unit,
|
|
298
|
+
custo_unitario: i.unitCost,
|
|
299
|
+
tipo: i.itemType
|
|
300
|
+
}))
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
// === WRITE (with confirmation) ===
|
|
304
|
+
case 'quanty_preparar_orcamento': {
|
|
305
|
+
const prepared = budgetService.prepareCreateBudget(auth, args.titulo);
|
|
306
|
+
const pendingId = generatePendingId();
|
|
307
|
+
pendingOperations.set(pendingId, {
|
|
308
|
+
action: prepared.action,
|
|
309
|
+
data: prepared.data,
|
|
310
|
+
expiresAt: Date.now() + 5 * 60 * 1000
|
|
311
|
+
});
|
|
312
|
+
return {
|
|
313
|
+
pending_id: pendingId,
|
|
314
|
+
preview: prepared.preview,
|
|
315
|
+
mensagem: 'Use quanty_executar com este pending_id para confirmar a criação.'
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
case 'quanty_preparar_item': {
|
|
319
|
+
const prepared = budgetService.prepareAddItem(args.orcamento_id, {
|
|
320
|
+
description: args.descricao,
|
|
321
|
+
unit: args.unidade,
|
|
322
|
+
quantity: args.quantidade,
|
|
323
|
+
unitCost: args.custo_unitario
|
|
324
|
+
});
|
|
325
|
+
const pendingId = generatePendingId();
|
|
326
|
+
pendingOperations.set(pendingId, {
|
|
327
|
+
action: prepared.action,
|
|
328
|
+
data: prepared.data,
|
|
329
|
+
expiresAt: Date.now() + 5 * 60 * 1000
|
|
330
|
+
});
|
|
331
|
+
return {
|
|
332
|
+
pending_id: pendingId,
|
|
333
|
+
preview: prepared.preview,
|
|
334
|
+
mensagem: 'Use quanty_executar com este pending_id para confirmar a adição.'
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
case 'quanty_executar': {
|
|
338
|
+
const pending = pendingOperations.get(args.pending_id);
|
|
339
|
+
if (!pending) {
|
|
340
|
+
return { erro: 'Operação pendente não encontrada ou expirada' };
|
|
341
|
+
}
|
|
342
|
+
if (Date.now() > pending.expiresAt) {
|
|
343
|
+
pendingOperations.delete(args.pending_id);
|
|
344
|
+
return { erro: 'Operação expirada. Prepare novamente.' };
|
|
345
|
+
}
|
|
346
|
+
pendingOperations.delete(args.pending_id);
|
|
347
|
+
switch (pending.action) {
|
|
348
|
+
case 'create_budget': {
|
|
349
|
+
const budgetId = await budgetService.executeCreateBudget(auth, pending.data);
|
|
350
|
+
return { sucesso: true, orcamento_id: budgetId, mensagem: 'Orçamento criado com sucesso!' };
|
|
351
|
+
}
|
|
352
|
+
case 'add_item': {
|
|
353
|
+
await budgetService.executeAddItem(auth, pending.data);
|
|
354
|
+
return { sucesso: true, mensagem: 'Item adicionado com sucesso!' };
|
|
355
|
+
}
|
|
356
|
+
case 'import_item': {
|
|
357
|
+
await budgetService.executeAddItem(auth, pending.data);
|
|
358
|
+
return { sucesso: true, mensagem: 'Item importado com sucesso!' };
|
|
359
|
+
}
|
|
360
|
+
default:
|
|
361
|
+
return { erro: `Ação desconhecida: ${pending.action}` };
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// === ANALYSIS ===
|
|
365
|
+
case 'quanty_orcamentos_similares': {
|
|
366
|
+
const similar = await analysisService.findSimilarBudgets(auth, args.orcamento_id);
|
|
367
|
+
return {
|
|
368
|
+
total: similar.length,
|
|
369
|
+
similares: similar.map(s => ({
|
|
370
|
+
id: s.id,
|
|
371
|
+
titulo: s.title,
|
|
372
|
+
similaridade: `${s.similarity}%`,
|
|
373
|
+
itens_em_comum: s.commonItems
|
|
374
|
+
}))
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
case 'quanty_sugerir_itens': {
|
|
378
|
+
const suggestions = await analysisService.suggestMissingItems(auth, args.orcamento_id);
|
|
379
|
+
return {
|
|
380
|
+
total: suggestions.length,
|
|
381
|
+
sugestoes: suggestions.map(s => ({
|
|
382
|
+
descricao: s.description,
|
|
383
|
+
unidade: s.unit,
|
|
384
|
+
custo_medio: `R$ ${s.avgUnitCost.toFixed(2)}`,
|
|
385
|
+
frequencia: s.frequency,
|
|
386
|
+
razao: s.reason
|
|
387
|
+
}))
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
case 'quanty_custo_medio': {
|
|
391
|
+
const metric = await analysisService.calculateCostPerUnit(auth, args.categoria || '', args.driver);
|
|
392
|
+
return {
|
|
393
|
+
driver: metric.driver,
|
|
394
|
+
custo_medio: `R$ ${metric.avgCostPerUnit.toFixed(2)}`,
|
|
395
|
+
custo_minimo: `R$ ${metric.minCost.toFixed(2)}`,
|
|
396
|
+
custo_maximo: `R$ ${metric.maxCost.toFixed(2)}`,
|
|
397
|
+
custo_p90: `R$ ${metric.p90Cost.toFixed(2)}`,
|
|
398
|
+
amostra: `${metric.sampleSize} orçamentos analisados`
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
default:
|
|
402
|
+
return { erro: `Tool não reconhecida: ${name}` };
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
async function main() {
|
|
406
|
+
console.error('[Quanty MCP] Starting server...');
|
|
407
|
+
// Initialize auth
|
|
408
|
+
let auth;
|
|
409
|
+
try {
|
|
410
|
+
auth = await initAuth();
|
|
411
|
+
console.error(`[Quanty MCP] Authenticated as ${auth.userName} (${auth.userEmail})`);
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
console.error('[Quanty MCP] Auth failed:', error);
|
|
415
|
+
process.exit(1);
|
|
416
|
+
}
|
|
417
|
+
// Create MCP server with resources capability
|
|
418
|
+
const server = new Server({ name: 'quanty-mcp-server', version: '1.0.0' }, { capabilities: { tools: {}, resources: {} } });
|
|
419
|
+
// List resources handler - Manual do Quanty
|
|
420
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
421
|
+
resources: [
|
|
422
|
+
{
|
|
423
|
+
uri: 'quanty://manual',
|
|
424
|
+
name: 'Manual do Quanty',
|
|
425
|
+
description: 'Documentação completa sobre orçamentação de engenharia, conceitos, terminologia e dicas de uso.',
|
|
426
|
+
mimeType: 'text/plain'
|
|
427
|
+
}
|
|
428
|
+
]
|
|
429
|
+
}));
|
|
430
|
+
// Read resource handler
|
|
431
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
432
|
+
if (request.params.uri === 'quanty://manual') {
|
|
433
|
+
return {
|
|
434
|
+
contents: [{ uri: 'quanty://manual', mimeType: 'text/plain', text: QUANTY_MANUAL }]
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
throw new Error(`Resource not found: ${request.params.uri}`);
|
|
438
|
+
});
|
|
439
|
+
// List tools handler
|
|
440
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
441
|
+
tools: TOOLS
|
|
442
|
+
}));
|
|
443
|
+
// Call tool handler
|
|
444
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
445
|
+
const { name, arguments: args } = request.params;
|
|
446
|
+
try {
|
|
447
|
+
const result = await handleToolCall(auth, name, args || {});
|
|
448
|
+
return {
|
|
449
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
return {
|
|
454
|
+
content: [{ type: 'text', text: JSON.stringify({ erro: error.message }) }],
|
|
455
|
+
isError: true
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
// Start server with stdio transport
|
|
460
|
+
const transport = new StdioServerTransport();
|
|
461
|
+
await server.connect(transport);
|
|
462
|
+
console.error('[Quanty MCP] Server running on stdio');
|
|
463
|
+
}
|
|
464
|
+
main().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@quantyapp/quanty-mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP Server para conectar Claude Desktop ao Quanty - Sistema de Orçamentos de Engenharia",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/mcp/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"quanty-mcp-server": "./dist/mcp/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "node dist/mcp/index.js",
|
|
13
|
+
"dev": "tsx src/mcp/index.ts",
|
|
14
|
+
"prepublishOnly": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"mcp",
|
|
18
|
+
"quanty",
|
|
19
|
+
"claude",
|
|
20
|
+
"anthropic",
|
|
21
|
+
"orçamento",
|
|
22
|
+
"engenharia",
|
|
23
|
+
"construção"
|
|
24
|
+
],
|
|
25
|
+
"author": "B2Builder",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/B2Builder-Brasil/Quanty"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://quantyapp.com.br",
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist/**/*"
|
|
37
|
+
],
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
40
|
+
"@supabase/supabase-js": "^2.39.0",
|
|
41
|
+
"dotenv": "^16.3.1",
|
|
42
|
+
"zod": "^3.22.4"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^20.10.0",
|
|
46
|
+
"tsx": "^4.7.0",
|
|
47
|
+
"typescript": "^5.3.0"
|
|
48
|
+
}
|
|
49
|
+
}
|