@kynver-app/mcp-estimator 0.1.1

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.
Files changed (52) hide show
  1. package/dist/index.d.ts +7 -0
  2. package/dist/index.js +11 -0
  3. package/dist/lib/kynver-client.d.ts +20 -0
  4. package/dist/lib/kynver-client.js +110 -0
  5. package/dist/lib/mcp-sse-server.d.ts +29 -0
  6. package/dist/lib/mcp-sse-server.js +191 -0
  7. package/dist/server.d.ts +6 -0
  8. package/dist/server.js +78 -0
  9. package/dist/sse.d.ts +1 -0
  10. package/dist/sse.js +9 -0
  11. package/dist/tools/catalog-tools.d.ts +387 -0
  12. package/dist/tools/catalog-tools.js +193 -0
  13. package/dist/tools/client-tools.d.ts +193 -0
  14. package/dist/tools/client-tools.js +105 -0
  15. package/dist/tools/company-tools.d.ts +163 -0
  16. package/dist/tools/company-tools.js +98 -0
  17. package/dist/tools/create-estimate.d.ts +107 -0
  18. package/dist/tools/create-estimate.js +64 -0
  19. package/dist/tools/estimate-tools.d.ts +531 -0
  20. package/dist/tools/estimate-tools.js +259 -0
  21. package/dist/tools/estimator-chat.d.ts +25 -0
  22. package/dist/tools/estimator-chat.js +15 -0
  23. package/dist/tools/get-estimate.d.ts +19 -0
  24. package/dist/tools/get-estimate.js +14 -0
  25. package/dist/tools/list-catalog.d.ts +25 -0
  26. package/dist/tools/list-catalog.js +22 -0
  27. package/dist/tools/photo-tools.d.ts +77 -0
  28. package/dist/tools/photo-tools.js +51 -0
  29. package/dist/tools/stats-pdf-tools.d.ts +113 -0
  30. package/dist/tools/stats-pdf-tools.js +73 -0
  31. package/dist/tools/template-tools.d.ts +96 -0
  32. package/dist/tools/template-tools.js +70 -0
  33. package/dist/tools/version-tools.d.ts +46 -0
  34. package/dist/tools/version-tools.js +33 -0
  35. package/dist/tools/vision-tools.d.ts +52 -0
  36. package/dist/tools/vision-tools.js +42 -0
  37. package/package.json +27 -0
  38. package/src/index.ts +13 -0
  39. package/src/lib/kynver-client.ts +136 -0
  40. package/src/lib/mcp-sse-server.ts +249 -0
  41. package/src/server.ts +101 -0
  42. package/src/sse.ts +10 -0
  43. package/src/tools/catalog-tools.ts +217 -0
  44. package/src/tools/client-tools.ts +114 -0
  45. package/src/tools/company-tools.ts +108 -0
  46. package/src/tools/estimate-tools.ts +281 -0
  47. package/src/tools/photo-tools.ts +60 -0
  48. package/src/tools/stats-pdf-tools.ts +80 -0
  49. package/src/tools/template-tools.ts +80 -0
  50. package/src/tools/version-tools.ts +39 -0
  51. package/src/tools/vision-tools.ts +48 -0
  52. package/tsconfig.json +15 -0
