@kynver-app/mcp-estimator 0.2.2 → 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kynver-app/mcp-estimator",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Kynver Estimator MCP server — chat, estimate, and catalog tools",
5
5
  "type": "module",
6
6
  "bin": {
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
  }
@@ -31,7 +31,7 @@ const reviewImportSchema = z.object({
31
31
  z.object({
32
32
  index: z.number(),
33
33
  action: z.enum(["keep", "skip", "edit"]),
34
- edits: z.record(z.unknown()).optional(),
34
+ edits: z.record(z.string(), z.unknown()).optional(),
35
35
  })
36
36
  )
37
37
  .optional(),
@@ -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,17 +156,18 @@ export const listTakeoffProjects = {
119
156
  export const uploadTakeoffInput = {
120
157
  name: "estimator_upload_takeoff_input",
121
158
  description:
122
- "Register a takeoff file upload. Pass base64 file content via the API route from your client, or use the web UI multipart upload at POST /api/estimator/takeoffs/{id}/inputs with kind + file.",
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();
126
- return JSON.stringify({
127
- message:
128
- "Use POST /api/estimator/takeoffs/{projectId}/inputs multipart (kind + file) from an authenticated client. MCP base64 upload route can be added when hosted file relay is available.",
129
- projectId: args.projectId,
163
+ const data = await client.post(`/estimator/takeoffs/${args.projectId}/inputs`, {
130
164
  kind: args.kind,
131
165
  fileName: args.fileName,
166
+ base64: args.base64,
167
+ ...(args.mimeType ? { mimeType: args.mimeType } : {}),
168
+ ...(args.remodelHints ? { remodelHints: args.remodelHints } : {}),
132
169
  });
170
+ return JSON.stringify(data);
133
171
  },
134
172
  };
135
173
 
@@ -18,7 +18,7 @@ export const analyzePhotos = {
18
18
  const data = await client.post("/estimator/analyze", {
19
19
  type: "photos",
20
20
  images: args.images,
21
- prompt: args.prompt,
21
+ ...(args.prompt ? { prompt: args.prompt } : {}),
22
22
  });
23
23
  return JSON.stringify(data);
24
24
  },
@@ -41,7 +41,7 @@ export const analyzeDocument = {
41
41
  const data = await client.post("/estimator/analyze", {
42
42
  type: "document",
43
43
  text: args.text,
44
- prompt: args.prompt,
44
+ ...(args.prompt ? { prompt: args.prompt } : {}),
45
45
  });
46
46
  return JSON.stringify(data);
47
47
  },