@kynver-app/mcp-estimator 0.2.3 → 0.2.4
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/package.json
CHANGED
package/src/server.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
7
|
import * as catalogTools from "./tools/catalog-tools.js";
|
|
8
|
+
import * as priceCatalogTools from "./tools/price-catalog-tools.js";
|
|
8
9
|
import * as estimateTools from "./tools/estimate-tools.js";
|
|
9
10
|
import * as templateTools from "./tools/template-tools.js";
|
|
10
11
|
import * as versionTools from "./tools/version-tools.js";
|
|
@@ -14,6 +15,8 @@ import * as visionTools from "./tools/vision-tools.js";
|
|
|
14
15
|
import * as companyTools from "./tools/company-tools.js";
|
|
15
16
|
import * as clientTools from "./tools/client-tools.js";
|
|
16
17
|
import * as takeoffTools from "./tools/takeoff-tools.js";
|
|
18
|
+
import * as intakeTools from "./tools/intake-tools.js";
|
|
19
|
+
import * as remodelEstimateTools from "./tools/remodel-estimate-tools.js";
|
|
17
20
|
import { readOwnPackageVersion } from "./lib/package-version.js";
|
|
18
21
|
|
|
19
22
|
const PACKAGE_VERSION = readOwnPackageVersion(import.meta.url);
|
|
@@ -50,6 +53,12 @@ export function createEstimatorServer(): McpServer {
|
|
|
50
53
|
register(catalogTools.updateConfig.name, { description: catalogTools.updateConfig.description, inputSchema: catalogTools.updateConfig.inputSchemaZod }, (args) => catalogTools.updateConfig.execute(args as any));
|
|
51
54
|
register(catalogTools.importTemplate.name, { description: catalogTools.importTemplate.description, inputSchema: catalogTools.importTemplate.inputSchemaZod }, (args) => catalogTools.importTemplate.execute(args as any));
|
|
52
55
|
|
|
56
|
+
// --- Price catalog tools (4) ---
|
|
57
|
+
register(priceCatalogTools.listPriceCatalogs.name, { description: priceCatalogTools.listPriceCatalogs.description, inputSchema: priceCatalogTools.listPriceCatalogs.inputSchemaZod }, (args) => priceCatalogTools.listPriceCatalogs.execute(args as any));
|
|
58
|
+
register(priceCatalogTools.getPriceCatalog.name, { description: priceCatalogTools.getPriceCatalog.description, inputSchema: priceCatalogTools.getPriceCatalog.inputSchemaZod }, (args) => priceCatalogTools.getPriceCatalog.execute(args as any));
|
|
59
|
+
register(priceCatalogTools.resolvePrice.name, { description: priceCatalogTools.resolvePrice.description, inputSchema: priceCatalogTools.resolvePrice.inputSchemaZod }, (args) => priceCatalogTools.resolvePrice.execute(args as any));
|
|
60
|
+
register(priceCatalogTools.seedPriceCatalog.name, { description: priceCatalogTools.seedPriceCatalog.description, inputSchema: priceCatalogTools.seedPriceCatalog.inputSchemaZod }, (args) => priceCatalogTools.seedPriceCatalog.execute(args as any));
|
|
61
|
+
|
|
53
62
|
// --- Estimate tools (11) ---
|
|
54
63
|
register(estimateTools.createReplacement.name, { description: estimateTools.createReplacement.description, inputSchema: estimateTools.createReplacement.inputSchemaZod }, (args) => estimateTools.createReplacement.execute(args as any));
|
|
55
64
|
register(estimateTools.createRepair.name, { description: estimateTools.createRepair.description, inputSchema: estimateTools.createRepair.inputSchemaZod }, (args) => estimateTools.createRepair.execute(args as any));
|
|
@@ -62,6 +71,7 @@ export function createEstimatorServer(): McpServer {
|
|
|
62
71
|
register(estimateTools.updateEstimate.name, { description: estimateTools.updateEstimate.description, inputSchema: estimateTools.updateEstimate.inputSchemaZod }, (args) => estimateTools.updateEstimate.execute(args as any));
|
|
63
72
|
register(estimateTools.deleteEstimate.name, { description: estimateTools.deleteEstimate.description, inputSchema: estimateTools.deleteEstimate.inputSchemaZod }, (args) => estimateTools.deleteEstimate.execute(args as any));
|
|
64
73
|
register(estimateTools.duplicateEstimate.name, { description: estimateTools.duplicateEstimate.description, inputSchema: estimateTools.duplicateEstimate.inputSchemaZod }, (args) => estimateTools.duplicateEstimate.execute(args as any));
|
|
74
|
+
register(remodelEstimateTools.buildRemodelEstimate.name, { description: remodelEstimateTools.buildRemodelEstimate.description, inputSchema: remodelEstimateTools.buildRemodelEstimate.inputSchemaZod }, (args) => remodelEstimateTools.buildRemodelEstimate.execute(args as any));
|
|
65
75
|
|
|
66
76
|
// --- Template tools (4) ---
|
|
67
77
|
register(templateTools.saveTemplate.name, { description: templateTools.saveTemplate.description, inputSchema: templateTools.saveTemplate.inputSchemaZod }, (args) => templateTools.saveTemplate.execute(args as any));
|
|
@@ -119,5 +129,8 @@ export function createEstimatorServer(): McpServer {
|
|
|
119
129
|
register(takeoffTools.approveEstimate.name, { description: takeoffTools.approveEstimate.description, inputSchema: takeoffTools.approveEstimate.inputSchemaZod }, (args) => takeoffTools.approveEstimate.execute(args as any));
|
|
120
130
|
register(takeoffTools.getEstimateApproval.name, { description: takeoffTools.getEstimateApproval.description, inputSchema: takeoffTools.getEstimateApproval.inputSchemaZod }, (args) => takeoffTools.getEstimateApproval.execute(args as any));
|
|
121
131
|
|
|
132
|
+
// --- Intake tools (1) ---
|
|
133
|
+
register(intakeTools.normalizeRemodelIntake.name, { description: intakeTools.normalizeRemodelIntake.description, inputSchema: intakeTools.normalizeRemodelIntake.inputSchemaZod }, (args) => intakeTools.normalizeRemodelIntake.execute(args as any));
|
|
134
|
+
|
|
122
135
|
return server;
|
|
123
136
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import * as client from "../lib/kynver-client.js";
|
|
3
|
+
|
|
4
|
+
const remodelProjectTypeSchema = z.enum([
|
|
5
|
+
"kitchen_remodel",
|
|
6
|
+
"bath_remodel",
|
|
7
|
+
"kitchen_and_bath_remodel",
|
|
8
|
+
"whole_home_remodel",
|
|
9
|
+
"other_remodel",
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
const normalizeRemodelIntakeSchema = z.object({
|
|
13
|
+
projectType: remodelProjectTypeSchema,
|
|
14
|
+
projectName: z.string().optional(),
|
|
15
|
+
siteAddress: z.string().optional(),
|
|
16
|
+
baseScopes: z
|
|
17
|
+
.array(
|
|
18
|
+
z.object({
|
|
19
|
+
key: z.string().optional(),
|
|
20
|
+
label: z.string(),
|
|
21
|
+
quantity: z.number().optional(),
|
|
22
|
+
unit: z.string().optional(),
|
|
23
|
+
serviceCode: z.string().optional(),
|
|
24
|
+
budgetLump: z.number().optional(),
|
|
25
|
+
notes: z.string().optional(),
|
|
26
|
+
confidence: z.enum(["high", "medium", "low", "unknown"]).optional(),
|
|
27
|
+
isAlternate: z.boolean().optional(),
|
|
28
|
+
alternateGroup: z.string().optional(),
|
|
29
|
+
})
|
|
30
|
+
)
|
|
31
|
+
.optional(),
|
|
32
|
+
alternates: z
|
|
33
|
+
.array(
|
|
34
|
+
z.object({
|
|
35
|
+
key: z.string(),
|
|
36
|
+
label: z.string(),
|
|
37
|
+
scopeKeys: z.array(z.string()).optional(),
|
|
38
|
+
lumpAdd: z.number().optional(),
|
|
39
|
+
notes: z.string().optional(),
|
|
40
|
+
})
|
|
41
|
+
)
|
|
42
|
+
.optional(),
|
|
43
|
+
attachments: z
|
|
44
|
+
.array(
|
|
45
|
+
z.object({
|
|
46
|
+
id: z.string(),
|
|
47
|
+
kind: z.enum(["photo", "drawing", "plan_set", "spec_pdf", "spreadsheet", "other"]),
|
|
48
|
+
fileName: z.string().optional(),
|
|
49
|
+
caption: z.string().optional(),
|
|
50
|
+
url: z.string().optional(),
|
|
51
|
+
mimeType: z.string().optional(),
|
|
52
|
+
})
|
|
53
|
+
)
|
|
54
|
+
.optional(),
|
|
55
|
+
exclusions: z.array(z.string()).optional(),
|
|
56
|
+
surprises: z.array(z.string()).optional(),
|
|
57
|
+
rateReferences: z
|
|
58
|
+
.array(
|
|
59
|
+
z.object({
|
|
60
|
+
key: z.string(),
|
|
61
|
+
label: z.string(),
|
|
62
|
+
value: z.number(),
|
|
63
|
+
unit: z.string().optional(),
|
|
64
|
+
source: z.enum(["operating_rule", "catalog", "user", "takeoff"]),
|
|
65
|
+
catalogId: z.string().optional(),
|
|
66
|
+
confidence: z.enum(["high", "medium", "low", "unknown"]).optional(),
|
|
67
|
+
})
|
|
68
|
+
)
|
|
69
|
+
.optional(),
|
|
70
|
+
assumptionsNeedingReview: z
|
|
71
|
+
.array(
|
|
72
|
+
z.object({
|
|
73
|
+
id: z.string(),
|
|
74
|
+
text: z.string(),
|
|
75
|
+
severity: z.enum(["info", "warn", "blocker"]),
|
|
76
|
+
relatedScopeKey: z.string().optional(),
|
|
77
|
+
})
|
|
78
|
+
)
|
|
79
|
+
.optional(),
|
|
80
|
+
remodelHints: z.record(z.string(), z.unknown()).optional(),
|
|
81
|
+
includeStandardAlternates: z.boolean().optional(),
|
|
82
|
+
notes: z.string().optional(),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export const normalizeRemodelIntake = {
|
|
86
|
+
name: "estimator_normalize_remodel_intake",
|
|
87
|
+
description:
|
|
88
|
+
"Validate and normalize a residential remodel intake payload for kitchen/bath estimating. Accepts project type, base scopes vs alternates, drawing/photo attachment metadata, exclusions/surprises, rate references, and optional remodelHints from takeoff. Returns provenance-backed scopes, Will Portland default rates, assumptions needing review, and an estimate-builder-ready payload.",
|
|
89
|
+
inputSchemaZod: normalizeRemodelIntakeSchema,
|
|
90
|
+
execute: async (args: z.infer<typeof normalizeRemodelIntakeSchema>): Promise<string> => {
|
|
91
|
+
if (!client.hasApiKey()) return client.noApiKeyMessage();
|
|
92
|
+
const data = await client.post("/estimator/intake/normalize-remodel", args);
|
|
93
|
+
return JSON.stringify(data, null, 2);
|
|
94
|
+
},
|
|
95
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import * as client from "../lib/kynver-client.js";
|
|
3
|
+
|
|
4
|
+
const listPriceCatalogsSchema = z.object({});
|
|
5
|
+
|
|
6
|
+
export const listPriceCatalogs = {
|
|
7
|
+
name: "estimator_list_price_catalogs",
|
|
8
|
+
description:
|
|
9
|
+
"List durable builtin price catalogs shipped with the estimator (region, effective date, markup policy). These are the MCP's stored price lists — not AgentOS memory. Use before seeding or resolving remodel defaults.",
|
|
10
|
+
inputSchemaZod: listPriceCatalogsSchema,
|
|
11
|
+
execute: async (_args: z.infer<typeof listPriceCatalogsSchema>): Promise<string> => {
|
|
12
|
+
if (!client.hasApiKey()) return client.noApiKeyMessage();
|
|
13
|
+
const data = await client.get("/estimator/price-catalog");
|
|
14
|
+
return JSON.stringify(data);
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const getPriceCatalogSchema = z.object({
|
|
19
|
+
catalogId: z.string(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const getPriceCatalog = {
|
|
23
|
+
name: "estimator_get_price_catalog",
|
|
24
|
+
description:
|
|
25
|
+
"Read all unit-price lines from a builtin price catalog (service codes, units, material/labor split, review/confidence metadata). Default Portland remodel catalog id: portland-remodel-will-2026-06.",
|
|
26
|
+
inputSchemaZod: getPriceCatalogSchema,
|
|
27
|
+
execute: async (args: z.infer<typeof getPriceCatalogSchema>): Promise<string> => {
|
|
28
|
+
if (!client.hasApiKey()) return client.noApiKeyMessage();
|
|
29
|
+
const data = await client.get(`/estimator/price-catalog/${encodeURIComponent(args.catalogId)}`);
|
|
30
|
+
return JSON.stringify(data);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const resolvePriceSchema = z.object({
|
|
35
|
+
serviceCode: z.string(),
|
|
36
|
+
materialType: z.string().nullable().optional(),
|
|
37
|
+
region: z.string().optional(),
|
|
38
|
+
overrideMaterialCost: z.number().min(0).optional(),
|
|
39
|
+
overrideLaborCost: z.number().min(0).optional(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export const resolvePrice = {
|
|
43
|
+
name: "estimator_resolve_price",
|
|
44
|
+
description:
|
|
45
|
+
"Resolve unit pricing for one service code with provenance: user price book → builtin catalog → optional intake override (overrideMaterialCost/overrideLaborCost). Returns source, region, effective date, and markupIncluded flag.",
|
|
46
|
+
inputSchemaZod: resolvePriceSchema,
|
|
47
|
+
execute: async (args: z.infer<typeof resolvePriceSchema>): Promise<string> => {
|
|
48
|
+
if (!client.hasApiKey()) return client.noApiKeyMessage();
|
|
49
|
+
|
|
50
|
+
const params = new URLSearchParams({ serviceCode: args.serviceCode });
|
|
51
|
+
if (args.materialType) params.set("materialType", args.materialType);
|
|
52
|
+
if (args.region) params.set("region", args.region);
|
|
53
|
+
if (args.overrideMaterialCost != null) {
|
|
54
|
+
params.set("overrideMaterialCost", String(args.overrideMaterialCost));
|
|
55
|
+
}
|
|
56
|
+
if (args.overrideLaborCost != null) {
|
|
57
|
+
params.set("overrideLaborCost", String(args.overrideLaborCost));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const data = await client.get(`/estimator/price-catalog/resolve?${params.toString()}`);
|
|
61
|
+
return JSON.stringify(data);
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const seedPriceCatalogSchema = z.object({
|
|
66
|
+
catalogId: z.string(),
|
|
67
|
+
overwrite: z.boolean().optional(),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
export const seedPriceCatalog = {
|
|
71
|
+
name: "estimator_seed_price_catalog",
|
|
72
|
+
description:
|
|
73
|
+
"Copy a builtin price catalog into the user's ServiceCatalogItem price book (skips existing codes unless overwrite=true). Use portland-remodel-will-2026-06 for Will's Portland remodel working rates.",
|
|
74
|
+
inputSchemaZod: seedPriceCatalogSchema,
|
|
75
|
+
execute: async (args: z.infer<typeof seedPriceCatalogSchema>): Promise<string> => {
|
|
76
|
+
if (!client.hasApiKey()) return client.noApiKeyMessage();
|
|
77
|
+
const data = await client.post("/estimator/price-catalog/seed", args);
|
|
78
|
+
return JSON.stringify(data);
|
|
79
|
+
},
|
|
80
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import * as client from "../lib/kynver-client.js";
|
|
3
|
+
|
|
4
|
+
const scopeSchema = z.object({
|
|
5
|
+
key: z.string(),
|
|
6
|
+
label: z.string().optional(),
|
|
7
|
+
quantity: z.number().optional(),
|
|
8
|
+
unit: z.string().optional(),
|
|
9
|
+
serviceCode: z.string().optional(),
|
|
10
|
+
budgetLump: z.number().optional(),
|
|
11
|
+
notes: z.string().optional(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const buildRemodelSchema = z.object({
|
|
15
|
+
intake: z.object({
|
|
16
|
+
projectName: z.string().optional(),
|
|
17
|
+
scopes: z.array(scopeSchema),
|
|
18
|
+
alternates: z
|
|
19
|
+
.array(
|
|
20
|
+
z.object({
|
|
21
|
+
key: z.string(),
|
|
22
|
+
label: z.string(),
|
|
23
|
+
scopeKeys: z.array(z.string()).optional(),
|
|
24
|
+
lumpAdd: z.number().optional(),
|
|
25
|
+
notes: z.string().optional(),
|
|
26
|
+
})
|
|
27
|
+
)
|
|
28
|
+
.optional(),
|
|
29
|
+
includeStandardAlternates: z.boolean().optional(),
|
|
30
|
+
exclusions: z.array(z.string()).optional(),
|
|
31
|
+
notes: z.string().optional(),
|
|
32
|
+
}),
|
|
33
|
+
takeoffRows: z
|
|
34
|
+
.array(
|
|
35
|
+
z.object({
|
|
36
|
+
id: z.string().optional(),
|
|
37
|
+
description: z.string(),
|
|
38
|
+
quantity: z.number(),
|
|
39
|
+
unit: z.string(),
|
|
40
|
+
serviceCode: z.string().nullable().optional(),
|
|
41
|
+
materialType: z.string().nullable().optional(),
|
|
42
|
+
})
|
|
43
|
+
)
|
|
44
|
+
.optional(),
|
|
45
|
+
overrides: z
|
|
46
|
+
.array(
|
|
47
|
+
z.object({
|
|
48
|
+
scopeKey: z.string().optional(),
|
|
49
|
+
serviceCode: z.string().optional(),
|
|
50
|
+
description: z.string().optional(),
|
|
51
|
+
quantity: z.number().optional(),
|
|
52
|
+
unitMaterial: z.number().optional(),
|
|
53
|
+
unitLabor: z.number().optional(),
|
|
54
|
+
lumpTotal: z.number().optional(),
|
|
55
|
+
notes: z.string().optional(),
|
|
56
|
+
})
|
|
57
|
+
)
|
|
58
|
+
.optional(),
|
|
59
|
+
pricing: z
|
|
60
|
+
.object({
|
|
61
|
+
contingencyPercent: z.number().optional(),
|
|
62
|
+
markupPercent: z.number().optional(),
|
|
63
|
+
profitPercent: z.number().optional(),
|
|
64
|
+
})
|
|
65
|
+
.optional(),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export const buildRemodelEstimate = {
|
|
69
|
+
name: "estimator_build_remodel_estimate",
|
|
70
|
+
description:
|
|
71
|
+
"Build a reviewable residential remodel estimate from normalized intake scopes, optional takeoff quantities, catalog prices, and overrides. Returns line items with provenance, alternates (both baths, upstairs expansion, roof, wall moving), exclusions, contingency/markup/profit breakdown, and reviewMarkdown. Does not persist — use estimator_generate_estimate_from_takeoff or estimator_create_repair after review. Scope keys: kitchen, bath_full, bath_half, bath_primary, bath_secondary, cabinets, drywall, paint, framing, roof, flooring, electrical_kitchen, electrical_bath.",
|
|
72
|
+
inputSchemaZod: buildRemodelSchema,
|
|
73
|
+
execute: async (args: z.infer<typeof buildRemodelSchema>): Promise<string> => {
|
|
74
|
+
if (!client.hasApiKey()) return client.noApiKeyMessage();
|
|
75
|
+
const data = await client.post("/estimator/estimates/build-remodel", args);
|
|
76
|
+
return JSON.stringify(data);
|
|
77
|
+
},
|
|
78
|
+
};
|
|
@@ -13,12 +13,49 @@ const createTakeoffProjectSchema = z.object({
|
|
|
13
13
|
|
|
14
14
|
const listTakeoffProjectsSchema = z.object({});
|
|
15
15
|
|
|
16
|
+
const remodelHintsSchema = z
|
|
17
|
+
.object({
|
|
18
|
+
cabinetLinearFt: z.number().nonnegative().optional(),
|
|
19
|
+
rooms: z
|
|
20
|
+
.array(
|
|
21
|
+
z.object({
|
|
22
|
+
name: z.string(),
|
|
23
|
+
lengthFt: z.number().positive().optional(),
|
|
24
|
+
widthFt: z.number().positive().optional(),
|
|
25
|
+
})
|
|
26
|
+
)
|
|
27
|
+
.optional(),
|
|
28
|
+
baths: z
|
|
29
|
+
.array(
|
|
30
|
+
z.object({
|
|
31
|
+
type: z.enum(["full", "half", "three_quarter", "unknown"]).optional(),
|
|
32
|
+
count: z.number().int().positive().optional(),
|
|
33
|
+
label: z.string().optional(),
|
|
34
|
+
})
|
|
35
|
+
)
|
|
36
|
+
.optional(),
|
|
37
|
+
flooringSqFt: z.number().nonnegative().optional(),
|
|
38
|
+
drywallSqFt: z.number().nonnegative().optional(),
|
|
39
|
+
paintSqFt: z.number().nonnegative().optional(),
|
|
40
|
+
roofSqFt: z.number().nonnegative().optional(),
|
|
41
|
+
flags: z
|
|
42
|
+
.object({
|
|
43
|
+
wallMovingRisk: z.boolean().optional(),
|
|
44
|
+
electricalSurpriseRisk: z.boolean().optional(),
|
|
45
|
+
plumbingSurpriseRisk: z.boolean().optional(),
|
|
46
|
+
})
|
|
47
|
+
.optional(),
|
|
48
|
+
notes: z.string().optional(),
|
|
49
|
+
})
|
|
50
|
+
.optional();
|
|
51
|
+
|
|
16
52
|
const uploadTakeoffInputSchema = z.object({
|
|
17
53
|
projectId: z.string().uuid(),
|
|
18
54
|
kind: z.string(),
|
|
19
55
|
fileName: z.string(),
|
|
20
56
|
base64: z.string(),
|
|
21
57
|
mimeType: z.string().optional(),
|
|
58
|
+
remodelHints: remodelHintsSchema,
|
|
22
59
|
});
|
|
23
60
|
|
|
24
61
|
const listTakeoffRowsSchema = z.object({
|
|
@@ -119,7 +156,7 @@ export const listTakeoffProjects = {
|
|
|
119
156
|
export const uploadTakeoffInput = {
|
|
120
157
|
name: "estimator_upload_takeoff_input",
|
|
121
158
|
description:
|
|
122
|
-
"Upload a takeoff input file (base64) for a project. kind: eagleview_pdf|eagleview_xml|eagleview_json|eagleview_csv|spec_pdf|spreadsheet|photo|voice|other. For remodel
|
|
159
|
+
"Upload a takeoff input file (base64) for a project. kind: eagleview_pdf|eagleview_xml|eagleview_json|eagleview_csv|spec_pdf|spreadsheet|photo|voice|other. For remodel photo/drawing takeoff, use kind=photo or spec_pdf and pass optional remodelHints (cabinet LF, room dims, bath counts, flooring/drywall/paint/roof sf, risk flags) — quantities are reviewable, not auto-measured from pixels.",
|
|
123
160
|
inputSchemaZod: uploadTakeoffInputSchema,
|
|
124
161
|
execute: async (args: z.infer<typeof uploadTakeoffInputSchema>): Promise<string> => {
|
|
125
162
|
if (!client.hasApiKey()) return client.noApiKeyMessage();
|
|
@@ -128,6 +165,7 @@ export const uploadTakeoffInput = {
|
|
|
128
165
|
fileName: args.fileName,
|
|
129
166
|
base64: args.base64,
|
|
130
167
|
...(args.mimeType ? { mimeType: args.mimeType } : {}),
|
|
168
|
+
...(args.remodelHints ? { remodelHints: args.remodelHints } : {}),
|
|
131
169
|
});
|
|
132
170
|
return JSON.stringify(data);
|
|
133
171
|
},
|