@@ -0,0 +1,114 @@
1
+ import { z } from "zod";
2
+ import * as client from "../lib/kynver-client.js";
3
+
4
+ // --- estimator_create_client ---
5
+
6
+ const createClientSchema = z.object({
7
+ name: z.string(),
8
+ companyId: z.string().optional(),
9
+ title: z.string().optional(),
10
+ phone: z.string().optional(),
11
+ email: z.string().optional(),
12
+ address: z.string().optional(),
13
+ notes: z.string().optional(),
14
+ });
15
+
16
+ export const createClient = {
17
+ name: "estimator_create_client",
18
+ description: "Create a new client (individual person) in the client book. Clients can optionally belong to a company (pass companyId). Include contact details: name (required), title, phone, email, address, and notes. The client can then be linked to estimates via their clientId.",
19
+ inputSchemaZod: createClientSchema,
20
+ execute: async (args: z.infer<typeof createClientSchema>): Promise<string> => {
21
+ if (!client.hasApiKey()) return client.noApiKeyMessage();
22
+ const data = await client.post("/estimator/clients", args);
23
+ return JSON.stringify(data);
24
+ },
25
+ };
26
+
27
+ // --- estimator_list_clients ---
28
+
29
+ const listClientsSchema = z.object({
30
+ search: z.string().optional(),
31
+ companyId: z.string().optional(),
32
+ limit: z.number().optional(),
33
+ cursor: z.string().optional(),
34
+ });
35
+
36
+ export const listClients = {
37
+ name: "estimator_list_clients",
38
+ description: "Search and list clients in the client book. Use the search param to find clients by name, phone, or email. Filter by companyId to see all contacts at a specific company. Returns client details with linked company info. Always search here before creating a new client to avoid duplicates.",
39
+ inputSchemaZod: listClientsSchema,
40
+ execute: async (args: z.infer<typeof listClientsSchema>): Promise<string> => {
41
+ if (!client.hasApiKey()) return client.noApiKeyMessage();
42
+
43
+ const params = new URLSearchParams();
44
+ if (args.search) params.set("search", args.search);
45
+ if (args.companyId) params.set("companyId", args.companyId);
46
+ if (args.limit != null) params.set("limit", String(args.limit));
47
+ if (args.cursor) params.set("cursor", args.cursor);
48
+
49
+ const q = params.toString();
50
+ const path = q ? `/estimator/clients?${q}` : "/estimator/clients";
51
+
52
+ const data = await client.get<unknown>(path);
53
+ return JSON.stringify(data);
54
+ },
55
+ };
56
+
57
+ // --- estimator_get_client ---
58
+
59
+ const getClientSchema = z.object({
60
+ clientId: z.string(),
61
+ });
62
+
63
+ export const getClient = {
64
+ name: "estimator_get_client",
65
+ description: "Get a specific client's full details including their company info and a summary of linked estimates (recent 10). Use this to see a client's estimate history before creating new ones.",
66
+ inputSchemaZod: getClientSchema,
67
+ execute: async (args: z.infer<typeof getClientSchema>): Promise<string> => {
68
+ if (!client.hasApiKey()) return client.noApiKeyMessage();
69
+ const data = await client.get<unknown>(`/estimator/clients/${args.clientId}`);
70
+ return JSON.stringify(data);
71
+ },
72
+ };
73
+
74
+ // --- estimator_update_client ---
75
+
76
+ const updateClientSchema = z.object({
77
+ clientId: z.string(),
78
+ name: z.string().optional(),
79
+ companyId: z.string().optional(),
80
+ title: z.string().optional(),
81
+ phone: z.string().optional(),
82
+ email: z.string().optional(),
83
+ address: z.string().optional(),
84
+ notes: z.string().optional(),
85
+ });
86
+
87
+ export const updateClient = {
88
+ name: "estimator_update_client",
89
+ description: "Update a client's contact details. Pass the clientId and any fields to change: name, title, phone, email, address, notes, or companyId (to move them to a different company).",
90
+ inputSchemaZod: updateClientSchema,
91
+ execute: async (args: z.infer<typeof updateClientSchema>): Promise<string> => {
92
+ if (!client.hasApiKey()) return client.noApiKeyMessage();
93
+ const { clientId, ...body } = args;
94
+ const data = await client.patch(`/estimator/clients/${clientId}`, body);
95
+ return JSON.stringify(data);
96
+ },
97
+ };
98
+
99
+ // --- estimator_delete_client ---
100
+
101
+ const deleteClientSchema = z.object({
102
+ clientId: z.string(),
103
+ });
104
+
105
+ export const deleteClient = {
106
+ name: "estimator_delete_client",
107
+ description: "Soft-delete a client from the client book. The client's data is preserved but hidden from searches. Existing estimates linked to this client are not affected.",
108
+ inputSchemaZod: deleteClientSchema,
109
+ execute: async (args: z.infer<typeof deleteClientSchema>): Promise<string> => {
110
+ if (!client.hasApiKey()) return client.noApiKeyMessage();
111
+ const data = await client.del(`/estimator/clients/${args.clientId}`);
112
+ return JSON.stringify(data);
113
+ },
114
+ };
@@ -0,0 +1,108 @@
1
+ import { z } from "zod";
2
+ import * as client from "../lib/kynver-client.js";
3
+
4
+ // --- estimator_create_company ---
5
+
6
+ const createCompanySchema = z.object({
7
+ name: z.string(),
8
+ phone: z.string().optional(),
9
+ email: z.string().optional(),
10
+ address: z.string().optional(),
11
+ notes: z.string().optional(),
12
+ });
13
+
14
+ export const createCompany = {
15
+ name: "estimator_create_company",
16
+ description: "Create a new company in the client book. Companies group clients (contacts) together. Include: name (required), phone, email, address, and notes. After creating, you can add clients to the company using estimator_create_client with the companyId.",
17
+ inputSchemaZod: createCompanySchema,
18
+ execute: async (args: z.infer<typeof createCompanySchema>): Promise<string> => {
19
+ if (!client.hasApiKey()) return client.noApiKeyMessage();
20
+ const data = await client.post("/estimator/companies", args);
21
+ return JSON.stringify(data);
22
+ },
23
+ };
24
+
25
+ // --- estimator_list_companies ---
26
+
27
+ const listCompaniesSchema = z.object({
28
+ search: z.string().optional(),
29
+ limit: z.number().optional(),
30
+ cursor: z.string().optional(),
31
+ });
32
+
33
+ export const listCompanies = {
34
+ name: "estimator_list_companies",
35
+ description: "Search and list companies in the client book. Use the search param to find companies by name. Returns company details with a count of contacts and estimates. Always search here before creating a new company to avoid duplicates.",
36
+ inputSchemaZod: listCompaniesSchema,
37
+ execute: async (args: z.infer<typeof listCompaniesSchema>): Promise<string> => {
38
+ if (!client.hasApiKey()) return client.noApiKeyMessage();
39
+
40
+ const params = new URLSearchParams();
41
+ if (args.search) params.set("search", args.search);
42
+ if (args.limit != null) params.set("limit", String(args.limit));
43
+ if (args.cursor) params.set("cursor", args.cursor);
44
+
45
+ const q = params.toString();
46
+ const path = q ? `/estimator/companies?${q}` : "/estimator/companies";
47
+
48
+ const data = await client.get<unknown>(path);
49
+ return JSON.stringify(data);
50
+ },
51
+ };
52
+
53
+ // --- estimator_get_company ---
54
+
55
+ const getCompanySchema = z.object({
56
+ companyId: z.string(),
57
+ });
58
+
59
+ export const getCompany = {
60
+ name: "estimator_get_company",
61
+ description: "Get a specific company's full details including all its client contacts and a summary of linked estimates.",
62
+ inputSchemaZod: getCompanySchema,
63
+ execute: async (args: z.infer<typeof getCompanySchema>): Promise<string> => {
64
+ if (!client.hasApiKey()) return client.noApiKeyMessage();
65
+ const data = await client.get<unknown>(`/estimator/companies/${args.companyId}`);
66
+ return JSON.stringify(data);
67
+ },
68
+ };
69
+
70
+ // --- estimator_update_company ---
71
+
72
+ const updateCompanySchema = z.object({
73
+ companyId: z.string(),
74
+ name: z.string().optional(),
75
+ phone: z.string().optional(),
76
+ email: z.string().optional(),
77
+ address: z.string().optional(),
78
+ notes: z.string().optional(),
79
+ });
80
+
81
+ export const updateCompany = {
82
+ name: "estimator_update_company",
83
+ description: "Update a company's details. Pass the companyId and any fields to change: name, phone, email, address, or notes.",
84
+ inputSchemaZod: updateCompanySchema,
85
+ execute: async (args: z.infer<typeof updateCompanySchema>): Promise<string> => {
86
+ if (!client.hasApiKey()) return client.noApiKeyMessage();
87
+ const { companyId, ...body } = args;
88
+ const data = await client.patch(`/estimator/companies/${companyId}`, body);
89
+ return JSON.stringify(data);
90
+ },
91
+ };
92
+
93
+ // --- estimator_delete_company ---
94
+
95
+ const deleteCompanySchema = z.object({
96
+ companyId: z.string(),
97
+ });
98
+
99
+ export const deleteCompany = {
100
+ name: "estimator_delete_company",
101
+ description: "Soft-delete a company from the client book. The company's data is preserved but hidden. Clients belonging to this company are NOT deleted — they become unlinked.",
102
+ inputSchemaZod: deleteCompanySchema,
103
+ execute: async (args: z.infer<typeof deleteCompanySchema>): Promise<string> => {
104
+ if (!client.hasApiKey()) return client.noApiKeyMessage();
105
+ const data = await client.del(`/estimator/companies/${args.companyId}`);
106
+ return JSON.stringify(data);
107
+ },
108
+ };
@@ -0,0 +1,281 @@
1
+ import { z } from "zod";
2
+ import * as client from "../lib/kynver-client.js";
3
+
4
+ // --- estimator_create_replacement ---
5
+
6
+ const createReplacementSchema = z.object({
7
+ projectName: z.string(),
8
+ roofMaterial: z.string(),
9
+ footprintSqFt: z.number().optional(),
10
+ roofAreaSqFt: z.number().optional(),
11
+ pitch: z.string().optional(),
12
+ layers: z.number().optional(),
13
+ ridgeLengthFt: z.number(),
14
+ hipLengthFt: z.number().optional(),
15
+ valleyLengthFt: z.number().optional(),
16
+ eavesFt: z.number(),
17
+ rakeFt: z.number().optional(),
18
+ wastePercent: z.number().optional(),
19
+ clientId: z.string().optional(),
20
+ companyId: z.string().optional(),
21
+ notes: z.string().optional(),
22
+ });
23
+
24
+ export const createReplacement = {
25
+ name: "estimator_create_replacement",
26
+ description:
27
+ "Create a full roof replacement estimate from dimensions. The system calculates line items automatically from the roof measurements using catalog prices. Requires: projectName, roofMaterial (asphalt, metal, tpo, etc.), ridgeLengthFt, eavesFt. Optional: footprintSqFt, roofAreaSqFt, pitch, layers, hipLengthFt, valleyLengthFt, rakeFt, wastePercent. PREREQUISITE: The user must have catalog items set up for the specified roofMaterial — call estimator_get_catalog first to verify.",
28
+ inputSchemaZod: createReplacementSchema,
29
+ execute: async (args: z.infer<typeof createReplacementSchema>): Promise<string> => {
30
+ if (!client.hasApiKey()) return client.noApiKeyMessage();
31
+ const { projectName, clientId, companyId, notes, ...measurements } = args;
32
+ const data = await client.post("/estimator/estimates", {
33
+ type: "replacement",
34
+ projectName,
35
+ roofMaterial: measurements.roofMaterial,
36
+ inputs: measurements,
37
+ clientId,
38
+ companyId,
39
+ notes,
40
+ });
41
+ return JSON.stringify(data);
42
+ },
43
+ };
44
+
45
+ // --- estimator_create_repair ---
46
+
47
+ const repairLineSchema = z.object({
48
+ serviceCode: z.string(),
49
+ materialType: z.string().nullable().optional(),
50
+ quantity: z.number(),
51
+ notes: z.string().optional(),
52
+ });
53
+
54
+ const createRepairSchema = z.object({
55
+ projectName: z.string(),
56
+ roofMaterial: z.string().optional(),
57
+ lineItems: z.array(repairLineSchema),
58
+ clientId: z.string().optional(),
59
+ companyId: z.string().optional(),
60
+ notes: z.string().optional(),
61
+ });
62
+
63
+ export const createRepair = {
64
+ name: "estimator_create_repair",
65
+ description:
66
+ "Create a repair/service estimate with specific line items. Each line item needs a serviceCode that exists in the user's catalog — ALWAYS call estimator_get_catalog first to get valid service codes. Do not guess or invent codes. Pass: projectName, lineItems array (each with serviceCode, quantity, optional materialType and notes). Optionally link to a client/company via clientId/companyId.",
67
+ inputSchemaZod: createRepairSchema,
68
+ execute: async (args: z.infer<typeof createRepairSchema>): Promise<string> => {
69
+ if (!client.hasApiKey()) return client.noApiKeyMessage();
70
+ const data = await client.post("/estimator/estimates", {
71
+ type: "repair_service",
72
+ projectName: args.projectName,
73
+ roofMaterial: args.roofMaterial,
74
+ lineItems: args.lineItems,
75
+ clientId: args.clientId,
76
+ companyId: args.companyId,
77
+ notes: args.notes,
78
+ });
79
+ return JSON.stringify(data);
80
+ },
81
+ };
82
+
83
+ // --- estimator_get_estimate ---
84
+
85
+ const getEstimateSchema = z.object({
86
+ id: z.string(),
87
+ });
88
+
89
+ export const getEstimate = {
90
+ name: "estimator_get_estimate",
91
+ description: "Get a specific estimate with all line items, totals (material, labor, grand total), status, client/company info, and metadata. Returns the complete estimate detail needed for viewing, editing, or reporting.",
92
+ inputSchemaZod: getEstimateSchema,
93
+ execute: async (args: z.infer<typeof getEstimateSchema>): Promise<string> => {
94
+ if (!client.hasApiKey()) return client.noApiKeyMessage();
95
+ const data = await client.get<unknown>(`/estimator/estimates/${args.id}`);
96
+ return JSON.stringify(data);
97
+ },
98
+ };
99
+
100
+ // --- estimator_list_estimates ---
101
+
102
+ const listEstimatesSchema = z.object({
103
+ status: z.string().optional(),
104
+ type: z.string().optional(),
105
+ clientId: z.string().optional(),
106
+ companyId: z.string().optional(),
107
+ from: z.string().optional(),
108
+ to: z.string().optional(),
109
+ limit: z.number().optional(),
110
+ cursor: z.string().optional(),
111
+ });
112
+
113
+ export const listEstimates = {
114
+ name: "estimator_list_estimates",
115
+ description:
116
+ "List the user's estimates with optional filters. Filter by: status (draft, sent, accepted, declined), type (replacement, repair_service), clientId, companyId, or date range (from/to as ISO strings like 2026-01-01). Returns summary info — use estimator_get_estimate for full details on a specific one.",
117
+ inputSchemaZod: listEstimatesSchema,
118
+ execute: async (args: z.infer<typeof listEstimatesSchema>): Promise<string> => {
119
+ if (!client.hasApiKey()) return client.noApiKeyMessage();
120
+
121
+ const params = new URLSearchParams();
122
+ if (args.status) params.set("status", args.status);
123
+ if (args.type) params.set("type", args.type);
124
+ if (args.clientId) params.set("clientId", args.clientId);
125
+ if (args.companyId) params.set("companyId", args.companyId);
126
+ if (args.from) params.set("from", args.from);
127
+ if (args.to) params.set("to", args.to);
128
+ if (args.limit != null) params.set("limit", String(args.limit));
129
+ if (args.cursor) params.set("cursor", args.cursor);
130
+
131
+ const q = params.toString();
132
+ const path = q ? `/estimator/estimates?${q}` : "/estimator/estimates";
133
+
134
+ const data = await client.get<unknown>(path);
135
+ return JSON.stringify(data);
136
+ },
137
+ };
138
+
139
+ // --- estimator_add_line_item ---
140
+
141
+ const addLineItemSchema = z.object({
142
+ estimateId: z.string(),
143
+ serviceCode: z.string(),
144
+ materialType: z.string().nullable().optional(),
145
+ quantity: z.number(),
146
+ notes: z.string().optional(),
147
+ });
148
+
149
+ export const addLineItem = {
150
+ name: "estimator_add_line_item",
151
+ description: "Add a line item to an existing estimate. The serviceCode must exist in the user's catalog — call estimator_get_catalog to verify. Pass: estimateId, serviceCode, quantity. Optionally add notes. The estimate totals are automatically recalculated.",
152
+ inputSchemaZod: addLineItemSchema,
153
+ execute: async (args: z.infer<typeof addLineItemSchema>): Promise<string> => {
154
+ if (!client.hasApiKey()) return client.noApiKeyMessage();
155
+ const { estimateId, ...body } = args;
156
+ const data = await client.post(`/estimator/estimates/${estimateId}/line-items`, body);
157
+ return JSON.stringify(data);
158
+ },
159
+ };
160
+
161
+ // --- estimator_remove_line_item ---
162
+
163
+ const removeLineItemSchema = z.object({
164
+ estimateId: z.string(),
165
+ lineItemId: z.string(),
166
+ });
167
+
168
+ export const removeLineItem = {
169
+ name: "estimator_remove_line_item",
170
+ description: "Remove a line item from an estimate. Requires estimateId and lineItemId (get lineItemIds from estimator_get_estimate). The estimate totals are automatically recalculated.",
171
+ inputSchemaZod: removeLineItemSchema,
172
+ execute: async (args: z.infer<typeof removeLineItemSchema>): Promise<string> => {
173
+ if (!client.hasApiKey()) return client.noApiKeyMessage();
174
+ const data = await client.del(`/estimator/estimates/${args.estimateId}/line-items/${args.lineItemId}`);
175
+ return JSON.stringify(data);
176
+ },
177
+ };
178
+
179
+ // --- estimator_update_line_item ---
180
+
181
+ const updateLineItemSchema = z.object({
182
+ estimateId: z.string(),
183
+ lineItemId: z.string(),
184
+ quantity: z.number().optional(),
185
+ materialCost: z.number().optional(),
186
+ laborCost: z.number().optional(),
187
+ notes: z.string().optional(),
188
+ });
189
+
190
+ export const updateLineItem = {
191
+ name: "estimator_update_line_item",
192
+ description: "Update a line item's quantity, materialCost, laborCost, or notes on an existing estimate. Requires estimateId and lineItemId. Totals are recalculated automatically after the update.",
193
+ inputSchemaZod: updateLineItemSchema,
194
+ execute: async (args: z.infer<typeof updateLineItemSchema>): Promise<string> => {
195
+ if (!client.hasApiKey()) return client.noApiKeyMessage();
196
+ const { estimateId, lineItemId, ...body } = args;
197
+ const data = await client.patch(`/estimator/estimates/${estimateId}/line-items/${lineItemId}`, body);
198
+ return JSON.stringify(data);
199
+ },
200
+ };
201
+
202
+ // --- estimator_adjust_to_target ---
203
+
204
+ const adjustToTargetSchema = z.object({
205
+ estimateId: z.string(),
206
+ targetTotal: z.number(),
207
+ strategy: z.enum(["uniform", "labor_only", "material_only"]).optional(),
208
+ });
209
+
210
+ export const adjustToTarget = {
211
+ name: "estimator_adjust_to_target",
212
+ description:
213
+ "Adjust all line items on an estimate to hit a specific target total price. Useful when a customer has a budget and you need to scale pricing to fit. Strategies: 'uniform' (default — scales all items proportionally), 'labor_only' (only adjusts labor costs), 'material_only' (only adjusts material costs). Requires estimateId and targetTotal.",
214
+ inputSchemaZod: adjustToTargetSchema,
215
+ execute: async (args: z.infer<typeof adjustToTargetSchema>): Promise<string> => {
216
+ if (!client.hasApiKey()) return client.noApiKeyMessage();
217
+ const { estimateId, ...body } = args;
218
+ const data = await client.post(`/estimator/estimates/${estimateId}/adjust`, body);
219
+ return JSON.stringify(data);
220
+ },
221
+ };
222
+
223
+ // --- estimator_update_estimate ---
224
+
225
+ const updateEstimateSchema = z.object({
226
+ estimateId: z.string(),
227
+ projectName: z.string().optional(),
228
+ status: z.string().optional(),
229
+ clientId: z.string().optional(),
230
+ companyId: z.string().optional(),
231
+ notes: z.string().optional(),
232
+ siteAddress: z.string().optional(),
233
+ });
234
+
235
+ export const updateEstimate = {
236
+ name: "estimator_update_estimate",
237
+ description: "Update an estimate's metadata — project name, status (draft/sent/accepted/declined), site address, client/company linking, or notes. Does NOT modify line items (use add/remove/update line item tools for that). Pass estimateId and any fields to change.",
238
+ inputSchemaZod: updateEstimateSchema,
239
+ execute: async (args: z.infer<typeof updateEstimateSchema>): Promise<string> => {
240
+ if (!client.hasApiKey()) return client.noApiKeyMessage();
241
+ const { estimateId, ...body } = args;
242
+ const data = await client.patch(`/estimator/estimates/${estimateId}`, body);
243
+ return JSON.stringify(data);
244
+ },
245
+ };
246
+
247
+ // --- estimator_delete_estimate ---
248
+
249
+ const deleteEstimateSchema = z.object({
250
+ estimateId: z.string(),
251
+ });
252
+
253
+ export const deleteEstimate = {
254
+ name: "estimator_delete_estimate",
255
+ description: "Soft-delete an estimate. The estimate data is preserved but hidden from list views. This cannot be easily undone.",
256
+ inputSchemaZod: deleteEstimateSchema,
257
+ execute: async (args: z.infer<typeof deleteEstimateSchema>): Promise<string> => {
258
+ if (!client.hasApiKey()) return client.noApiKeyMessage();
259
+ const data = await client.del(`/estimator/estimates/${args.estimateId}`);
260
+ return JSON.stringify(data);
261
+ },
262
+ };
263
+
264
+ // --- estimator_duplicate_estimate ---
265
+
266
+ const duplicateEstimateSchema = z.object({
267
+ estimateId: z.string(),
268
+ projectName: z.string().optional(),
269
+ });
270
+
271
+ export const duplicateEstimate = {
272
+ name: "estimator_duplicate_estimate",
273
+ description: "Create a copy of an existing estimate with all its line items, pricing, and configuration. Optionally give the copy a new projectName. Useful for creating similar estimates for different clients or properties.",
274
+ inputSchemaZod: duplicateEstimateSchema,
275
+ execute: async (args: z.infer<typeof duplicateEstimateSchema>): Promise<string> => {
276
+ if (!client.hasApiKey()) return client.noApiKeyMessage();
277
+ const { estimateId, ...body } = args;
278
+ const data = await client.post(`/estimator/estimates/${estimateId}/duplicate`, body);
279
+ return JSON.stringify(data);
280
+ },
281
+ };
@@ -0,0 +1,60 @@
1
+ import { z } from "zod";
2
+ import * as client from "../lib/kynver-client.js";
3
+
4
+ // --- estimator_upload_photo ---
5
+
6
+ const uploadPhotoSchema = z.object({
7
+ estimateId: z.string(),
8
+ imageUrl: z.string(),
9
+ caption: z.string().optional(),
10
+ });
11
+
12
+ export const uploadPhoto = {
13
+ name: "estimator_upload_photo",
14
+ description:
15
+ "Attach a photo to an estimate by URL. The photo is downloaded and stored. Include a caption to describe what the photo shows (e.g., 'North-facing roof slope showing damaged shingles'). Photos can be included in PDF exports and analyzed with estimator_analyze_photos.",
16
+ inputSchemaZod: uploadPhotoSchema,
17
+ execute: async (args: z.infer<typeof uploadPhotoSchema>): Promise<string> => {
18
+ if (!client.hasApiKey()) return client.noApiKeyMessage();
19
+ const { estimateId, ...body } = args;
20
+ const data = await client.post(`/estimator/estimates/${estimateId}/photos`, body);
21
+ return JSON.stringify(data);
22
+ },
23
+ };
24
+
25
+ // --- estimator_list_photos ---
26
+
27
+ const listPhotosSchema = z.object({
28
+ estimateId: z.string(),
29
+ });
30
+
31
+ export const listPhotos = {
32
+ name: "estimator_list_photos",
33
+ description: "List all photos attached to an estimate. Returns photo IDs, URLs, captions, and sort order. Use photo IDs when deleting photos or configuring PDF output.",
34
+ inputSchemaZod: listPhotosSchema,
35
+ execute: async (args: z.infer<typeof listPhotosSchema>): Promise<string> => {
36
+ if (!client.hasApiKey()) return client.noApiKeyMessage();
37
+ const data = await client.get<unknown>(`/estimator/estimates/${args.estimateId}/photos`);
38
+ return JSON.stringify(data);
39
+ },
40
+ };
41
+
42
+ // --- estimator_delete_photo ---
43
+
44
+ const deletePhotoSchema = z.object({
45
+ estimateId: z.string(),
46
+ photoId: z.string(),
47
+ });
48
+
49
+ export const deletePhoto = {
50
+ name: "estimator_delete_photo",
51
+ description: "Remove a photo from an estimate. Requires estimateId and photoId (get IDs from estimator_list_photos).",
52
+ inputSchemaZod: deletePhotoSchema,
53
+ execute: async (args: z.infer<typeof deletePhotoSchema>): Promise<string> => {
54
+ if (!client.hasApiKey()) return client.noApiKeyMessage();
55
+ const data = await client.del(
56
+ `/estimator/estimates/${args.estimateId}/photos/${args.photoId}`
57
+ );
58
+ return JSON.stringify(data);
59
+ },
60
+ };
@@ -0,0 +1,80 @@
1
+ import { z } from "zod";
2
+ import * as client from "../lib/kynver-client.js";
3
+
4
+ // --- estimator_get_stats ---
5
+
6
+ const getStatsSchema = z.object({
7
+ from: z.string().optional(),
8
+ to: z.string().optional(),
9
+ });
10
+
11
+ export const getStats = {
12
+ name: "estimator_get_stats",
13
+ description: "Get aggregate statistics for the user's estimating business. Returns: total estimates count, estimates by status, total revenue (accepted estimates), average estimate value, estimates by type (replacement vs repair). Optionally filter by date range (from/to as ISO strings). Use this for dashboard data and business reporting.",
14
+ inputSchemaZod: getStatsSchema,
15
+ execute: async (args: z.infer<typeof getStatsSchema>): Promise<string> => {
16
+ if (!client.hasApiKey()) return client.noApiKeyMessage();
17
+
18
+ const params = new URLSearchParams();
19
+ if (args.from) params.set("from", args.from);
20
+ if (args.to) params.set("to", args.to);
21
+
22
+ const q = params.toString();
23
+ const path = q ? `/estimator/stats?${q}` : "/estimator/stats";
24
+
25
+ const data = await client.get<unknown>(path);
26
+ return JSON.stringify(data);
27
+ },
28
+ };
29
+
30
+ // --- estimator_export_pdf ---
31
+
32
+ const exportPdfSchema = z.object({
33
+ estimateId: z.string(),
34
+ });
35
+
36
+ export const exportPdf = {
37
+ name: "estimator_export_pdf",
38
+ description:
39
+ "Generate a PDF document for an estimate. Returns a download URL (not the binary file). The PDF layout is controlled by the estimate's output config — use estimator_update_output_config to customize which columns, sections, and details appear. The PDF includes company branding from estimator config.",
40
+ inputSchemaZod: exportPdfSchema,
41
+ execute: async (args: z.infer<typeof exportPdfSchema>): Promise<string> => {
42
+ if (!client.hasApiKey()) return client.noApiKeyMessage();
43
+ // Trigger PDF generation on the server side
44
+ try {
45
+ await client.get<unknown>(`/estimator/estimates/${args.estimateId}/pdf`);
46
+ } catch {
47
+ // Even if the binary response fails to parse, the PDF was generated
48
+ }
49
+ return JSON.stringify({
50
+ message: "PDF generated successfully.",
51
+ downloadUrl: `/api/estimator/estimates/${args.estimateId}/pdf`,
52
+ });
53
+ },
54
+ };
55
+
56
+ // --- estimator_update_output_config ---
57
+
58
+ const updateOutputConfigSchema = z.object({
59
+ estimateId: z.string(),
60
+ showMaterialCost: z.boolean().optional(),
61
+ showLaborCost: z.boolean().optional(),
62
+ showUnitPrice: z.boolean().optional(),
63
+ showQuantity: z.boolean().optional(),
64
+ showNotes: z.boolean().optional(),
65
+ headerText: z.string().optional(),
66
+ footerText: z.string().optional(),
67
+ template: z.string().optional(),
68
+ });
69
+
70
+ export const updateOutputConfig = {
71
+ name: "estimator_update_output_config",
72
+ description: "Configure what appears in an estimate's PDF output. Toggle visibility of: material costs, labor costs, unit prices, quantities, line item notes. Set custom header/footer text. Choose a template style. This controls the presentation layer — the underlying estimate data is not changed.",
73
+ inputSchemaZod: updateOutputConfigSchema,
74
+ execute: async (args: z.infer<typeof updateOutputConfigSchema>): Promise<string> => {
75
+ if (!client.hasApiKey()) return client.noApiKeyMessage();
76
+ const { estimateId, ...body } = args;
77
+ const data = await client.patch(`/estimator/estimates/${estimateId}/output-config`, body);
78
+ return JSON.stringify(data);
79
+ },
80
+ };