@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 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,3 @@
1
+ #!/usr/bin/env node
2
+ import 'dotenv/config';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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
+ }