@kynver-app/mcp-estimator 0.2.22 → 0.2.23

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/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{StdioServerTransport as Ci}from"@modelcontextprotocol/sdk/server/stdio.js";import{existsSync as bt,readFileSync as vt}from"node:fs";import{dirname as ot,join as rt}from"node:path";import{fileURLToPath as st}from"node:url";function kt(e){let t=ot(st(e));for(let o=0;o<6;o+=1){if(bt(rt(t,"package.json")))return t;let s=ot(t);if(s===t)break;t=s}throw new Error(`package.json not found above ${ot(st(e))}`)}function wt(e,t){let o=rt(kt(e),"package.json"),u=JSON.parse(vt(o,"utf8"))[t];if(typeof u!="string"||!u.trim())throw new Error(`Missing package.json ${t} at ${o}`);return u}function it(e=import.meta.url){return wt(e,"version")}function xt(e){return e.some(t=>t==="--version"||t==="-v")}function Pt(e,t){console.log(t?`${t} ${e}`:e),process.exit(0)}function ct(e,t=import.meta.url,o){return xt(e)?(Pt(it(t),o),!0):!1}import{McpServer as _i}from"@modelcontextprotocol/sdk/server/mcp.js";import{z as p}from"zod";import{AsyncLocalStorage as It}from"node:async_hooks";var I=class extends Error{constructor(t){super(t),this.name="EstimatorApiError"}};function _t(e){if(!e||typeof e!="object")return"";let t=e.name;return typeof t=="string"?t:""}function lt(e){if(!(!e||typeof e!="object"))return e.cause}function E(e){let t=_t(e);if(t==="TimeoutError"||t==="AbortError")return!0;let o=lt(e);return o?E(o):!1}function C(e){let t=e instanceof Error?e.message:String(e);if(/MCP error -32000\b/i.test(t)||/connection closed/i.test(t)||/fetch failed|ECONNRESET|ECONNREFUSED|ENOTFOUND|socket hang up|network error|timed out fetching a new connection|P2024|connection pool/i.test(t))return!0;let o=lt(e);return o?C(o):!1}function R(e,t="save"){return E(e)?t==="read"?"The estimate service did not respond in time. Nothing was loaded.":"The estimate service did not respond in time. The save was not confirmed.":t==="read"?"The estimate service could not be reached. Nothing was loaded.":"The estimate service could not be reached. The save was not confirmed."}var pt=2e4,Ct=new It,At=process.env.KYNVER_API_URL??"https://kynver.com",nt=process.env.KYNVER_API_KEY??"";function jt(e){let t=e.startsWith("/")?e:`/${e}`;if(t.includes("..")||t.includes("\0"))throw new Error("Invalid API path: path traversal is not allowed")}function Rt(e){jt(e);let t=e.startsWith("/")?e:`/${e}`,o=t.startsWith("/api")?t:`/api${t}`;return`${At.replace(/\/$/,"")}${o}`}function Nt(){let e={"Content-Type":"application/json"};nt&&(e.Authorization=`Bearer ${nt}`);let t=Ct.getStore(),o=t?.userId??process.env.KYNVER_USER_ID?.trim(),s=process.env.KYNVER_SERVICE_SECRET?.trim();if(o){if(t?.sessionCredential)return t.authKind==="runnerToken"?(delete e.Authorization,e["X-Kynver-Runner-Token"]=t.sessionCredential):e.Authorization=`Bearer ${t.sessionCredential}`,e;let u=process.env.KYNVER_RUNNER_TOKEN?.trim();if(u){if(t?.authKind==="apiKey"||t?.authKind==="runnerToken")throw new Error("User-authenticated session did not propagate its credential; refusing the deployment-wide KYNVER_RUNNER_TOKEN (cross-tenant risk).");if(t?.authKind!=="internal")return e["X-Kynver-Runner-Token"]=u,e}if(!s)throw new Error("User context requires authentication: set KYNVER_RUNNER_TOKEN (preferred) or KYNVER_SERVICE_SECRET in the MCP server environment.");e["X-Kynver-User-Id"]=o,e["X-Kynver-Service-Secret"]=s}return e}async function Kt(e){let t=await e.text();if(!e.ok){let o=t;try{let s=JSON.parse(t);o=s.error??s.message??t}catch{}throw new Error(o||`HTTP ${e.status}`)}if(t)try{return JSON.parse(t)}catch{return t}}async function tt(e,t,o,s="save",u=pt){let P;try{P=await fetch(Rt(t),{method:e,headers:Nt(),...o!==void 0?{body:JSON.stringify(o)}:{},signal:AbortSignal.timeout(u)})}catch(h){throw h instanceof I?h:C(h)||E(h)?new I(R(h,s)):h}try{return await Kt(P)}catch(h){throw C(h)?new I(R(h,s)):h}}async function f(e){return tt("GET",e,void 0,"read")}async function d(e,t){return tt("POST",e,t,"save")}async function S(e,t,o=pt){return tt("PATCH",e,t,"save",o)}async function k(e){return tt("DELETE",e,void 0,"save")}function n(){return!!nt}function a(){return"No API key configured. Set KYNVER_API_KEY in your MCP server config."}var Et=p.object({message:p.string()}),Z={name:"estimator_import_catalog",description:"Start a catalog import from a file. This is a placeholder \u2014 MCP cannot do file uploads. Describe the import request in the message field and the system will guide the user through the upload flow.",inputSchemaZod:Et,execute:async e=>{if(!n())return a();let t=await d("/v1/agents/kynver-estimator/chat",{message:e.message});return JSON.stringify(t)}},Zt=p.object({importId:p.string(),approved:p.boolean().optional(),changes:p.array(p.object({index:p.number(),action:p.enum(["keep","skip","edit"]),edits:p.record(p.string(),p.unknown()).optional()})).optional()}),z={name:"estimator_review_import",description:"Review a pending catalog import batch before committing. Shows parsed rows with suggested service codes, categories, and pricing. You can approve all, skip individual rows, or edit values before committing. Requires an importId from a previous import.",inputSchemaZod:Zt,execute:async e=>{if(!n())return a();let{importId:t,...o}=e,s=await S(`/estimator/import/${t}`,o);return JSON.stringify(s)}},zt=p.object({importId:p.string()}),M={name:"estimator_commit_import",description:"Finalize a reviewed catalog import, writing all approved rows into the user's service catalog. Requires an importId that has been reviewed. This action cannot be undone \u2014 rows are added permanently.",inputSchemaZod:zt,execute:async e=>{if(!n())return a();let t=await d(`/estimator/import/${e.importId}/commit`,{});return JSON.stringify(t)}},Mt=p.object({name:p.string(),serviceCode:p.string(),category:p.string(),materialType:p.string().nullable().optional(),unit:p.string(),materialCost:p.number().min(0),laborCost:p.number().min(0),minCharge:p.number().min(0).optional(),notes:p.string().optional()}),O={name:"estimator_setup_price",description:"Add a new service to the user's price book (service catalog). Every service needs: a unique serviceCode (e.g., CABINET_INSTALL, PAINT_WALLS, ROOF_INSPECT), a display name, category (general construction: demolition, framing, plumbing, electrical, hvac, drywall, paint, flooring, cabinetry, countertops, tile, windows, doors, insulation, appliances, general_conditions; roofing: tear_off, decking, underlayment, field_install, ridge_hip, flashing, misc, and others), unit (sq_ft, linear_ft, each, sheet, roll, bundle, hour, lump), and material/labor costs. Items in the catalog are used when creating estimates.",inputSchemaZod:Mt,execute:async e=>{if(!n())return a();let t=await d("/estimator/catalog",e);return JSON.stringify(t)}},Ot=p.object({itemId:p.string(),name:p.string().optional(),serviceCode:p.string().optional(),category:p.string().optional(),materialType:p.string().nullable().optional(),unit:p.string().optional(),materialCost:p.number().min(0).optional(),laborCost:p.number().min(0).optional(),minCharge:p.number().min(0).optional(),notes:p.string().optional()}),q={name:"estimator_update_price",description:"Update an existing item in the service catalog. Use estimator_get_catalog first to find the itemId. You can change the name, service code, category, unit, material cost, labor cost, minimum charge, or notes.",inputSchemaZod:Ot,execute:async e=>{if(!n())return a();let{itemId:t,...o}=e,s=await S(`/estimator/catalog/${t}`,o);return JSON.stringify(s)}},qt=p.object({category:p.string().optional(),materialType:p.string().optional(),limit:p.number().optional(),cursor:p.string().optional()}),L={name:"estimator_get_catalog",description:"List all items in the user's service catalog (price book). Returns service codes, names, categories, units, and material/labor costs. IMPORTANT: Always call this before creating estimates to get valid service codes. Optional filters must match enum strings. Categories include general construction (demolition, framing, drywall, paint, flooring, cabinetry, tile, electrical, plumbing, general_conditions, \u2026) and roofing (tear_off, decking, underlayment, field_install, ridge_hip, flashing, misc, \u2026). Materials include general construction (drywall, interior_paint, hardwood, ceramic_tile, stock_cabinet, quartz, \u2026) and roofing (asphalt_architectural, asphalt_3tab, metal_standing_seam, tpo, \u2026). When unsure, omit both filters to fetch the full catalog.",inputSchemaZod:qt,execute:async e=>{if(!n())return a();let t=new URLSearchParams;e.category&&t.set("category",e.category),e.materialType&&t.set("materialType",e.materialType),e.limit!=null&&t.set("limit",String(e.limit)),e.cursor&&t.set("cursor",e.cursor);let o=t.toString(),s=o?`/estimator/catalog?${o}`:"/estimator/catalog",u=await f(s);return JSON.stringify(u)}},Lt=p.object({}),J={name:"estimator_get_config",description:"Get the user's estimator settings \u2014 company name, default markup/waste percentage, tax rate, currency, contact info, license number, and default notes/terms for estimates.",inputSchemaZod:Lt,execute:async e=>{if(!n())return a();let t=await f("/estimator/config");return JSON.stringify(t)}},Jt=p.object({defaultMarkup:p.number().optional(),taxRate:p.number().optional(),companyName:p.string().optional(),companyPhone:p.string().optional(),companyEmail:p.string().optional(),companyAddress:p.string().optional(),companyLogo:p.string().optional(),licenseNumber:p.string().optional(),defaultTerms:p.string().optional()}),$={name:"estimator_update_config",description:"Update the user's estimator settings. You can set: companyName, defaultMarkup (waste %), taxRate, companyPhone, companyEmail, companyAddress, companyLogo (URL), licenseNumber (contractor CCB/license number), and defaultTerms (terms & conditions appended to estimates).",inputSchemaZod:Jt,execute:async e=>{if(!n())return a();let t=await S("/estimator/config",e);return JSON.stringify(t)}},$t=p.object({templateId:p.string().optional()}),U={name:"estimator_import_template",description:"Import a pre-built pricing catalog template into the user's service catalog. Call without arguments to see available templates (returns id, name, description, item count). Call with a templateId to import all items. general-construction-2026 is the general-construction starter (demolition, framing, drywall, paint, flooring, cabinetry, and the other trades). dealers-supply-2025 is optional roofing material pricing. Existing rows are not deleted. A legacy Misc row with no material for the same service code is moved onto the template category and material; prices, names, and notes stay. Other existing rows are skipped.",inputSchemaZod:$t,execute:async e=>{if(!n())return a();if(!e.templateId){let o=await f("/estimator/catalog/templates");return JSON.stringify(o)}let t=await d("/estimator/catalog/templates",{templateId:e.templateId});return JSON.stringify(t)}};import{z as v}from"zod";var Dt=v.object({}),D={name:"estimator_list_price_catalogs",description:"List durable builtin price catalogs shipped with the estimator (region, effective date, markup policy). These are the MCP's stored price lists \u2014 not AgentOS memory. Use before seeding or resolving remodel defaults.",inputSchemaZod:Dt,execute:async e=>{if(!n())return a();let t=await f("/estimator/price-catalog");return JSON.stringify(t)}},Ft=v.object({catalogId:v.string()}),F={name:"estimator_get_price_catalog",description:"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.",inputSchemaZod:Ft,execute:async e=>{if(!n())return a();let t=await f(`/estimator/price-catalog/${encodeURIComponent(e.catalogId)}`);return JSON.stringify(t)}},Vt=v.object({serviceCode:v.string(),materialType:v.string().nullable().optional(),region:v.string().optional(),overrideMaterialCost:v.number().min(0).optional(),overrideLaborCost:v.number().min(0).optional()}),V={name:"estimator_resolve_price",description:"Resolve unit pricing for one service code with provenance: user price book \u2192 builtin catalog \u2192 optional intake override (overrideMaterialCost/overrideLaborCost). Returns source, region, effective date, and markupIncluded flag.",inputSchemaZod:Vt,execute:async e=>{if(!n())return a();let t=new URLSearchParams({serviceCode:e.serviceCode});e.materialType&&t.set("materialType",e.materialType),e.region&&t.set("region",e.region),e.overrideMaterialCost!=null&&t.set("overrideMaterialCost",String(e.overrideMaterialCost)),e.overrideLaborCost!=null&&t.set("overrideLaborCost",String(e.overrideLaborCost));let o=await f(`/estimator/price-catalog/resolve?${t.toString()}`);return JSON.stringify(o)}},Bt=v.object({catalogId:v.string(),overwrite:v.boolean().optional()}),B={name:"estimator_seed_price_catalog",description:"Copy a builtin price catalog into the user's price book. Missing codes are inserted. A legacy Misc row with no material for that code is reclassified; prices stay unless overwrite=true. Rows are never deleted. Use portland-remodel-will-2026-06 for the Portland general-construction working rates.",inputSchemaZod:Bt,execute:async e=>{if(!n())return a();let t=await d("/estimator/price-catalog/seed",e);return JSON.stringify(t)}};import{z as r}from"zod";var Ht=new Set(["draft","final_status","sent","accepted","declined"]),Yt=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,Wt=2e3,Xt=8e3,Qt="projectName, status, clientId, companyId, siteAddress, notes (client-facing), internalNotes (contractor-only internal note)";function mt(e){if(e!==void 0)return e===null?null:e}function dt(e,t,o){if(e!==void 0){if(e===null||e==="")return null;if(!Yt.test(e)){o.push(`${t} was not a valid id, so it was left unchanged.`);return}return e}}function eo(e){let t=typeof e.estimateId=="string"?e.estimateId.trim():"";if(!t)return{ok:!1,error:"estimateId is required to update an estimate."};let o=[],s={},u=[];if(e.projectName!==void 0&&e.projectName!==null){let j=e.projectName.trim();if(!j)o.push("projectName was empty, so it was left unchanged.");else{if(j.length>200)return{ok:!1,error:"projectName is too long to save."};s.projectName=j,u.push("projectName")}}if(e.status!==void 0&&e.status!==null&&e.status!==""){let j=e.status.trim().toLowerCase();Ht.has(j)?(s.status=j,u.push("status")):o.push(`status "${e.status}" is not a draft, sent, accepted, or declined value, so it was left unchanged.`)}let P=dt(e.clientId,"clientId",o);P!==void 0&&(s.clientId=P,u.push("clientId"));let h=dt(e.companyId,"companyId",o);if(h!==void 0&&(s.companyId=h,u.push("companyId")),e.siteAddress!==void 0){if(e.siteAddress!==null&&e.siteAddress.length>500)return{ok:!1,error:"siteAddress is too long to save."};s.siteAddress=e.siteAddress,u.push("siteAddress")}let K=mt(e.notes);if(K!==void 0){if(K!==null&&K.length>Wt)return{ok:!1,error:"The client-facing note is too long to save."};s.notes=K,u.push("notes")}let Tt=e.internalNotes!==void 0?e.internalNotes:e.internalNote,et=mt(Tt);if(et!==void 0){if(et!==null&&et.length>Xt)return{ok:!1,error:"The internal note is too long to save."};s.internalNotes=et,u.push("internalNotes")}return u.length===0?{ok:!1,error:`No estimate fields to update.${o.length?` ${o.join(" ")}`:""} Send estimateId and at least one of: ${Qt}.`}:{ok:!0,estimateId:t,body:s,changed:u,warnings:o}}function to(e,t,o){let s=e&&typeof e=="object"?e:{};return JSON.stringify({ok:!0,id:typeof s.id=="string"?s.id:null,projectName:s.projectName??null,status:s.status??null,notes:s.notes??null,internalNotes:s.internalNotes??null,changed:t,...o.length?{warnings:o}:{}})}function oo(e){return e instanceof Error&&e.name==="EstimatorApiError"?e.message:E(e)||C(e)?R(e):(e instanceof Error?e.message:String(e)).replace(/MCP error\s*-32000:?/gi,"").replace(/connection closed/gi,"the save did not finish").trim()||"The estimate update did not finish. The save was not confirmed."}async function ut(e,t){let o=eo(e);if(!o.ok)return JSON.stringify({ok:!1,error:o.error});try{let s=await t(`/estimator/estimates/${o.estimateId}`,o.body);return to(s,o.changed,o.warnings)}catch(s){return JSON.stringify({ok:!1,error:oo(s)})}}var io=r.object({projectName:r.string(),roofMaterial:r.string(),footprintSqFt:r.number().optional(),roofAreaSqFt:r.number().optional(),pitch:r.string().optional(),layers:r.number().optional(),ridgeLengthFt:r.number(),hipLengthFt:r.number().optional(),valleyLengthFt:r.number().optional(),eavesFt:r.number(),rakeFt:r.number().optional(),wastePercent:r.number().optional(),clientId:r.string().optional(),companyId:r.string().optional(),notes:r.string().optional()}),G={name:"estimator_create_replacement",description:"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 \u2014 call estimator_get_catalog first to verify.",inputSchemaZod:io,execute:async e=>{if(!n())return a();let{projectName:t,clientId:o,companyId:s,notes:u,...P}=e,h=await d("/estimator/estimates",{type:"replacement",projectName:t,roofMaterial:P.roofMaterial,inputs:P,clientId:o,companyId:s,notes:u});return JSON.stringify(h)}},no=r.object({serviceCode:r.string(),materialType:r.string().nullable().optional(),quantity:r.number(),notes:r.string().optional()}),ao=r.object({projectName:r.string(),roofMaterial:r.string().optional(),lineItems:r.array(no),clientId:r.string().optional(),companyId:r.string().optional(),notes:r.string().optional()}),H={name:"estimator_create_repair",description:"Create a repair/service estimate with specific line items. Each line item needs a serviceCode that exists in the user's catalog \u2014 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.",inputSchemaZod:ao,execute:async e=>{if(!n())return a();let t=await d("/estimator/estimates",{type:"repair_service",projectName:e.projectName,roofMaterial:e.roofMaterial,lineItems:e.lineItems,clientId:e.clientId,companyId:e.companyId,notes:e.notes});return JSON.stringify(t)}},so=r.object({id:r.string()}),Y={name:"estimator_get_estimate",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.",inputSchemaZod:so,execute:async e=>{if(!n())return a();let t=await f(`/estimator/estimates/${e.id}`);return JSON.stringify(t)}},ro=r.object({status:r.string().optional(),type:r.string().optional(),clientId:r.string().optional(),companyId:r.string().optional(),from:r.string().optional(),to:r.string().optional(),limit:r.number().optional(),cursor:r.string().optional()}),W={name:"estimator_list_estimates",description:"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 \u2014 use estimator_get_estimate for full details on a specific one.",inputSchemaZod:ro,execute:async e=>{if(!n())return a();let t=new URLSearchParams;e.status&&t.set("status",e.status),e.type&&t.set("type",e.type),e.clientId&&t.set("clientId",e.clientId),e.companyId&&t.set("companyId",e.companyId),e.from&&t.set("from",e.from),e.to&&t.set("to",e.to),e.limit!=null&&t.set("limit",String(e.limit)),e.cursor&&t.set("cursor",e.cursor);let o=t.toString(),s=o?`/estimator/estimates?${o}`:"/estimator/estimates",u=await f(s);return JSON.stringify(u)}},co=r.object({estimateId:r.string(),serviceCode:r.string(),materialType:r.string().nullable().optional(),quantity:r.number(),notes:r.string().optional()}),X={name:"estimator_add_line_item",description:"Add a line item to an existing estimate. The serviceCode must exist in the user's catalog \u2014 call estimator_get_catalog to verify. Pass: estimateId, serviceCode, quantity. Optionally add notes. The estimate totals are automatically recalculated.",inputSchemaZod:co,execute:async e=>{if(!n())return a();let{estimateId:t,...o}=e,s=await d(`/estimator/estimates/${t}/line-items`,o);return JSON.stringify(s)}},lo=r.object({estimateId:r.string(),lineItemId:r.string()}),Q={name:"estimator_remove_line_item",description:"Remove a line item from an estimate. Requires estimateId and lineItemId (get lineItemIds from estimator_get_estimate). The estimate totals are automatically recalculated.",inputSchemaZod:lo,execute:async e=>{if(!n())return a();let t=await k(`/estimator/estimates/${e.estimateId}/line-items/${e.lineItemId}`);return JSON.stringify(t)}},po=r.object({estimateId:r.string(),lineItemId:r.string(),quantity:r.number().optional(),materialCost:r.number().optional(),laborCost:r.number().optional(),notes:r.string().optional()}),ee={name:"estimator_update_line_item",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.",inputSchemaZod:po,execute:async e=>{if(!n())return a();let{estimateId:t,lineItemId:o,...s}=e,u=await S(`/estimator/estimates/${t}/line-items/${o}`,s);return JSON.stringify(u)}},mo=r.object({estimateId:r.string(),targetTotal:r.number(),strategy:r.enum(["uniform","labor_only","material_only"]).optional()}),te={name:"estimator_adjust_to_target",description:"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 \u2014 scales all items proportionally), 'labor_only' (only adjusts labor costs), 'material_only' (only adjusts material costs). Requires estimateId and targetTotal.",inputSchemaZod:mo,execute:async e=>{if(!n())return a();let{estimateId:t,...o}=e,s=await d(`/estimator/estimates/${t}/adjust`,o);return JSON.stringify(s)}},uo=r.object({estimateId:r.string(),projectName:r.string().optional(),status:r.string().optional(),clientId:r.string().nullable().optional(),companyId:r.string().nullable().optional(),notes:r.string().nullable().optional(),internalNotes:r.string().nullable().optional(),internalNote:r.string().nullable().optional(),siteAddress:r.string().nullable().optional()}),oe={name:"estimator_update_estimate",description:"Update an estimate's project name, status (draft, sent, accepted, or declined), site address, client or company link, client-facing notes, or the contractor-only internal note. Use internalNotes for an internal note the customer must not see. Use notes for text that can be shown to the customer. internalNote is another name for internalNotes. Does not change line items. Pass estimateId and only the fields to change. A result with ok:false means the save did not stick.",inputSchemaZod:uo,execute:async e=>n()?ut(e,(t,o)=>S(t,o)):a()},fo=r.object({estimateId:r.string()}),ie={name:"estimator_delete_estimate",description:"Soft-delete an estimate. The estimate data is preserved but hidden from list views. This cannot be easily undone.",inputSchemaZod:fo,execute:async e=>{if(!n())return a();let t=await k(`/estimator/estimates/${e.estimateId}`);return JSON.stringify(t)}},go=r.object({estimateId:r.string(),projectName:r.string().optional()}),ne={name:"estimator_duplicate_estimate",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.",inputSchemaZod:go,execute:async e=>{if(!n())return a();let{estimateId:t,...o}=e,s=await d(`/estimator/estimates/${t}/duplicate`,o);return JSON.stringify(s)}};import{z as w}from"zod";var yo=w.object({estimateId:w.string(),name:w.string()}),ae={name:"estimator_save_template",description:"Save an existing estimate as a reusable template. Templates capture the estimate's line items, quantities, and structure so you can quickly create similar estimates in the future. Requires the estimateId of the source estimate and a name for the template.",inputSchemaZod:yo,execute:async e=>{if(!n())return a();let t=await d("/estimator/templates",e);return JSON.stringify(t)}},So=w.object({type:w.string().optional()}),se={name:"estimator_list_templates",description:"List all saved estimate templates. Optionally filter by type: 'replacement' or 'repair_service'. Returns template names, types, and IDs. Use template IDs with estimator_create_from_template to create new estimates.",inputSchemaZod:So,execute:async e=>{if(!n())return a();let t=new URLSearchParams;e.type&&t.set("type",e.type);let o=t.toString(),s=o?`/estimator/templates?${o}`:"/estimator/templates",u=await f(s);return JSON.stringify(u)}},To=w.object({id:w.string()}),re={name:"estimator_delete_template",description:"Delete a saved estimate template permanently. This does not affect any estimates that were previously created from this template.",inputSchemaZod:To,execute:async e=>{if(!n())return a();let t=await k(`/estimator/templates/${e.id}`);return JSON.stringify(t)}},bo=w.object({templateId:w.string(),projectName:w.string(),clientId:w.string().optional()}),ce={name:"estimator_create_from_template",description:"Create a new estimate from a saved template. The new estimate gets all the template's line items and structure. Requires: templateId (from estimator_list_templates), projectName. Optionally link to a clientId. The new estimate is created as a draft.",inputSchemaZod:bo,execute:async e=>{if(!n())return a();let t=await d("/estimator/estimates",e);return JSON.stringify(t)}};import{z as le}from"zod";var ko=le.object({estimateId:le.string()}),pe={name:"estimator_list_versions",description:"List all saved version snapshots of an estimate. Versions are created automatically when significant changes are made. Shows version numbers and timestamps. Use this to track the history of changes to an estimate.",inputSchemaZod:ko,execute:async e=>{if(!n())return a();let t=await f(`/estimator/estimates/${e.estimateId}/versions`);return JSON.stringify(t)}},wo=le.object({estimateId:le.string(),versionId:le.string()}),me={name:"estimator_get_version",description:"Get a specific historical version of an estimate. Returns the full estimate snapshot as it existed at that point in time \u2014 all line items, totals, and metadata. Useful for comparing current vs previous versions or reverting changes.",inputSchemaZod:wo,execute:async e=>{if(!n())return a();let t=await f(`/estimator/estimates/${e.estimateId}/versions/${e.versionId}`);return JSON.stringify(t)}};import{z as _}from"zod";var Po=_.object({estimateId:_.string(),imageUrl:_.string(),caption:_.string().optional()}),de={name:"estimator_upload_photo",description:"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.",inputSchemaZod:Po,execute:async e=>{if(!n())return a();let{estimateId:t,...o}=e,s=await d(`/estimator/estimates/${t}/photos`,o);return JSON.stringify(s)}},_o=_.object({estimateId:_.string()}),ue={name:"estimator_list_photos",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.",inputSchemaZod:_o,execute:async e=>{if(!n())return a();let t=await f(`/estimator/estimates/${e.estimateId}/photos`);return JSON.stringify(t)}},Io=_.object({estimateId:_.string(),photoId:_.string()}),fe={name:"estimator_delete_photo",description:"Remove a photo from an estimate. Requires estimateId and photoId (get IDs from estimator_list_photos).",inputSchemaZod:Io,execute:async e=>{if(!n())return a();let t=await k(`/estimator/estimates/${e.estimateId}/photos/${e.photoId}`);return JSON.stringify(t)}};import{z as b}from"zod";var Ao=b.object({from:b.string().optional(),to:b.string().optional()}),ge={name:"estimator_get_stats",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.",inputSchemaZod:Ao,execute:async e=>{if(!n())return a();let t=new URLSearchParams;e.from&&t.set("from",e.from),e.to&&t.set("to",e.to);let o=t.toString(),s=o?`/estimator/stats?${o}`:"/estimator/stats",u=await f(s);return JSON.stringify(u)}},jo=b.object({estimateId:b.string()}),he={name:"estimator_export_pdf",description:"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 \u2014 use estimator_update_output_config to customize which columns, sections, and details appear. The PDF includes company branding from estimator config.",inputSchemaZod:jo,execute:async e=>{if(!n())return a();try{await f(`/estimator/estimates/${e.estimateId}/pdf`)}catch{}return JSON.stringify({message:"PDF generated successfully.",downloadUrl:`/api/estimator/estimates/${e.estimateId}/pdf`})}},Ro=b.object({estimateId:b.string(),showMaterialCost:b.boolean().optional(),showLaborCost:b.boolean().optional(),showUnitPrice:b.boolean().optional(),showQuantity:b.boolean().optional(),showNotes:b.boolean().optional(),headerText:b.string().optional(),footerText:b.string().optional(),template:b.string().optional()}),ye={name:"estimator_update_output_config",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 \u2014 the underlying estimate data is not changed.",inputSchemaZod:Ro,execute:async e=>{if(!n())return a();let{estimateId:t,...o}=e,s=await S(`/estimator/estimates/${t}/output-config`,o);return JSON.stringify(s)}};import{z as A}from"zod";var Ko=A.object({estimateId:A.string(),recipient:A.string().email().optional(),ccRecipients:A.array(A.string().email()).max(10).optional().default([]),subject:A.string().min(1).max(200).optional(),message:A.string().min(1).max(1e4).optional(),confirmExternalRecipient:A.boolean().optional().default(!1)}),Se={name:"estimator_email_pdf",description:"Email an estimate PDF to a primary recipient with optional CC recipients in one group email. Fetches and attaches the PDF server-side (no secrets exposed). Defaults to the account owner's email when recipient is omitted. Estimate client emails and linked client records are trusted without extra confirmation; any external To or CC address requires confirmExternalRecipient=true after explicit user confirmation. Respects the estimate approval gate before customer send. Returns send status and audit metadata.",inputSchemaZod:Ko,execute:async e=>{if(!n())return a();let{estimateId:t,...o}=e,s=await d(`/estimator/estimates/${t}/email-pdf`,o);return JSON.stringify(s)}};import{z as N}from"zod";var Zo=N.object({images:N.array(N.string()),prompt:N.string().optional()}),Te={name:"estimator_analyze_photos",description:"Analyze roof photos using Claude's vision AI. Provide image URLs (can be multiple). The AI identifies: roofing materials, visible damage (missing shingles, cracks, moss, ponding), approximate measurements, and repair recommendations. Use the optional prompt parameter to ask specific questions about the photos. Results can inform estimate creation.",inputSchemaZod:Zo,execute:async e=>{if(!n())return a();let t=await d("/estimator/analyze",{type:"photos",images:e.images,...e.prompt?{prompt:e.prompt}:{}});return JSON.stringify(t)}},zo=N.object({text:N.string(),prompt:N.string().optional()}),be={name:"estimator_analyze_document",description:"Analyze a text document (inspection report, insurance scope of work, contractor notes) and extract structured estimating data \u2014 identified services, quantities, materials, and damage descriptions. Pass the document text in the text field. Use the optional prompt parameter to focus the analysis. Results can be used to create estimates with accurate line items.",inputSchemaZod:zo,execute:async e=>{if(!n())return a();let t=await d("/estimator/analyze",{type:"document",text:e.text,...e.prompt?{prompt:e.prompt}:{}});return JSON.stringify(t)}};import{z as y}from"zod";var Oo=y.object({name:y.string(),phone:y.string().optional(),email:y.string().optional(),address:y.string().optional(),notes:y.string().optional()}),ve={name:"estimator_create_company",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.",inputSchemaZod:Oo,execute:async e=>{if(!n())return a();let t=await d("/estimator/companies",e);return JSON.stringify(t)}},qo=y.object({search:y.string().optional(),limit:y.number().optional(),cursor:y.string().optional()}),ke={name:"estimator_list_companies",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.",inputSchemaZod:qo,execute:async e=>{if(!n())return a();let t=new URLSearchParams;e.search&&t.set("search",e.search),e.limit!=null&&t.set("limit",String(e.limit)),e.cursor&&t.set("cursor",e.cursor);let o=t.toString(),s=o?`/estimator/companies?${o}`:"/estimator/companies",u=await f(s);return JSON.stringify(u)}},Lo=y.object({companyId:y.string()}),we={name:"estimator_get_company",description:"Get a specific company's full details including all its client contacts and a summary of linked estimates.",inputSchemaZod:Lo,execute:async e=>{if(!n())return a();let t=await f(`/estimator/companies/${e.companyId}`);return JSON.stringify(t)}},Jo=y.object({companyId:y.string(),name:y.string().optional(),phone:y.string().optional(),email:y.string().optional(),address:y.string().optional(),notes:y.string().optional()}),xe={name:"estimator_update_company",description:"Update a company's details. Pass the companyId and any fields to change: name, phone, email, address, or notes.",inputSchemaZod:Jo,execute:async e=>{if(!n())return a();let{companyId:t,...o}=e,s=await S(`/estimator/companies/${t}`,o);return JSON.stringify(s)}},$o=y.object({companyId:y.string()}),Pe={name:"estimator_delete_company",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 \u2014 they become unlinked.",inputSchemaZod:$o,execute:async e=>{if(!n())return a();let t=await k(`/estimator/companies/${e.companyId}`);return JSON.stringify(t)}};import{z as g}from"zod";var Do=g.object({name:g.string(),companyId:g.string().optional(),title:g.string().optional(),phone:g.string().optional(),email:g.string().optional(),address:g.string().optional(),notes:g.string().optional()}),_e={name:"estimator_create_client",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.",inputSchemaZod:Do,execute:async e=>{if(!n())return a();let t=await d("/estimator/clients",e);return JSON.stringify(t)}},Fo=g.object({search:g.string().optional(),companyId:g.string().optional(),limit:g.number().optional(),cursor:g.string().optional()}),Ie={name:"estimator_list_clients",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.",inputSchemaZod:Fo,execute:async e=>{if(!n())return a();let t=new URLSearchParams;e.search&&t.set("search",e.search),e.companyId&&t.set("companyId",e.companyId),e.limit!=null&&t.set("limit",String(e.limit)),e.cursor&&t.set("cursor",e.cursor);let o=t.toString(),s=o?`/estimator/clients?${o}`:"/estimator/clients",u=await f(s);return JSON.stringify(u)}},Vo=g.object({clientId:g.string()}),Ce={name:"estimator_get_client",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.",inputSchemaZod:Vo,execute:async e=>{if(!n())return a();let t=await f(`/estimator/clients/${e.clientId}`);return JSON.stringify(t)}},Bo=g.object({clientId:g.string(),name:g.string().optional(),companyId:g.string().optional(),title:g.string().optional(),phone:g.string().optional(),email:g.string().optional(),address:g.string().optional(),notes:g.string().optional()}),Ae={name:"estimator_update_client",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).",inputSchemaZod:Bo,execute:async e=>{if(!n())return a();let{clientId:t,...o}=e,s=await S(`/estimator/clients/${t}`,o);return JSON.stringify(s)}},Go=g.object({clientId:g.string()}),je={name:"estimator_delete_client",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.",inputSchemaZod:Go,execute:async e=>{if(!n())return a();let t=await k(`/estimator/clients/${e.clientId}`);return JSON.stringify(t)}};import{z as c}from"zod";var at=c.object({projectId:c.string().uuid()}),Yo=c.object({projectName:c.string(),buildingType:c.enum(["commercial","residential"]),siteAddress:c.string().optional(),clientId:c.string().optional(),companyId:c.string().optional()}),Wo=c.object({}),Xo=c.object({cabinetLinearFt:c.number().nonnegative().optional(),rooms:c.array(c.object({name:c.string(),lengthFt:c.number().positive().optional(),widthFt:c.number().positive().optional()})).optional(),baths:c.array(c.object({type:c.enum(["full","half","three_quarter","unknown"]).optional(),count:c.number().int().positive().optional(),label:c.string().optional()})).optional(),flooringSqFt:c.number().nonnegative().optional(),drywallSqFt:c.number().nonnegative().optional(),paintSqFt:c.number().nonnegative().optional(),roofSqFt:c.number().nonnegative().optional(),flags:c.object({wallMovingRisk:c.boolean().optional(),electricalSurpriseRisk:c.boolean().optional(),plumbingSurpriseRisk:c.boolean().optional()}).optional(),notes:c.string().optional()}).optional(),Qo=c.object({projectId:c.string().uuid(),kind:c.string(),fileName:c.string(),base64:c.string(),mimeType:c.string().optional(),remodelHints:Xo}),ei=c.object({projectId:c.string().uuid(),reviewState:c.string().optional()}),ti=c.object({projectId:c.string().uuid(),rowId:c.string().uuid(),action:c.enum(["approve","skip","edit"]),edits:c.object({description:c.string().optional(),quantity:c.number().optional(),unit:c.string().optional(),serviceCode:c.string().nullable().optional()}).optional()}),oi=c.object({projectId:c.string().uuid(),overrideNote:c.string().optional()}),ii=c.object({segment:c.enum(["commercial","residential"]).optional()}),ni=c.object({projectId:c.string().uuid(),systemPackSlug:c.string(),serviceProfileId:c.string().optional()}),ai=c.object({}),si=c.object({slug:c.string(),name:c.string(),segment:c.enum(["commercial_service","commercial_production","residential_service","residential_production"]),laborMultiplier:c.number().optional(),defaultWastePercent:c.number().optional()}),ri=c.object({estimateId:c.string().uuid(),note:c.string().optional()}),ci=c.object({estimateId:c.string().uuid(),approvalId:c.string().uuid(),action:c.enum(["approve","reject"]),note:c.string().optional()}),li=c.object({estimateId:c.string().uuid()}),Re={name:"estimator_create_takeoff_project",description:"Create a takeoff project shell (commercial or residential). Upload inputs, run extract, review rows, commit, apply system pack, then generate estimate.",inputSchemaZod:Yo,execute:async e=>{if(!n())return a();let t=await d("/estimator/takeoffs",e);return JSON.stringify(t)}},Ne={name:"estimator_get_takeoff_project",description:"Get takeoff project with inputs, rows, status, pack/profile, linked estimate.",inputSchemaZod:at,execute:async e=>{if(!n())return a();let t=await f(`/estimator/takeoffs/${e.projectId}`);return JSON.stringify(t)}},Ke={name:"estimator_list_takeoff_projects",description:"List takeoff projects for the workspace.",inputSchemaZod:Wo,execute:async e=>{if(!n())return a();let t=await f("/estimator/takeoffs");return JSON.stringify(t)}},Ee={name:"estimator_upload_takeoff_input",description:"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) \u2014 quantities are reviewable, not auto-measured from pixels.",inputSchemaZod:Qo,execute:async e=>{if(!n())return a();let t=await d(`/estimator/takeoffs/${e.projectId}/inputs`,{kind:e.kind,fileName:e.fileName,base64:e.base64,...e.mimeType?{mimeType:e.mimeType}:{},...e.remodelHints?{remodelHints:e.remodelHints}:{}});return JSON.stringify(t)}},Ze={name:"estimator_run_takeoff_extract",description:"Run takeoff extractors on all uploaded inputs (idempotent).",inputSchemaZod:at,execute:async e=>{if(!n())return a();let t=await d(`/estimator/takeoffs/${e.projectId}/extract`,{});return JSON.stringify(t)}},ze={name:"estimator_list_takeoff_rows",description:"List takeoff rows with evidence and review state.",inputSchemaZod:ei,execute:async e=>{if(!n())return a();let t=e.reviewState?`?reviewState=${e.reviewState}`:"",o=await f(`/estimator/takeoffs/${e.projectId}/rows${t}`);return JSON.stringify(o)}},Me={name:"estimator_review_takeoff_row",description:"Approve, skip, or edit a single takeoff row.",inputSchemaZod:ti,execute:async e=>{if(!n())return a();let{projectId:t,rowId:o,...s}=e,u=await S(`/estimator/takeoffs/${t}/rows/${o}`,s);return JSON.stringify(u)}},Oe={name:"estimator_commit_takeoff",description:"Mark takeoff ready after all rows reviewed and area measurement approved (or override note).",inputSchemaZod:oi,execute:async e=>{if(!n())return a();let t=await d(`/estimator/takeoffs/${e.projectId}/commit`,{overrideNote:e.overrideNote});return JSON.stringify(t)}},qe={name:"estimator_list_system_packs",description:"List built-in commercial and residential system packs.",inputSchemaZod:ii,execute:async e=>{if(!n())return a();let t=e.segment?`?segment=${e.segment}`:"",o=await f(`/estimator/system-packs${t}`);return JSON.stringify(o)}},Le={name:"estimator_apply_system_pack",description:"Set system pack slug and optional service profile on takeoff project.",inputSchemaZod:ni,execute:async e=>{if(!n())return a();let t=await S(`/estimator/takeoffs/${e.projectId}`,{systemPackSlug:e.systemPackSlug,serviceProfileId:e.serviceProfileId??null});return JSON.stringify(t)}},Je={name:"estimator_list_service_profiles",description:"List service department profiles (includes v6 defaults).",inputSchemaZod:ai,execute:async e=>{if(!n())return a();let t=await f("/estimator/service-profiles");return JSON.stringify(t)}},$e={name:"estimator_upsert_service_profile",description:"Create or update a service department profile.",inputSchemaZod:si,execute:async e=>{if(!n())return a();let t=await d("/estimator/service-profiles",e);return JSON.stringify(t)}},Ue={name:"estimator_generate_estimate_from_takeoff",description:"Generate draft estimate from committed takeoff + system pack. Preflight fails if catalog codes missing.",inputSchemaZod:at,execute:async e=>{if(!n())return a();let t=await d(`/estimator/takeoffs/${e.projectId}/generate-estimate`,{});return JSON.stringify(t)}},De={name:"estimator_request_estimate_approval",description:"Request owner approval before customer send.",inputSchemaZod:ri,execute:async e=>{if(!n())return a();let t=await d(`/estimator/estimates/${e.estimateId}/approval`,{note:e.note});return JSON.stringify(t)}},Fe={name:"estimator_approve_estimate",description:"Approve or reject estimate (workspace owner).",inputSchemaZod:ci,execute:async e=>{if(!n())return a();let t=await d(`/estimator/estimates/${e.estimateId}/approval`,{approvalId:e.approvalId,action:e.action,note:e.note});return JSON.stringify(t)}},Ve={name:"estimator_get_estimate_approval",description:"Get latest approval status for an estimate.",inputSchemaZod:li,execute:async e=>{if(!n())return a();let t=await f(`/estimator/estimates/${e.estimateId}/approval`);return JSON.stringify(t)}};import{z as l}from"zod";var mi=l.enum(["kitchen_remodel","bath_remodel","kitchen_and_bath_remodel","whole_home_remodel","other_remodel"]),di=l.object({projectType:mi,projectName:l.string().optional(),siteAddress:l.string().optional(),baseScopes:l.array(l.object({key:l.string().optional(),label:l.string(),quantity:l.number().optional(),unit:l.string().optional(),serviceCode:l.string().optional(),budgetLump:l.number().optional(),notes:l.string().optional(),confidence:l.enum(["high","medium","low","unknown"]).optional(),isAlternate:l.boolean().optional(),alternateGroup:l.string().optional()})).optional(),alternates:l.array(l.object({key:l.string(),label:l.string(),scopeKeys:l.array(l.string()).optional(),lumpAdd:l.number().optional(),notes:l.string().optional()})).optional(),attachments:l.array(l.object({id:l.string(),kind:l.enum(["photo","drawing","plan_set","spec_pdf","spreadsheet","other"]),fileName:l.string().optional(),caption:l.string().optional(),url:l.string().optional(),mimeType:l.string().optional()})).optional(),exclusions:l.array(l.string()).optional(),surprises:l.array(l.string()).optional(),rateReferences:l.array(l.object({key:l.string(),label:l.string(),value:l.number(),unit:l.string().optional(),source:l.enum(["operating_rule","catalog","user","takeoff"]),catalogId:l.string().optional(),confidence:l.enum(["high","medium","low","unknown"]).optional()})).optional(),assumptionsNeedingReview:l.array(l.object({id:l.string(),text:l.string(),severity:l.enum(["info","warn","blocker"]),relatedScopeKey:l.string().optional()})).optional(),remodelHints:l.record(l.string(),l.unknown()).optional(),includeStandardAlternates:l.boolean().optional(),notes:l.string().optional()}),Be={name:"estimator_normalize_remodel_intake",description:"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.",inputSchemaZod:di,execute:async e=>{if(!n())return a();let t=await d("/estimator/intake/normalize-remodel",e);return JSON.stringify(t,null,2)}};import{z as m}from"zod";var fi=m.object({key:m.string(),label:m.string().optional(),quantity:m.number().optional(),unit:m.string().optional(),serviceCode:m.string().optional(),budgetLump:m.number().optional(),notes:m.string().optional()}),gi=m.object({intake:m.object({projectName:m.string().optional(),scopes:m.array(fi),alternates:m.array(m.object({key:m.string(),label:m.string(),scopeKeys:m.array(m.string()).optional(),lumpAdd:m.number().optional(),notes:m.string().optional()})).optional(),includeStandardAlternates:m.boolean().optional(),expandScopeComponents:m.boolean().optional(),exclusions:m.array(m.string()).optional(),assumptions:m.array(m.string()).optional(),notes:m.string().optional()}),takeoffRows:m.array(m.object({id:m.string().optional(),description:m.string(),quantity:m.number(),unit:m.string(),serviceCode:m.string().nullable().optional(),materialType:m.string().nullable().optional()})).optional(),overrides:m.array(m.object({scopeKey:m.string().optional(),serviceCode:m.string().optional(),description:m.string().optional(),quantity:m.number().optional(),unitMaterial:m.number().optional(),unitLabor:m.number().optional(),lumpTotal:m.number().optional(),notes:m.string().optional()})).optional(),pricing:m.object({contingencyPercent:m.number().optional(),markupPercent:m.number().optional(),profitPercent:m.number().optional()}).optional()}),Ge={name:"estimator_build_remodel_estimate",description:"Build a reviewable residential remodel estimate from normalized intake scopes, optional takeoff quantities, catalog prices, and overrides. Lump room scopes (kitchen, baths) auto-expand into a per-trade breakdown of line items (demolition, cabinetry, countertops, plumbing, electrical, appliances, flooring, tile, finishes) \u2014 each with its own scope-of-work description carried in notes \u2014 so client-facing output is itemized, not a single vague lump (set intake.expandScopeComponents=false for the legacy single-line behavior). Returns line items with provenance, alternates (both baths, upstairs expansion, roof, wall moving), a default+custom assumptions list, default+custom exclusions, contingency/markup/profit breakdown, and reviewMarkdown. Pass project-specific items via intake.assumptions and intake.exclusions. Does not persist \u2014 use estimator_generate_estimate_from_takeoff or estimator_create_repair after review. Room scope keys: kitchen, bath_full, bath_half, bath_primary, bath_secondary. Trade/quantity scope keys: cabinets, drywall, paint, framing, roof, flooring, electrical_kitchen, electrical_bath. Whole-project phase scope keys: general_conditions, permits, demolition, protection, final_clean.",inputSchemaZod:gi,execute:async e=>{if(!n())return a();let t=await d("/estimator/estimates/build-remodel",e);return JSON.stringify(t)}};import{z as i}from"zod";var ft=i.object({intake:i.object({projectName:i.string().optional(),scopes:i.array(i.object({key:i.string(),label:i.string().optional(),quantity:i.number().optional(),unit:i.string().optional(),serviceCode:i.string().optional(),budgetLump:i.number().optional(),notes:i.string().optional()})),expandScopeComponents:i.boolean().optional(),includeStandardAlternates:i.boolean().optional(),exclusions:i.array(i.string()).optional(),assumptions:i.array(i.string()).optional(),notes:i.string().optional()}),pricing:i.object({contingencyPercent:i.number().optional(),markupPercent:i.number().optional(),profitPercent:i.number().optional()}).optional()}),yi=i.object({preset:i.enum(["matthew-portland-2026-06"]).optional(),client:i.object({projectTitle:i.string(),clientName:i.string().optional(),preparedBy:i.string().optional(),location:i.string().optional(),documentType:i.string().optional(),validityNote:i.string().optional()}),budgetPackage:ft.optional(),fullPackage:ft.optional(),alternates:i.array(i.object({key:i.string(),label:i.string(),description:i.string(),budgetPlaceholder:i.number().nullable()})).optional(),correctionNote:i.string().optional(),scopeBasis:i.array(i.string()).optional()}),He={name:"estimator_build_remodel_deliverable",description:"Build a residential remodel deliverable package using the Matthew v3 golden output standard: contractor planning-budget tone, cover/header, executive summary option cards, scope basis, pricing summary, numbered detailed line items, key allowances, assumptions, exclusions, separated Option C alternates (never in headline totals), recommended next steps, and footer/validity language. Returns clientMarkdown + clientHtml + handoff JSON. Use preset matthew-portland-2026-06 for the reviewed Matthew Portland fixture ($339,025 full / $167,200 bank-budget; golden fixture at remodel-estimates/saved/2026-06-03-matthew-remodel-reviewed-v3/). Guards against superseded totals and generic AI prose. Does not persist \u2014 use clientHtml or handoff for PDF. For single-scope preview only, use estimator_build_remodel_estimate. Prefer estimator_draft_remodel_deliverable when you have scope notes and want the tool to assemble budget/full intakes.",inputSchemaZod:yi,execute:async e=>{if(!n())return a();let t=await d("/estimator/estimates/build-remodel-deliverable",e);return JSON.stringify(t)}},gt=i.object({key:i.string().min(1),label:i.string().optional(),quantity:i.number().positive().optional(),unit:i.string().optional(),serviceCode:i.string().optional(),budgetLump:i.number().nonnegative().optional(),notes:i.string().optional()}),Si=i.object({contingencyPercent:i.number().optional(),markupPercent:i.number().optional(),profitPercent:i.number().optional()}).optional(),ht=i.object({label:i.string().optional(),whatsIncluded:i.string().optional(),projectName:i.string().optional(),scopes:i.array(gt),assumptions:i.array(i.string()).optional(),exclusions:i.array(i.string()).optional(),notes:i.string().optional(),expandScopeComponents:i.boolean().optional(),pricing:Si}),Ti=ht.extend({derivedFromFull:i.object({omitFromFullKeys:i.array(i.string()).optional(),replaceWithAllowanceLump:i.array(i.object({key:i.string().min(1),label:i.string().optional(),budgetLump:i.number().nonnegative(),notes:i.string().optional()})).optional(),additionalScopes:i.array(gt).optional()}).optional()}),bi=i.object({client:i.object({projectTitle:i.string().min(1),clientName:i.string().optional(),preparedBy:i.string().optional(),location:i.string().optional(),documentType:i.string().optional(),validityNote:i.string().optional()}),fullScope:ht,budgetScope:Ti.optional(),alternates:i.array(i.object({key:i.string(),label:i.string(),description:i.string(),budgetPlaceholder:i.number().nullable()})).optional(),correctionNote:i.string().optional(),scopeBasis:i.array(i.string()).optional()}),Ye={name:"estimator_draft_remodel_deliverable",description:"Draft a sendable residential remodel deliverable from scope notes in a single call, following the Matthew v3 client-output section contract when scopeBasis/next steps are provided. Provide one fullScope.scopes list (each scope is a key like 'kitchen', 'bath_primary', 'drywall', or a custom slug, plus optional budgetLump/notes \u2014 the built-in catalog supplies label/unit/default rate). Budget package can be (a) explicit via budgetScope.scopes, (b) derived from full via budgetScope.derivedFromFull.omitFromFullKeys and/or replaceWithAllowanceLump, or (c) omitted to mirror full as collapsed allowances. Returns RemodelDeliverablePackage with clientMarkdown, clientHtml, handoff metadata, and reviewMarkdown (agent-facing, includes MCP guidance). Does not persist. Use for fresh remodel intakes instead of estimator_build_remodel_deliverable when you have not yet authored both intakes by hand.",inputSchemaZod:bi,execute:async e=>{if(!n())return a();let t=await d("/estimator/estimates/draft-remodel-deliverable",e);return JSON.stringify(t)}};import{z as x}from"zod";var ki=x.object({estimateId:x.string(),recipientEmail:x.string().email().optional(),recipientName:x.string().max(200).optional(),senderNote:x.string().max(2e3).optional(),expiresInDays:x.number().int().min(1).max(365).optional()}),We={name:"estimator_create_signing_request",description:"Create a contract signing request for an estimate. Returns a tokenized signing URL that can be shared with the client. The client can review and electronically sign the estimate at that URL. The signing request expires after expiresInDays (default 30). Does not send emails during implementation.",inputSchemaZod:ki,execute:async e=>{if(!n())return a();let{estimateId:t,...o}=e,s=await d(`/estimator/estimates/${t}/signing-requests`,o);return JSON.stringify(s)}},wi=x.object({estimateId:x.string()}),Xe={name:"estimator_get_signing_requests",description:"List all signing requests for an estimate. Returns status (pending/signed/expired/cancelled), signing URL, recipient, and audit metadata for each request.",inputSchemaZod:wi,execute:async e=>{if(!n())return a();let t=await f(`/estimator/estimates/${e.estimateId}/signing-requests`);return JSON.stringify(t)}},xi=x.object({estimateId:x.string(),requestId:x.string()}),Qe={name:"estimator_cancel_signing_request",description:"Cancel a pending signing request. Cannot cancel a request that has already been signed. The signing URL will become invalid immediately.",inputSchemaZod:xi,execute:async e=>{if(!n())return a();let{estimateId:t,requestId:o}=e,s=await S(`/estimator/estimates/${t}/signing-requests`,{requestId:o});return JSON.stringify(s)}};var Ii=it(import.meta.url);function yt(e,t=!1){return{content:[{type:"text",text:e}],...t?{isError:!0}:{}}}function St(){let e=new _i({name:"kynver-mcp-estimator",version:Ii},{capabilities:{tools:{}}});function t(o,s,u){e.registerTool(o,s,async P=>{try{return yt(await u(P))}catch(h){let K=h instanceof I?h.message:C(h)?R(h):h instanceof Error?h.message:"The estimate service could not be reached. The save was not confirmed.";return yt(K,!0)}})}return t(Z.name,{description:Z.description,inputSchema:Z.inputSchemaZod},o=>Z.execute(o)),t(z.name,{description:z.description,inputSchema:z.inputSchemaZod},o=>z.execute(o)),t(M.name,{description:M.description,inputSchema:M.inputSchemaZod},o=>M.execute(o)),t(O.name,{description:O.description,inputSchema:O.inputSchemaZod},o=>O.execute(o)),t(q.name,{description:q.description,inputSchema:q.inputSchemaZod},o=>q.execute(o)),t(L.name,{description:L.description,inputSchema:L.inputSchemaZod},o=>L.execute(o)),t(J.name,{description:J.description,inputSchema:J.inputSchemaZod},o=>J.execute(o)),t($.name,{description:$.description,inputSchema:$.inputSchemaZod},o=>$.execute(o)),t(U.name,{description:U.description,inputSchema:U.inputSchemaZod},o=>U.execute(o)),t(D.name,{description:D.description,inputSchema:D.inputSchemaZod},o=>D.execute(o)),t(F.name,{description:F.description,inputSchema:F.inputSchemaZod},o=>F.execute(o)),t(V.name,{description:V.description,inputSchema:V.inputSchemaZod},o=>V.execute(o)),t(B.name,{description:B.description,inputSchema:B.inputSchemaZod},o=>B.execute(o)),t(G.name,{description:G.description,inputSchema:G.inputSchemaZod},o=>G.execute(o)),t(H.name,{description:H.description,inputSchema:H.inputSchemaZod},o=>H.execute(o)),t(Y.name,{description:Y.description,inputSchema:Y.inputSchemaZod},o=>Y.execute(o)),t(W.name,{description:W.description,inputSchema:W.inputSchemaZod},o=>W.execute(o)),t(X.name,{description:X.description,inputSchema:X.inputSchemaZod},o=>X.execute(o)),t(Q.name,{description:Q.description,inputSchema:Q.inputSchemaZod},o=>Q.execute(o)),t(ee.name,{description:ee.description,inputSchema:ee.inputSchemaZod},o=>ee.execute(o)),t(te.name,{description:te.description,inputSchema:te.inputSchemaZod},o=>te.execute(o)),t(oe.name,{description:oe.description,inputSchema:oe.inputSchemaZod},o=>oe.execute(o)),t(ie.name,{description:ie.description,inputSchema:ie.inputSchemaZod},o=>ie.execute(o)),t(ne.name,{description:ne.description,inputSchema:ne.inputSchemaZod},o=>ne.execute(o)),t(Ge.name,{description:Ge.description,inputSchema:Ge.inputSchemaZod},o=>Ge.execute(o)),t(He.name,{description:He.description,inputSchema:He.inputSchemaZod},o=>He.execute(o)),t(Ye.name,{description:Ye.description,inputSchema:Ye.inputSchemaZod},o=>Ye.execute(o)),t(ae.name,{description:ae.description,inputSchema:ae.inputSchemaZod},o=>ae.execute(o)),t(se.name,{description:se.description,inputSchema:se.inputSchemaZod},o=>se.execute(o)),t(re.name,{description:re.description,inputSchema:re.inputSchemaZod},o=>re.execute(o)),t(ce.name,{description:ce.description,inputSchema:ce.inputSchemaZod},o=>ce.execute(o)),t(pe.name,{description:pe.description,inputSchema:pe.inputSchemaZod},o=>pe.execute(o)),t(me.name,{description:me.description,inputSchema:me.inputSchemaZod},o=>me.execute(o)),t(de.name,{description:de.description,inputSchema:de.inputSchemaZod},o=>de.execute(o)),t(ue.name,{description:ue.description,inputSchema:ue.inputSchemaZod},o=>ue.execute(o)),t(fe.name,{description:fe.description,inputSchema:fe.inputSchemaZod},o=>fe.execute(o)),t(ge.name,{description:ge.description,inputSchema:ge.inputSchemaZod},o=>ge.execute(o)),t(he.name,{description:he.description,inputSchema:he.inputSchemaZod},o=>he.execute(o)),t(ye.name,{description:ye.description,inputSchema:ye.inputSchemaZod},o=>ye.execute(o)),t(Se.name,{description:Se.description,inputSchema:Se.inputSchemaZod},o=>Se.execute(o)),t(Te.name,{description:Te.description,inputSchema:Te.inputSchemaZod},o=>Te.execute(o)),t(be.name,{description:be.description,inputSchema:be.inputSchemaZod},o=>be.execute(o)),t(ve.name,{description:ve.description,inputSchema:ve.inputSchemaZod},o=>ve.execute(o)),t(ke.name,{description:ke.description,inputSchema:ke.inputSchemaZod},o=>ke.execute(o)),t(we.name,{description:we.description,inputSchema:we.inputSchemaZod},o=>we.execute(o)),t(xe.name,{description:xe.description,inputSchema:xe.inputSchemaZod},o=>xe.execute(o)),t(Pe.name,{description:Pe.description,inputSchema:Pe.inputSchemaZod},o=>Pe.execute(o)),t(_e.name,{description:_e.description,inputSchema:_e.inputSchemaZod},o=>_e.execute(o)),t(Ie.name,{description:Ie.description,inputSchema:Ie.inputSchemaZod},o=>Ie.execute(o)),t(Ce.name,{description:Ce.description,inputSchema:Ce.inputSchemaZod},o=>Ce.execute(o)),t(Ae.name,{description:Ae.description,inputSchema:Ae.inputSchemaZod},o=>Ae.execute(o)),t(je.name,{description:je.description,inputSchema:je.inputSchemaZod},o=>je.execute(o)),t(Re.name,{description:Re.description,inputSchema:Re.inputSchemaZod},o=>Re.execute(o)),t(Ne.name,{description:Ne.description,inputSchema:Ne.inputSchemaZod},o=>Ne.execute(o)),t(Ke.name,{description:Ke.description,inputSchema:Ke.inputSchemaZod},o=>Ke.execute(o)),t(Ee.name,{description:Ee.description,inputSchema:Ee.inputSchemaZod},o=>Ee.execute(o)),t(Ze.name,{description:Ze.description,inputSchema:Ze.inputSchemaZod},o=>Ze.execute(o)),t(ze.name,{description:ze.description,inputSchema:ze.inputSchemaZod},o=>ze.execute(o)),t(Me.name,{description:Me.description,inputSchema:Me.inputSchemaZod},o=>Me.execute(o)),t(Oe.name,{description:Oe.description,inputSchema:Oe.inputSchemaZod},o=>Oe.execute(o)),t(qe.name,{description:qe.description,inputSchema:qe.inputSchemaZod},o=>qe.execute(o)),t(Le.name,{description:Le.description,inputSchema:Le.inputSchemaZod},o=>Le.execute(o)),t(Je.name,{description:Je.description,inputSchema:Je.inputSchemaZod},o=>Je.execute(o)),t($e.name,{description:$e.description,inputSchema:$e.inputSchemaZod},o=>$e.execute(o)),t(Ue.name,{description:Ue.description,inputSchema:Ue.inputSchemaZod},o=>Ue.execute(o)),t(De.name,{description:De.description,inputSchema:De.inputSchemaZod},o=>De.execute(o)),t(Fe.name,{description:Fe.description,inputSchema:Fe.inputSchemaZod},o=>Fe.execute(o)),t(Ve.name,{description:Ve.description,inputSchema:Ve.inputSchemaZod},o=>Ve.execute(o)),t(Be.name,{description:Be.description,inputSchema:Be.inputSchemaZod},o=>Be.execute(o)),t(We.name,{description:We.description,inputSchema:We.inputSchemaZod},o=>We.execute(o)),t(Xe.name,{description:Xe.description,inputSchema:Xe.inputSchemaZod},o=>Xe.execute(o)),t(Qe.name,{description:Qe.description,inputSchema:Qe.inputSchemaZod},o=>Qe.execute(o)),e}ct(process.argv.slice(2),import.meta.url,"kynver-mcp-estimator");var Ai=St(),ji=new Ci;await Ai.connect(ji);
2
+ import{StdioServerTransport as Ci}from"@modelcontextprotocol/sdk/server/stdio.js";import{existsSync as bt,readFileSync as kt}from"node:fs";import{dirname as ot,join as rt}from"node:path";import{fileURLToPath as st}from"node:url";function vt(e){let t=ot(st(e));for(let o=0;o<6;o+=1){if(bt(rt(t,"package.json")))return t;let s=ot(t);if(s===t)break;t=s}throw new Error(`package.json not found above ${ot(st(e))}`)}function wt(e,t){let o=rt(vt(e),"package.json"),u=JSON.parse(kt(o,"utf8"))[t];if(typeof u!="string"||!u.trim())throw new Error(`Missing package.json ${t} at ${o}`);return u}function it(e=import.meta.url){return wt(e,"version")}function xt(e){return e.some(t=>t==="--version"||t==="-v")}function Pt(e,t){console.log(t?`${t} ${e}`:e),process.exit(0)}function ct(e,t=import.meta.url,o){return xt(e)?(Pt(it(t),o),!0):!1}import{McpServer as _i}from"@modelcontextprotocol/sdk/server/mcp.js";import{z as p}from"zod";import{AsyncLocalStorage as It}from"node:async_hooks";var C=class extends Error{constructor(t){super(t),this.name="EstimatorApiError"}};function _t(e){if(!e||typeof e!="object")return"";let t=e.name;return typeof t=="string"?t:""}function lt(e){if(!(!e||typeof e!="object"))return e.cause}function E(e){let t=_t(e);if(t==="TimeoutError"||t==="AbortError")return!0;let o=lt(e);return o?E(o):!1}function A(e){let t=e instanceof Error?e.message:String(e);if(/MCP error -32000\b/i.test(t)||/connection closed/i.test(t)||/fetch failed|ECONNRESET|ECONNREFUSED|ENOTFOUND|socket hang up|network error|timed out fetching a new connection|P2024|connection pool/i.test(t))return!0;let o=lt(e);return o?A(o):!1}function N(e,t="save"){return E(e)?t==="read"?"The estimate service did not respond in time. Nothing was loaded.":"The estimate service did not respond in time. The save was not confirmed.":t==="read"?"The estimate service could not be reached. Nothing was loaded.":"The estimate service could not be reached. The save was not confirmed."}var pt=2e4,Ct=new It,At=process.env.KYNVER_API_URL??"https://kynver.com",nt=process.env.KYNVER_API_KEY??"";function jt(e){let t=e.startsWith("/")?e:`/${e}`;if(t.includes("..")||t.includes("\0"))throw new Error("Invalid API path: path traversal is not allowed")}function Rt(e){jt(e);let t=e.startsWith("/")?e:`/${e}`,o=t.startsWith("/api")?t:`/api${t}`;return`${At.replace(/\/$/,"")}${o}`}function Nt(){let e={"Content-Type":"application/json"};nt&&(e.Authorization=`Bearer ${nt}`);let t=Ct.getStore(),o=t?.userId??process.env.KYNVER_USER_ID?.trim(),s=process.env.KYNVER_SERVICE_SECRET?.trim();if(o){if(t?.sessionCredential)return t.authKind==="runnerToken"?(delete e.Authorization,e["X-Kynver-Runner-Token"]=t.sessionCredential):e.Authorization=`Bearer ${t.sessionCredential}`,e;let u=process.env.KYNVER_RUNNER_TOKEN?.trim();if(u){if(t?.authKind==="apiKey"||t?.authKind==="runnerToken")throw new Error("User-authenticated session did not propagate its credential; refusing the deployment-wide KYNVER_RUNNER_TOKEN (cross-tenant risk).");if(t?.authKind!=="internal")return e["X-Kynver-Runner-Token"]=u,e}if(!s)throw new Error("User context requires authentication: set KYNVER_RUNNER_TOKEN (preferred) or KYNVER_SERVICE_SECRET in the MCP server environment.");e["X-Kynver-User-Id"]=o,e["X-Kynver-Service-Secret"]=s}return e}async function Kt(e){let t=await e.text();if(!e.ok){let o=t;try{let s=JSON.parse(t);o=s.error??s.message??t}catch{}throw new Error(o||`HTTP ${e.status}`)}if(t)try{return JSON.parse(t)}catch{return t}}async function tt(e,t,o,s="save",u=pt){let P;try{P=await fetch(Rt(t),{method:e,headers:Nt(),...o!==void 0?{body:JSON.stringify(o)}:{},signal:AbortSignal.timeout(u)})}catch(h){throw h instanceof C?h:A(h)||E(h)?new C(N(h,s)):h}try{return await Kt(P)}catch(h){throw A(h)?new C(N(h,s)):h}}async function f(e){return tt("GET",e,void 0,"read")}async function d(e,t){return tt("POST",e,t,"save")}async function T(e,t,o=pt){return tt("PATCH",e,t,"save",o)}async function v(e){return tt("DELETE",e,void 0,"save")}function n(){return!!nt}function a(){return"No API key configured. Set KYNVER_API_KEY in your MCP server config."}var Et=p.object({message:p.string()}),Z={name:"estimator_import_catalog",description:"Start a catalog import from a file. This is a placeholder \u2014 MCP cannot do file uploads. Describe the import request in the message field and the system will guide the user through the upload flow.",inputSchemaZod:Et,execute:async e=>{if(!n())return a();let t=await d("/v1/agents/kynver-estimator/chat",{message:e.message});return JSON.stringify(t)}},Zt=p.object({importId:p.string(),approved:p.boolean().optional(),changes:p.array(p.object({index:p.number(),action:p.enum(["keep","skip","edit"]),edits:p.record(p.string(),p.unknown()).optional()})).optional()}),z={name:"estimator_review_import",description:"Review a pending catalog import batch before committing. Shows parsed rows with suggested service codes, categories, and pricing. You can approve all, skip individual rows, or edit values before committing. Requires an importId from a previous import.",inputSchemaZod:Zt,execute:async e=>{if(!n())return a();let{importId:t,...o}=e,s=await T(`/estimator/import/${t}`,o);return JSON.stringify(s)}},zt=p.object({importId:p.string()}),M={name:"estimator_commit_import",description:"Finalize a reviewed catalog import, writing all approved rows into the user's service catalog. Requires an importId that has been reviewed. This action cannot be undone \u2014 rows are added permanently.",inputSchemaZod:zt,execute:async e=>{if(!n())return a();let t=await d(`/estimator/import/${e.importId}/commit`,{});return JSON.stringify(t)}},Mt=p.object({name:p.string(),serviceCode:p.string(),category:p.string(),materialType:p.string().nullable().optional(),unit:p.string(),materialCost:p.number().min(0),laborCost:p.number().min(0),minCharge:p.number().min(0).optional(),notes:p.string().optional()}),O={name:"estimator_setup_price",description:"Add a new service to the user's price book (service catalog). Every service needs: a unique serviceCode (e.g., CABINET_INSTALL, PAINT_WALLS, ROOF_INSPECT), a display name, category (general construction: demolition, framing, plumbing, electrical, hvac, drywall, paint, flooring, cabinetry, countertops, tile, windows, doors, insulation, appliances, general_conditions; roofing: tear_off, decking, underlayment, field_install, ridge_hip, flashing, misc, and others), unit (sq = one roofing square of 100 sq ft, sq_ft, linear_ft, each, sheet, roll, bundle, gallon, hour, lump; per-square roofing work is always sq), and material/labor costs. Items in the catalog are used when creating estimates.",inputSchemaZod:Mt,execute:async e=>{if(!n())return a();let t=await d("/estimator/catalog",e);return JSON.stringify(t)}},Ot=p.object({itemId:p.string(),name:p.string().optional(),serviceCode:p.string().optional(),category:p.string().optional(),materialType:p.string().nullable().optional(),unit:p.string().optional(),materialCost:p.number().min(0).optional(),laborCost:p.number().min(0).optional(),minCharge:p.number().min(0).optional(),notes:p.string().optional()}),q={name:"estimator_update_price",description:"Update an existing item in the service catalog. Use estimator_get_catalog first to find the itemId. You can change the name, service code, category, unit, material cost, labor cost, minimum charge, or notes.",inputSchemaZod:Ot,execute:async e=>{if(!n())return a();let{itemId:t,...o}=e,s=await T(`/estimator/catalog/${t}`,o);return JSON.stringify(s)}},qt=p.object({category:p.string().optional(),materialType:p.string().optional(),limit:p.number().optional(),cursor:p.string().optional()}),L={name:"estimator_get_catalog",description:"List all items in the user's service catalog (price book). Returns service codes, names, categories, units, and material/labor costs. IMPORTANT: Always call this before creating estimates to get valid service codes. Optional filters must match enum strings. Categories include general construction (demolition, framing, drywall, paint, flooring, cabinetry, tile, electrical, plumbing, general_conditions, \u2026) and roofing (tear_off, decking, underlayment, field_install, ridge_hip, flashing, misc, \u2026). Materials include general construction (drywall, interior_paint, hardwood, ceramic_tile, stock_cabinet, quartz, \u2026) and roofing (asphalt_architectural, asphalt_3tab, metal_standing_seam, tpo, \u2026). When unsure, omit both filters to fetch the full catalog.",inputSchemaZod:qt,execute:async e=>{if(!n())return a();let t=new URLSearchParams;e.category&&t.set("category",e.category),e.materialType&&t.set("materialType",e.materialType),e.limit!=null&&t.set("limit",String(e.limit)),e.cursor&&t.set("cursor",e.cursor);let o=t.toString(),s=o?`/estimator/catalog?${o}`:"/estimator/catalog",u=await f(s);return JSON.stringify(u)}},Lt=p.object({}),$={name:"estimator_get_config",description:"Get the user's estimator settings \u2014 company name, default markup/waste percentage, tax rate, currency, contact info, license number, and default notes/terms for estimates.",inputSchemaZod:Lt,execute:async e=>{if(!n())return a();let t=await f("/estimator/config");return JSON.stringify(t)}},$t=p.object({defaultMarkup:p.number().optional(),taxRate:p.number().optional(),companyName:p.string().optional(),companyPhone:p.string().optional(),companyEmail:p.string().optional(),companyAddress:p.string().optional(),companyLogo:p.string().optional(),licenseNumber:p.string().optional(),defaultTerms:p.string().optional()}),J={name:"estimator_update_config",description:"Update the user's estimator settings. You can set: companyName, defaultMarkup (waste %), taxRate, companyPhone, companyEmail, companyAddress, companyLogo (URL), licenseNumber (contractor CCB/license number), and defaultTerms (terms & conditions appended to estimates).",inputSchemaZod:$t,execute:async e=>{if(!n())return a();let t=await T("/estimator/config",e);return JSON.stringify(t)}},Jt=p.object({templateId:p.string().optional()}),U={name:"estimator_import_template",description:"Import a pre-built pricing catalog template into the user's service catalog. Call without arguments to see available templates (returns id, name, description, item count). Call with a templateId to import all items. general-construction-2026 is the general-construction starter (demolition, framing, drywall, paint, flooring, cabinetry, and the other trades). dealers-supply-2025 is optional roofing material pricing. Existing rows are not deleted. A legacy Misc row with no material for the same service code is moved onto the template category and material; prices, names, and notes stay. Other existing rows are skipped.",inputSchemaZod:Jt,execute:async e=>{if(!n())return a();if(!e.templateId){let o=await f("/estimator/catalog/templates");return JSON.stringify(o)}let t=await d("/estimator/catalog/templates",{templateId:e.templateId});return JSON.stringify(t)}};import{z as k}from"zod";var Ft=k.object({}),F={name:"estimator_list_price_catalogs",description:"List durable builtin price catalogs shipped with the estimator (region, effective date, markup policy). These are the MCP's stored price lists \u2014 not AgentOS memory. Use before seeding or resolving remodel defaults.",inputSchemaZod:Ft,execute:async e=>{if(!n())return a();let t=await f("/estimator/price-catalog");return JSON.stringify(t)}},Dt=k.object({catalogId:k.string()}),D={name:"estimator_get_price_catalog",description:"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.",inputSchemaZod:Dt,execute:async e=>{if(!n())return a();let t=await f(`/estimator/price-catalog/${encodeURIComponent(e.catalogId)}`);return JSON.stringify(t)}},Vt=k.object({serviceCode:k.string(),materialType:k.string().nullable().optional(),region:k.string().optional(),overrideMaterialCost:k.number().min(0).optional(),overrideLaborCost:k.number().min(0).optional()}),V={name:"estimator_resolve_price",description:"Resolve unit pricing for one service code with provenance: user price book \u2192 builtin catalog \u2192 optional intake override (overrideMaterialCost/overrideLaborCost). Returns source, region, effective date, and markupIncluded flag.",inputSchemaZod:Vt,execute:async e=>{if(!n())return a();let t=new URLSearchParams({serviceCode:e.serviceCode});e.materialType&&t.set("materialType",e.materialType),e.region&&t.set("region",e.region),e.overrideMaterialCost!=null&&t.set("overrideMaterialCost",String(e.overrideMaterialCost)),e.overrideLaborCost!=null&&t.set("overrideLaborCost",String(e.overrideLaborCost));let o=await f(`/estimator/price-catalog/resolve?${t.toString()}`);return JSON.stringify(o)}},Bt=k.object({catalogId:k.string(),overwrite:k.boolean().optional()}),B={name:"estimator_seed_price_catalog",description:"Copy a builtin price catalog into the user's price book. Missing codes are inserted. A legacy Misc row with no material for that code is reclassified; prices stay unless overwrite=true. Rows are never deleted. Use portland-remodel-will-2026-06 for the Portland general-construction working rates.",inputSchemaZod:Bt,execute:async e=>{if(!n())return a();let t=await d("/estimator/price-catalog/seed",e);return JSON.stringify(t)}};import{z as r}from"zod";var Ht=new Set(["draft","final_status","sent","accepted","declined"]),Yt=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,Wt=2e3,Xt=8e3,Qt="projectName, status, clientId, companyId, siteAddress, notes (client-facing), internalNotes (contractor-only internal note)";function mt(e){if(e!==void 0)return e===null?null:e}function dt(e,t,o){if(e!==void 0){if(e===null||e==="")return null;if(!Yt.test(e)){o.push(`${t} was not a valid id, so it was left unchanged.`);return}return e}}function eo(e){let t=typeof e.estimateId=="string"?e.estimateId.trim():"";if(!t)return{ok:!1,error:"estimateId is required to update an estimate."};let o=[],s={},u=[];if(e.projectName!==void 0&&e.projectName!==null){let R=e.projectName.trim();if(!R)o.push("projectName was empty, so it was left unchanged.");else{if(R.length>200)return{ok:!1,error:"projectName is too long to save."};s.projectName=R,u.push("projectName")}}if(e.status!==void 0&&e.status!==null&&e.status!==""){let R=e.status.trim().toLowerCase();Ht.has(R)?(s.status=R,u.push("status")):o.push(`status "${e.status}" is not a draft, sent, accepted, or declined value, so it was left unchanged.`)}let P=dt(e.clientId,"clientId",o);P!==void 0&&(s.clientId=P,u.push("clientId"));let h=dt(e.companyId,"companyId",o);if(h!==void 0&&(s.companyId=h,u.push("companyId")),e.siteAddress!==void 0){if(e.siteAddress!==null&&e.siteAddress.length>500)return{ok:!1,error:"siteAddress is too long to save."};s.siteAddress=e.siteAddress,u.push("siteAddress")}let I=mt(e.notes);if(I!==void 0){if(I!==null&&I.length>Wt)return{ok:!1,error:"The client-facing note is too long to save."};s.notes=I,u.push("notes")}let Tt=e.internalNotes!==void 0?e.internalNotes:e.internalNote,et=mt(Tt);if(et!==void 0){if(et!==null&&et.length>Xt)return{ok:!1,error:"The internal note is too long to save."};s.internalNotes=et,u.push("internalNotes")}return u.length===0?{ok:!1,error:`No estimate fields to update.${o.length?` ${o.join(" ")}`:""} Send estimateId and at least one of: ${Qt}.`}:{ok:!0,estimateId:t,body:s,changed:u,warnings:o}}function to(e,t,o){let s=e&&typeof e=="object"?e:{};return JSON.stringify({ok:!0,id:typeof s.id=="string"?s.id:null,projectName:s.projectName??null,status:s.status??null,notes:s.notes??null,internalNotes:s.internalNotes??null,changed:t,...o.length?{warnings:o}:{}})}function oo(e){return e instanceof Error&&e.name==="EstimatorApiError"?e.message:E(e)||A(e)?N(e):(e instanceof Error?e.message:String(e)).replace(/MCP error\s*-32000:?/gi,"").replace(/connection closed/gi,"the save did not finish").trim()||"The estimate update did not finish. The save was not confirmed."}async function ut(e,t){let o=eo(e);if(!o.ok)return JSON.stringify({ok:!1,error:o.error});try{let s=await t(`/estimator/estimates/${o.estimateId}`,o.body);return to(s,o.changed,o.warnings)}catch(s){return JSON.stringify({ok:!1,error:oo(s)})}}var io=r.object({projectName:r.string(),roofMaterial:r.string(),footprintSqFt:r.number().optional(),roofAreaSqFt:r.number().optional(),pitch:r.string().optional(),layers:r.number().optional(),ridgeLengthFt:r.number(),hipLengthFt:r.number().optional(),valleyLengthFt:r.number().optional(),eavesFt:r.number(),rakeFt:r.number().optional(),wastePercent:r.number().optional(),roofSquares:r.number().optional(),pipeBoots:r.number().optional(),chimneys:r.number().optional(),stepFlashingFt:r.number().optional(),deckSheets:r.number().optional(),clientId:r.string().optional(),companyId:r.string().optional(),siteAddress:r.string().optional(),notes:r.string().optional()}),G={name:"estimator_create_replacement",description:`Create a full roof replacement estimate from measurements. The server computes every line from the user's price book: tear-off per layer, haul-off, deck inspection, ice & water, underlayment and shingles with waste, drip edge, starter, ridge cap, ridge vent, valleys, a steep charge at 8/12 and up, plus step flashing, pipe boots, chimney reflash and decking sheets when given, and a dumpster and permit when the book prices them. Requires: projectName, roofMaterial (asphalt_3tab, asphalt_architectural, asphalt_designer, metal_standing_seam, tpo, and the other roof systems), ridgeLengthFt, eavesFt. Give the roof size as roofSquares (squares) or roofAreaSqFt, or footprintSqFt + pitch. Optional: pitch ("7/12"), layers (tear-off layers), rakeFt, hipLengthFt, valleyLengthFt, wastePercent (defaults to the account waste), pipeBoots, chimneys, stepFlashingFt, deckSheets, clientId, siteAddress. Returns the estimate and warnings for any line the price book does not price.`,inputSchemaZod:io,execute:async e=>{if(!n())return a();let{projectName:t,clientId:o,companyId:s,siteAddress:u,notes:P,...h}=e,I=await d("/estimator/estimates",{type:"replacement",projectName:t,roofMaterial:h.roofMaterial,inputs:h,clientId:o,companyId:s,siteAddress:u,notes:P});return JSON.stringify(I)}},no=r.object({serviceCode:r.string(),materialType:r.string().nullable().optional(),quantity:r.number(),notes:r.string().optional()}),ao=r.object({projectName:r.string(),roofMaterial:r.string().optional(),lineItems:r.array(no),clientId:r.string().optional(),companyId:r.string().optional(),notes:r.string().optional()}),H={name:"estimator_create_repair",description:"Create a repair/service estimate with specific line items. Each line item needs a serviceCode that exists in the user's catalog \u2014 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.",inputSchemaZod:ao,execute:async e=>{if(!n())return a();let t=await d("/estimator/estimates",{type:"repair_service",projectName:e.projectName,roofMaterial:e.roofMaterial,lineItems:e.lineItems,clientId:e.clientId,companyId:e.companyId,notes:e.notes});return JSON.stringify(t)}},so=r.object({id:r.string()}),Y={name:"estimator_get_estimate",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.",inputSchemaZod:so,execute:async e=>{if(!n())return a();let t=await f(`/estimator/estimates/${e.id}`);return JSON.stringify(t)}},ro=r.object({status:r.string().optional(),type:r.string().optional(),clientId:r.string().optional(),companyId:r.string().optional(),from:r.string().optional(),to:r.string().optional(),limit:r.number().optional(),cursor:r.string().optional()}),W={name:"estimator_list_estimates",description:"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 \u2014 use estimator_get_estimate for full details on a specific one.",inputSchemaZod:ro,execute:async e=>{if(!n())return a();let t=new URLSearchParams;e.status&&t.set("status",e.status),e.type&&t.set("type",e.type),e.clientId&&t.set("clientId",e.clientId),e.companyId&&t.set("companyId",e.companyId),e.from&&t.set("from",e.from),e.to&&t.set("to",e.to),e.limit!=null&&t.set("limit",String(e.limit)),e.cursor&&t.set("cursor",e.cursor);let o=t.toString(),s=o?`/estimator/estimates?${o}`:"/estimator/estimates",u=await f(s);return JSON.stringify(u)}},co=r.object({estimateId:r.string(),serviceCode:r.string(),materialType:r.string().nullable().optional(),quantity:r.number(),notes:r.string().optional()}),X={name:"estimator_add_line_item",description:"Add a line item to an existing estimate. The serviceCode must exist in the user's catalog \u2014 call estimator_get_catalog to verify. Pass: estimateId, serviceCode, quantity. Optionally add notes. The estimate totals are automatically recalculated.",inputSchemaZod:co,execute:async e=>{if(!n())return a();let{estimateId:t,...o}=e,s=await d(`/estimator/estimates/${t}/line-items`,o);return JSON.stringify(s)}},lo=r.object({estimateId:r.string(),lineItemId:r.string()}),Q={name:"estimator_remove_line_item",description:"Remove a line item from an estimate. Requires estimateId and lineItemId (get lineItemIds from estimator_get_estimate). The estimate totals are automatically recalculated.",inputSchemaZod:lo,execute:async e=>{if(!n())return a();let t=await v(`/estimator/estimates/${e.estimateId}/line-items/${e.lineItemId}`);return JSON.stringify(t)}},po=r.object({estimateId:r.string(),lineItemId:r.string(),quantity:r.number().optional(),materialCost:r.number().optional(),laborCost:r.number().optional(),notes:r.string().optional()}),ee={name:"estimator_update_line_item",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.",inputSchemaZod:po,execute:async e=>{if(!n())return a();let{estimateId:t,lineItemId:o,...s}=e,u=await T(`/estimator/estimates/${t}/line-items/${o}`,s);return JSON.stringify(u)}},mo=r.object({estimateId:r.string(),targetTotal:r.number(),strategy:r.enum(["uniform","labor_only","material_only"]).optional()}),te={name:"estimator_adjust_to_target",description:"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 \u2014 scales all items proportionally), 'labor_only' (only adjusts labor costs), 'material_only' (only adjusts material costs). Requires estimateId and targetTotal.",inputSchemaZod:mo,execute:async e=>{if(!n())return a();let{estimateId:t,...o}=e,s=await d(`/estimator/estimates/${t}/adjust`,o);return JSON.stringify(s)}},uo=r.object({estimateId:r.string(),projectName:r.string().optional(),status:r.string().optional(),clientId:r.string().nullable().optional(),companyId:r.string().nullable().optional(),notes:r.string().nullable().optional(),internalNotes:r.string().nullable().optional(),internalNote:r.string().nullable().optional(),siteAddress:r.string().nullable().optional()}),oe={name:"estimator_update_estimate",description:"Update an estimate's project name, status (draft, sent, accepted, or declined), site address, client or company link, client-facing notes, or the contractor-only internal note. Use internalNotes for an internal note the customer must not see. Use notes for text that can be shown to the customer. internalNote is another name for internalNotes. Does not change line items. Pass estimateId and only the fields to change. A result with ok:false means the save did not stick.",inputSchemaZod:uo,execute:async e=>n()?ut(e,(t,o)=>T(t,o)):a()},fo=r.object({estimateId:r.string()}),ie={name:"estimator_delete_estimate",description:"Soft-delete an estimate. The estimate data is preserved but hidden from list views. This cannot be easily undone.",inputSchemaZod:fo,execute:async e=>{if(!n())return a();let t=await v(`/estimator/estimates/${e.estimateId}`);return JSON.stringify(t)}},go=r.object({estimateId:r.string(),projectName:r.string().optional()}),ne={name:"estimator_duplicate_estimate",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.",inputSchemaZod:go,execute:async e=>{if(!n())return a();let{estimateId:t,...o}=e,s=await d(`/estimator/estimates/${t}/duplicate`,o);return JSON.stringify(s)}};import{z as w}from"zod";var yo=w.object({estimateId:w.string(),name:w.string()}),ae={name:"estimator_save_template",description:"Save an existing estimate as a reusable template. Templates capture the estimate's line items, quantities, and structure so you can quickly create similar estimates in the future. Requires the estimateId of the source estimate and a name for the template.",inputSchemaZod:yo,execute:async e=>{if(!n())return a();let t=await d("/estimator/templates",e);return JSON.stringify(t)}},So=w.object({type:w.string().optional()}),se={name:"estimator_list_templates",description:"List all saved estimate templates. Optionally filter by type: 'replacement' or 'repair_service'. Returns template names, types, and IDs. Use template IDs with estimator_create_from_template to create new estimates.",inputSchemaZod:So,execute:async e=>{if(!n())return a();let t=new URLSearchParams;e.type&&t.set("type",e.type);let o=t.toString(),s=o?`/estimator/templates?${o}`:"/estimator/templates",u=await f(s);return JSON.stringify(u)}},To=w.object({id:w.string()}),re={name:"estimator_delete_template",description:"Delete a saved estimate template permanently. This does not affect any estimates that were previously created from this template.",inputSchemaZod:To,execute:async e=>{if(!n())return a();let t=await v(`/estimator/templates/${e.id}`);return JSON.stringify(t)}},bo=w.object({templateId:w.string(),projectName:w.string(),clientId:w.string().optional()}),ce={name:"estimator_create_from_template",description:"Create a new estimate from a saved template. The new estimate gets all the template's line items and structure. Requires: templateId (from estimator_list_templates), projectName. Optionally link to a clientId. The new estimate is created as a draft.",inputSchemaZod:bo,execute:async e=>{if(!n())return a();let{templateId:t,...o}=e,s=await d(`/estimator/templates/${encodeURIComponent(t)}/estimates`,o);return JSON.stringify(s)}};import{z as le}from"zod";var vo=le.object({estimateId:le.string()}),pe={name:"estimator_list_versions",description:"List all saved version snapshots of an estimate. Versions are created automatically when significant changes are made. Shows version numbers and timestamps. Use this to track the history of changes to an estimate.",inputSchemaZod:vo,execute:async e=>{if(!n())return a();let t=await f(`/estimator/estimates/${e.estimateId}/versions`);return JSON.stringify(t)}},wo=le.object({estimateId:le.string(),versionId:le.string()}),me={name:"estimator_get_version",description:"Get a specific historical version of an estimate. Returns the full estimate snapshot as it existed at that point in time \u2014 all line items, totals, and metadata. Useful for comparing current vs previous versions or reverting changes.",inputSchemaZod:wo,execute:async e=>{if(!n())return a();let t=await f(`/estimator/estimates/${e.estimateId}/versions/${e.versionId}`);return JSON.stringify(t)}};import{z as _}from"zod";var Po=_.object({estimateId:_.string(),imageUrl:_.string(),caption:_.string().optional()}),de={name:"estimator_upload_photo",description:"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.",inputSchemaZod:Po,execute:async e=>{if(!n())return a();let{estimateId:t,...o}=e,s=await d(`/estimator/estimates/${t}/photos`,o);return JSON.stringify(s)}},_o=_.object({estimateId:_.string()}),ue={name:"estimator_list_photos",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.",inputSchemaZod:_o,execute:async e=>{if(!n())return a();let t=await f(`/estimator/estimates/${e.estimateId}/photos`);return JSON.stringify(t)}},Io=_.object({estimateId:_.string(),photoId:_.string()}),fe={name:"estimator_delete_photo",description:"Remove a photo from an estimate. Requires estimateId and photoId (get IDs from estimator_list_photos).",inputSchemaZod:Io,execute:async e=>{if(!n())return a();let t=await v(`/estimator/estimates/${e.estimateId}/photos/${e.photoId}`);return JSON.stringify(t)}};import{z as S}from"zod";var Ao=S.object({from:S.string().optional(),to:S.string().optional()}),ge={name:"estimator_get_stats",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.",inputSchemaZod:Ao,execute:async e=>{if(!n())return a();let t=new URLSearchParams;e.from&&t.set("from",e.from),e.to&&t.set("to",e.to);let o=t.toString(),s=o?`/estimator/stats?${o}`:"/estimator/stats",u=await f(s);return JSON.stringify(u)}},jo=S.object({estimateId:S.string(),optionEstimateIds:S.array(S.string()).max(4).optional().describe("Good / Better / Best proposal: 2-4 estimate ids to put side by side in one PDF (one estimate per option). estimateId is the first option when it is not in this list."),optionLabels:S.array(S.string()).max(4).optional().describe("Names for the options, in the same order (default Good, Better, Best).")}),he={name:"estimator_export_pdf",description:"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 \u2014 use estimator_update_output_config to customize which columns, sections, and details appear. The PDF includes company branding from estimator config. Pass optionEstimateIds for one Good / Better / Best proposal that compares several estimates.",inputSchemaZod:jo,execute:async e=>{if(!n())return a();let t=(e.optionEstimateIds??[]).filter(u=>u&&u!==e.estimateId),o=new URLSearchParams;t.length>0&&o.set("options",t.join(",")),t.length>0&&e.optionLabels?.length&&o.set("labels",e.optionLabels.join(","));let s=o.toString()?`?${o.toString()}`:"";try{await f(`/estimator/estimates/${e.estimateId}/pdf${s}`)}catch{}return JSON.stringify({message:t.length>0?"Options PDF generated successfully.":"PDF generated successfully.",downloadUrl:`/api/estimator/estimates/${e.estimateId}/pdf${s}`})}},Ro=S.object({estimateId:S.string(),showMaterialCost:S.boolean().optional(),showLaborCost:S.boolean().optional(),showUnitPrice:S.boolean().optional(),showQuantity:S.boolean().optional(),showNotes:S.boolean().optional(),headerText:S.string().optional(),footerText:S.string().optional(),template:S.string().optional()}),ye={name:"estimator_update_output_config",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 \u2014 the underlying estimate data is not changed.",inputSchemaZod:Ro,execute:async e=>{if(!n())return a();let{estimateId:t,...o}=e,s=await T(`/estimator/estimates/${t}/output-config`,o);return JSON.stringify(s)}};import{z as j}from"zod";var Ko=j.object({estimateId:j.string(),recipient:j.string().email().optional(),ccRecipients:j.array(j.string().email()).max(10).optional().default([]),subject:j.string().min(1).max(200).optional(),message:j.string().min(1).max(1e4).optional(),confirmExternalRecipient:j.boolean().optional().default(!1)}),Se={name:"estimator_email_pdf",description:"Email an estimate PDF to a primary recipient with optional CC recipients in one group email. Fetches and attaches the PDF server-side (no secrets exposed). Defaults to the account owner's email when recipient is omitted. Estimate client emails and linked client records are trusted without extra confirmation; any external To or CC address requires confirmExternalRecipient=true after explicit user confirmation. Respects the estimate approval gate before customer send. Returns send status and audit metadata.",inputSchemaZod:Ko,execute:async e=>{if(!n())return a();let{estimateId:t,...o}=e,s=await d(`/estimator/estimates/${t}/email-pdf`,o);return JSON.stringify(s)}};import{z as K}from"zod";var Zo=K.object({images:K.array(K.string()),prompt:K.string().optional()}),Te={name:"estimator_analyze_photos",description:"Analyze roof photos using Claude's vision AI. Provide image URLs (can be multiple). The AI identifies: roofing materials, visible damage (missing shingles, cracks, moss, ponding), approximate measurements, and repair recommendations. Use the optional prompt parameter to ask specific questions about the photos. Results can inform estimate creation.",inputSchemaZod:Zo,execute:async e=>{if(!n())return a();let t=await d("/estimator/analyze",{type:"photos",images:e.images,...e.prompt?{prompt:e.prompt}:{}});return JSON.stringify(t)}},zo=K.object({text:K.string(),prompt:K.string().optional()}),be={name:"estimator_analyze_document",description:"Analyze a text document (inspection report, insurance scope of work, contractor notes) and extract structured estimating data \u2014 identified services, quantities, materials, and damage descriptions. Pass the document text in the text field. Use the optional prompt parameter to focus the analysis. Results can be used to create estimates with accurate line items.",inputSchemaZod:zo,execute:async e=>{if(!n())return a();let t=await d("/estimator/analyze",{type:"document",text:e.text,...e.prompt?{prompt:e.prompt}:{}});return JSON.stringify(t)}};import{z as y}from"zod";var Oo=y.object({name:y.string(),phone:y.string().optional(),email:y.string().optional(),address:y.string().optional(),notes:y.string().optional()}),ke={name:"estimator_create_company",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.",inputSchemaZod:Oo,execute:async e=>{if(!n())return a();let t=await d("/estimator/companies",e);return JSON.stringify(t)}},qo=y.object({search:y.string().optional(),limit:y.number().optional(),cursor:y.string().optional()}),ve={name:"estimator_list_companies",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.",inputSchemaZod:qo,execute:async e=>{if(!n())return a();let t=new URLSearchParams;e.search&&t.set("search",e.search),e.limit!=null&&t.set("limit",String(e.limit)),e.cursor&&t.set("cursor",e.cursor);let o=t.toString(),s=o?`/estimator/companies?${o}`:"/estimator/companies",u=await f(s);return JSON.stringify(u)}},Lo=y.object({companyId:y.string()}),we={name:"estimator_get_company",description:"Get a specific company's full details including all its client contacts and a summary of linked estimates.",inputSchemaZod:Lo,execute:async e=>{if(!n())return a();let t=await f(`/estimator/companies/${e.companyId}`);return JSON.stringify(t)}},$o=y.object({companyId:y.string(),name:y.string().optional(),phone:y.string().optional(),email:y.string().optional(),address:y.string().optional(),notes:y.string().optional()}),xe={name:"estimator_update_company",description:"Update a company's details. Pass the companyId and any fields to change: name, phone, email, address, or notes.",inputSchemaZod:$o,execute:async e=>{if(!n())return a();let{companyId:t,...o}=e,s=await T(`/estimator/companies/${t}`,o);return JSON.stringify(s)}},Jo=y.object({companyId:y.string()}),Pe={name:"estimator_delete_company",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 \u2014 they become unlinked.",inputSchemaZod:Jo,execute:async e=>{if(!n())return a();let t=await v(`/estimator/companies/${e.companyId}`);return JSON.stringify(t)}};import{z as g}from"zod";var Fo=g.object({name:g.string(),companyId:g.string().optional(),title:g.string().optional(),phone:g.string().optional(),email:g.string().optional(),address:g.string().optional(),notes:g.string().optional()}),_e={name:"estimator_create_client",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.",inputSchemaZod:Fo,execute:async e=>{if(!n())return a();let t=await d("/estimator/clients",e);return JSON.stringify(t)}},Do=g.object({search:g.string().optional(),companyId:g.string().optional(),limit:g.number().optional(),cursor:g.string().optional()}),Ie={name:"estimator_list_clients",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.",inputSchemaZod:Do,execute:async e=>{if(!n())return a();let t=new URLSearchParams;e.search&&t.set("search",e.search),e.companyId&&t.set("companyId",e.companyId),e.limit!=null&&t.set("limit",String(e.limit)),e.cursor&&t.set("cursor",e.cursor);let o=t.toString(),s=o?`/estimator/clients?${o}`:"/estimator/clients",u=await f(s);return JSON.stringify(u)}},Vo=g.object({clientId:g.string()}),Ce={name:"estimator_get_client",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.",inputSchemaZod:Vo,execute:async e=>{if(!n())return a();let t=await f(`/estimator/clients/${e.clientId}`);return JSON.stringify(t)}},Bo=g.object({clientId:g.string(),name:g.string().optional(),companyId:g.string().optional(),title:g.string().optional(),phone:g.string().optional(),email:g.string().optional(),address:g.string().optional(),notes:g.string().optional()}),Ae={name:"estimator_update_client",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).",inputSchemaZod:Bo,execute:async e=>{if(!n())return a();let{clientId:t,...o}=e,s=await T(`/estimator/clients/${t}`,o);return JSON.stringify(s)}},Go=g.object({clientId:g.string()}),je={name:"estimator_delete_client",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.",inputSchemaZod:Go,execute:async e=>{if(!n())return a();let t=await v(`/estimator/clients/${e.clientId}`);return JSON.stringify(t)}};import{z as c}from"zod";var at=c.object({projectId:c.string().uuid()}),Yo=c.object({projectName:c.string(),buildingType:c.enum(["commercial","residential"]),siteAddress:c.string().optional(),clientId:c.string().optional(),companyId:c.string().optional()}),Wo=c.object({}),Xo=c.object({cabinetLinearFt:c.number().nonnegative().optional(),rooms:c.array(c.object({name:c.string(),lengthFt:c.number().positive().optional(),widthFt:c.number().positive().optional()})).optional(),baths:c.array(c.object({type:c.enum(["full","half","three_quarter","unknown"]).optional(),count:c.number().int().positive().optional(),label:c.string().optional()})).optional(),flooringSqFt:c.number().nonnegative().optional(),drywallSqFt:c.number().nonnegative().optional(),paintSqFt:c.number().nonnegative().optional(),roofSqFt:c.number().nonnegative().optional(),flags:c.object({wallMovingRisk:c.boolean().optional(),electricalSurpriseRisk:c.boolean().optional(),plumbingSurpriseRisk:c.boolean().optional()}).optional(),notes:c.string().optional()}).optional(),Qo=c.object({projectId:c.string().uuid(),kind:c.string(),fileName:c.string(),base64:c.string(),mimeType:c.string().optional(),remodelHints:Xo}),ei=c.object({projectId:c.string().uuid(),reviewState:c.string().optional()}),ti=c.object({projectId:c.string().uuid(),rowId:c.string().uuid(),action:c.enum(["approve","skip","edit"]),edits:c.object({description:c.string().optional(),quantity:c.number().optional(),unit:c.string().optional(),serviceCode:c.string().nullable().optional()}).optional()}),oi=c.object({projectId:c.string().uuid(),overrideNote:c.string().optional()}),ii=c.object({segment:c.enum(["commercial","residential"]).optional()}),ni=c.object({projectId:c.string().uuid(),systemPackSlug:c.string(),serviceProfileId:c.string().optional()}),ai=c.object({}),si=c.object({slug:c.string(),name:c.string(),segment:c.enum(["commercial_service","commercial_production","residential_service","residential_production"]),laborMultiplier:c.number().optional(),defaultWastePercent:c.number().optional()}),ri=c.object({estimateId:c.string().uuid(),note:c.string().optional()}),ci=c.object({estimateId:c.string().uuid(),approvalId:c.string().uuid(),action:c.enum(["approve","reject"]),note:c.string().optional()}),li=c.object({estimateId:c.string().uuid()}),Re={name:"estimator_create_takeoff_project",description:"Create a takeoff project shell (commercial or residential). Upload inputs, run extract, review rows, commit, apply system pack, then generate estimate.",inputSchemaZod:Yo,execute:async e=>{if(!n())return a();let t=await d("/estimator/takeoffs",e);return JSON.stringify(t)}},Ne={name:"estimator_get_takeoff_project",description:"Get takeoff project with inputs, rows, status, pack/profile, linked estimate.",inputSchemaZod:at,execute:async e=>{if(!n())return a();let t=await f(`/estimator/takeoffs/${e.projectId}`);return JSON.stringify(t)}},Ke={name:"estimator_list_takeoff_projects",description:"List takeoff projects for the workspace.",inputSchemaZod:Wo,execute:async e=>{if(!n())return a();let t=await f("/estimator/takeoffs");return JSON.stringify(t)}},Ee={name:"estimator_upload_takeoff_input",description:"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) \u2014 quantities are reviewable, not auto-measured from pixels.",inputSchemaZod:Qo,execute:async e=>{if(!n())return a();let t=await d(`/estimator/takeoffs/${e.projectId}/inputs`,{kind:e.kind,fileName:e.fileName,base64:e.base64,...e.mimeType?{mimeType:e.mimeType}:{},...e.remodelHints?{remodelHints:e.remodelHints}:{}});return JSON.stringify(t)}},Ze={name:"estimator_run_takeoff_extract",description:"Run takeoff extractors on all uploaded inputs (idempotent).",inputSchemaZod:at,execute:async e=>{if(!n())return a();let t=await d(`/estimator/takeoffs/${e.projectId}/extract`,{});return JSON.stringify(t)}},ze={name:"estimator_list_takeoff_rows",description:"List takeoff rows with evidence and review state.",inputSchemaZod:ei,execute:async e=>{if(!n())return a();let t=e.reviewState?`?reviewState=${e.reviewState}`:"",o=await f(`/estimator/takeoffs/${e.projectId}/rows${t}`);return JSON.stringify(o)}},Me={name:"estimator_review_takeoff_row",description:"Approve, skip, or edit a single takeoff row.",inputSchemaZod:ti,execute:async e=>{if(!n())return a();let{projectId:t,rowId:o,...s}=e,u=await T(`/estimator/takeoffs/${t}/rows/${o}`,s);return JSON.stringify(u)}},Oe={name:"estimator_commit_takeoff",description:"Mark takeoff ready after all rows reviewed and area measurement approved (or override note).",inputSchemaZod:oi,execute:async e=>{if(!n())return a();let t=await d(`/estimator/takeoffs/${e.projectId}/commit`,{overrideNote:e.overrideNote});return JSON.stringify(t)}},qe={name:"estimator_list_system_packs",description:"List built-in commercial and residential system packs.",inputSchemaZod:ii,execute:async e=>{if(!n())return a();let t=e.segment?`?segment=${e.segment}`:"",o=await f(`/estimator/system-packs${t}`);return JSON.stringify(o)}},Le={name:"estimator_apply_system_pack",description:"Set system pack slug and optional service profile on takeoff project.",inputSchemaZod:ni,execute:async e=>{if(!n())return a();let t=await T(`/estimator/takeoffs/${e.projectId}`,{systemPackSlug:e.systemPackSlug,serviceProfileId:e.serviceProfileId??null});return JSON.stringify(t)}},$e={name:"estimator_list_service_profiles",description:"List service department profiles (includes v6 defaults).",inputSchemaZod:ai,execute:async e=>{if(!n())return a();let t=await f("/estimator/service-profiles");return JSON.stringify(t)}},Je={name:"estimator_upsert_service_profile",description:"Create or update a service department profile.",inputSchemaZod:si,execute:async e=>{if(!n())return a();let t=await d("/estimator/service-profiles",e);return JSON.stringify(t)}},Ue={name:"estimator_generate_estimate_from_takeoff",description:"Generate draft estimate from committed takeoff + system pack. Preflight fails if catalog codes missing.",inputSchemaZod:at,execute:async e=>{if(!n())return a();let t=await d(`/estimator/takeoffs/${e.projectId}/generate-estimate`,{});return JSON.stringify(t)}},Fe={name:"estimator_request_estimate_approval",description:"Request owner approval before customer send.",inputSchemaZod:ri,execute:async e=>{if(!n())return a();let t=await d(`/estimator/estimates/${e.estimateId}/approval`,{note:e.note});return JSON.stringify(t)}},De={name:"estimator_approve_estimate",description:"Approve or reject estimate (workspace owner).",inputSchemaZod:ci,execute:async e=>{if(!n())return a();let t=await d(`/estimator/estimates/${e.estimateId}/approval`,{approvalId:e.approvalId,action:e.action,note:e.note});return JSON.stringify(t)}},Ve={name:"estimator_get_estimate_approval",description:"Get latest approval status for an estimate.",inputSchemaZod:li,execute:async e=>{if(!n())return a();let t=await f(`/estimator/estimates/${e.estimateId}/approval`);return JSON.stringify(t)}};import{z as l}from"zod";var mi=l.enum(["kitchen_remodel","bath_remodel","kitchen_and_bath_remodel","whole_home_remodel","other_remodel"]),di=l.object({projectType:mi,projectName:l.string().optional(),siteAddress:l.string().optional(),baseScopes:l.array(l.object({key:l.string().optional(),label:l.string(),quantity:l.number().optional(),unit:l.string().optional(),serviceCode:l.string().optional(),budgetLump:l.number().optional(),notes:l.string().optional(),confidence:l.enum(["high","medium","low","unknown"]).optional(),isAlternate:l.boolean().optional(),alternateGroup:l.string().optional()})).optional(),alternates:l.array(l.object({key:l.string(),label:l.string(),scopeKeys:l.array(l.string()).optional(),lumpAdd:l.number().optional(),notes:l.string().optional()})).optional(),attachments:l.array(l.object({id:l.string(),kind:l.enum(["photo","drawing","plan_set","spec_pdf","spreadsheet","other"]),fileName:l.string().optional(),caption:l.string().optional(),url:l.string().optional(),mimeType:l.string().optional()})).optional(),exclusions:l.array(l.string()).optional(),surprises:l.array(l.string()).optional(),rateReferences:l.array(l.object({key:l.string(),label:l.string(),value:l.number(),unit:l.string().optional(),source:l.enum(["operating_rule","catalog","user","takeoff"]),catalogId:l.string().optional(),confidence:l.enum(["high","medium","low","unknown"]).optional()})).optional(),assumptionsNeedingReview:l.array(l.object({id:l.string(),text:l.string(),severity:l.enum(["info","warn","blocker"]),relatedScopeKey:l.string().optional()})).optional(),remodelHints:l.record(l.string(),l.unknown()).optional(),includeStandardAlternates:l.boolean().optional(),notes:l.string().optional()}),Be={name:"estimator_normalize_remodel_intake",description:"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.",inputSchemaZod:di,execute:async e=>{if(!n())return a();let t=await d("/estimator/intake/normalize-remodel",e);return JSON.stringify(t,null,2)}};import{z as m}from"zod";var fi=m.object({key:m.string(),label:m.string().optional(),quantity:m.number().optional(),unit:m.string().optional(),serviceCode:m.string().optional(),budgetLump:m.number().optional(),notes:m.string().optional()}),gi=m.object({intake:m.object({projectName:m.string().optional(),scopes:m.array(fi),alternates:m.array(m.object({key:m.string(),label:m.string(),scopeKeys:m.array(m.string()).optional(),lumpAdd:m.number().optional(),notes:m.string().optional()})).optional(),includeStandardAlternates:m.boolean().optional(),expandScopeComponents:m.boolean().optional(),exclusions:m.array(m.string()).optional(),assumptions:m.array(m.string()).optional(),notes:m.string().optional()}),takeoffRows:m.array(m.object({id:m.string().optional(),description:m.string(),quantity:m.number(),unit:m.string(),serviceCode:m.string().nullable().optional(),materialType:m.string().nullable().optional()})).optional(),overrides:m.array(m.object({scopeKey:m.string().optional(),serviceCode:m.string().optional(),description:m.string().optional(),quantity:m.number().optional(),unitMaterial:m.number().optional(),unitLabor:m.number().optional(),lumpTotal:m.number().optional(),notes:m.string().optional()})).optional(),pricing:m.object({contingencyPercent:m.number().optional(),markupPercent:m.number().optional(),profitPercent:m.number().optional()}).optional()}),Ge={name:"estimator_build_remodel_estimate",description:"Build a reviewable residential remodel estimate from normalized intake scopes, optional takeoff quantities, catalog prices, and overrides. Lump room scopes (kitchen, baths) auto-expand into a per-trade breakdown of line items (demolition, cabinetry, countertops, plumbing, electrical, appliances, flooring, tile, finishes) \u2014 each with its own scope-of-work description carried in notes \u2014 so client-facing output is itemized, not a single vague lump (set intake.expandScopeComponents=false for the legacy single-line behavior). Returns line items with provenance, alternates (both baths, upstairs expansion, roof, wall moving), a default+custom assumptions list, default+custom exclusions, contingency/markup/profit breakdown, and reviewMarkdown. Pass project-specific items via intake.assumptions and intake.exclusions. Does not persist \u2014 use estimator_generate_estimate_from_takeoff or estimator_create_repair after review. Room scope keys: kitchen, bath_full, bath_half, bath_primary, bath_secondary. Trade/quantity scope keys: cabinets, drywall, paint, framing, roof, flooring, electrical_kitchen, electrical_bath. Whole-project phase scope keys: general_conditions, permits, demolition, protection, final_clean.",inputSchemaZod:gi,execute:async e=>{if(!n())return a();let t=await d("/estimator/estimates/build-remodel",e);return JSON.stringify(t)}};import{z as i}from"zod";var ft=i.object({intake:i.object({projectName:i.string().optional(),scopes:i.array(i.object({key:i.string(),label:i.string().optional(),quantity:i.number().optional(),unit:i.string().optional(),serviceCode:i.string().optional(),budgetLump:i.number().optional(),notes:i.string().optional()})),expandScopeComponents:i.boolean().optional(),includeStandardAlternates:i.boolean().optional(),exclusions:i.array(i.string()).optional(),assumptions:i.array(i.string()).optional(),notes:i.string().optional()}),pricing:i.object({contingencyPercent:i.number().optional(),markupPercent:i.number().optional(),profitPercent:i.number().optional()}).optional()}),yi=i.object({preset:i.enum(["matthew-portland-2026-06"]).optional(),client:i.object({projectTitle:i.string(),clientName:i.string().optional(),preparedBy:i.string().optional(),location:i.string().optional(),documentType:i.string().optional(),validityNote:i.string().optional()}),budgetPackage:ft.optional(),fullPackage:ft.optional(),alternates:i.array(i.object({key:i.string(),label:i.string(),description:i.string(),budgetPlaceholder:i.number().nullable()})).optional(),correctionNote:i.string().optional(),scopeBasis:i.array(i.string()).optional()}),He={name:"estimator_build_remodel_deliverable",description:"Build a residential remodel deliverable package using the Matthew v3 golden output standard: contractor planning-budget tone, cover/header, executive summary option cards, scope basis, pricing summary, numbered detailed line items, key allowances, assumptions, exclusions, separated Option C alternates (never in headline totals), recommended next steps, and footer/validity language. Returns clientMarkdown + clientHtml + handoff JSON. Use preset matthew-portland-2026-06 for the reviewed Matthew Portland fixture ($339,025 full / $167,200 bank-budget; golden fixture at remodel-estimates/saved/2026-06-03-matthew-remodel-reviewed-v3/). Guards against superseded totals and generic AI prose. Does not persist \u2014 use clientHtml or handoff for PDF. For single-scope preview only, use estimator_build_remodel_estimate. Prefer estimator_draft_remodel_deliverable when you have scope notes and want the tool to assemble budget/full intakes.",inputSchemaZod:yi,execute:async e=>{if(!n())return a();let t=await d("/estimator/estimates/build-remodel-deliverable",e);return JSON.stringify(t)}},gt=i.object({key:i.string().min(1),label:i.string().optional(),quantity:i.number().positive().optional(),unit:i.string().optional(),serviceCode:i.string().optional(),budgetLump:i.number().nonnegative().optional(),notes:i.string().optional()}),Si=i.object({contingencyPercent:i.number().optional(),markupPercent:i.number().optional(),profitPercent:i.number().optional()}).optional(),ht=i.object({label:i.string().optional(),whatsIncluded:i.string().optional(),projectName:i.string().optional(),scopes:i.array(gt),assumptions:i.array(i.string()).optional(),exclusions:i.array(i.string()).optional(),notes:i.string().optional(),expandScopeComponents:i.boolean().optional(),pricing:Si}),Ti=ht.extend({derivedFromFull:i.object({omitFromFullKeys:i.array(i.string()).optional(),replaceWithAllowanceLump:i.array(i.object({key:i.string().min(1),label:i.string().optional(),budgetLump:i.number().nonnegative(),notes:i.string().optional()})).optional(),additionalScopes:i.array(gt).optional()}).optional()}),bi=i.object({client:i.object({projectTitle:i.string().min(1),clientName:i.string().optional(),preparedBy:i.string().optional(),location:i.string().optional(),documentType:i.string().optional(),validityNote:i.string().optional()}),fullScope:ht,budgetScope:Ti.optional(),alternates:i.array(i.object({key:i.string(),label:i.string(),description:i.string(),budgetPlaceholder:i.number().nullable()})).optional(),correctionNote:i.string().optional(),scopeBasis:i.array(i.string()).optional()}),Ye={name:"estimator_draft_remodel_deliverable",description:"Draft a sendable residential remodel deliverable from scope notes in a single call, following the Matthew v3 client-output section contract when scopeBasis/next steps are provided. Provide one fullScope.scopes list (each scope is a key like 'kitchen', 'bath_primary', 'drywall', or a custom slug, plus optional budgetLump/notes \u2014 the built-in catalog supplies label/unit/default rate). Budget package can be (a) explicit via budgetScope.scopes, (b) derived from full via budgetScope.derivedFromFull.omitFromFullKeys and/or replaceWithAllowanceLump, or (c) omitted to mirror full as collapsed allowances. Returns RemodelDeliverablePackage with clientMarkdown, clientHtml, handoff metadata, and reviewMarkdown (agent-facing, includes MCP guidance). Does not persist. Use for fresh remodel intakes instead of estimator_build_remodel_deliverable when you have not yet authored both intakes by hand.",inputSchemaZod:bi,execute:async e=>{if(!n())return a();let t=await d("/estimator/estimates/draft-remodel-deliverable",e);return JSON.stringify(t)}};import{z as x}from"zod";var vi=x.object({estimateId:x.string(),recipientEmail:x.string().email().optional(),recipientName:x.string().max(200).optional(),senderNote:x.string().max(2e3).optional(),expiresInDays:x.number().int().min(1).max(365).optional()}),We={name:"estimator_create_signing_request",description:"Create a contract signing request for an estimate. Returns a tokenized signing URL that can be shared with the client. The client can review and electronically sign the estimate at that URL. The signing request expires after expiresInDays (default 30). Does not send emails during implementation.",inputSchemaZod:vi,execute:async e=>{if(!n())return a();let{estimateId:t,...o}=e,s=await d(`/estimator/estimates/${t}/signing-requests`,o);return JSON.stringify(s)}},wi=x.object({estimateId:x.string()}),Xe={name:"estimator_get_signing_requests",description:"List all signing requests for an estimate. Returns status (pending/signed/expired/cancelled), signing URL, recipient, and audit metadata for each request.",inputSchemaZod:wi,execute:async e=>{if(!n())return a();let t=await f(`/estimator/estimates/${e.estimateId}/signing-requests`);return JSON.stringify(t)}},xi=x.object({estimateId:x.string(),requestId:x.string()}),Qe={name:"estimator_cancel_signing_request",description:"Cancel a pending signing request. Cannot cancel a request that has already been signed. The signing URL will become invalid immediately.",inputSchemaZod:xi,execute:async e=>{if(!n())return a();let{estimateId:t,requestId:o}=e,s=await T(`/estimator/estimates/${t}/signing-requests`,{requestId:o});return JSON.stringify(s)}};var Ii=it(import.meta.url);function yt(e,t=!1){return{content:[{type:"text",text:e}],...t?{isError:!0}:{}}}function St(){let e=new _i({name:"kynver-mcp-estimator",version:Ii},{capabilities:{tools:{}}});function t(o,s,u){e.registerTool(o,s,async P=>{try{return yt(await u(P))}catch(h){let I=h instanceof C?h.message:A(h)?N(h):h instanceof Error?h.message:"The estimate service could not be reached. The save was not confirmed.";return yt(I,!0)}})}return t(Z.name,{description:Z.description,inputSchema:Z.inputSchemaZod},o=>Z.execute(o)),t(z.name,{description:z.description,inputSchema:z.inputSchemaZod},o=>z.execute(o)),t(M.name,{description:M.description,inputSchema:M.inputSchemaZod},o=>M.execute(o)),t(O.name,{description:O.description,inputSchema:O.inputSchemaZod},o=>O.execute(o)),t(q.name,{description:q.description,inputSchema:q.inputSchemaZod},o=>q.execute(o)),t(L.name,{description:L.description,inputSchema:L.inputSchemaZod},o=>L.execute(o)),t($.name,{description:$.description,inputSchema:$.inputSchemaZod},o=>$.execute(o)),t(J.name,{description:J.description,inputSchema:J.inputSchemaZod},o=>J.execute(o)),t(U.name,{description:U.description,inputSchema:U.inputSchemaZod},o=>U.execute(o)),t(F.name,{description:F.description,inputSchema:F.inputSchemaZod},o=>F.execute(o)),t(D.name,{description:D.description,inputSchema:D.inputSchemaZod},o=>D.execute(o)),t(V.name,{description:V.description,inputSchema:V.inputSchemaZod},o=>V.execute(o)),t(B.name,{description:B.description,inputSchema:B.inputSchemaZod},o=>B.execute(o)),t(G.name,{description:G.description,inputSchema:G.inputSchemaZod},o=>G.execute(o)),t(H.name,{description:H.description,inputSchema:H.inputSchemaZod},o=>H.execute(o)),t(Y.name,{description:Y.description,inputSchema:Y.inputSchemaZod},o=>Y.execute(o)),t(W.name,{description:W.description,inputSchema:W.inputSchemaZod},o=>W.execute(o)),t(X.name,{description:X.description,inputSchema:X.inputSchemaZod},o=>X.execute(o)),t(Q.name,{description:Q.description,inputSchema:Q.inputSchemaZod},o=>Q.execute(o)),t(ee.name,{description:ee.description,inputSchema:ee.inputSchemaZod},o=>ee.execute(o)),t(te.name,{description:te.description,inputSchema:te.inputSchemaZod},o=>te.execute(o)),t(oe.name,{description:oe.description,inputSchema:oe.inputSchemaZod},o=>oe.execute(o)),t(ie.name,{description:ie.description,inputSchema:ie.inputSchemaZod},o=>ie.execute(o)),t(ne.name,{description:ne.description,inputSchema:ne.inputSchemaZod},o=>ne.execute(o)),t(Ge.name,{description:Ge.description,inputSchema:Ge.inputSchemaZod},o=>Ge.execute(o)),t(He.name,{description:He.description,inputSchema:He.inputSchemaZod},o=>He.execute(o)),t(Ye.name,{description:Ye.description,inputSchema:Ye.inputSchemaZod},o=>Ye.execute(o)),t(ae.name,{description:ae.description,inputSchema:ae.inputSchemaZod},o=>ae.execute(o)),t(se.name,{description:se.description,inputSchema:se.inputSchemaZod},o=>se.execute(o)),t(re.name,{description:re.description,inputSchema:re.inputSchemaZod},o=>re.execute(o)),t(ce.name,{description:ce.description,inputSchema:ce.inputSchemaZod},o=>ce.execute(o)),t(pe.name,{description:pe.description,inputSchema:pe.inputSchemaZod},o=>pe.execute(o)),t(me.name,{description:me.description,inputSchema:me.inputSchemaZod},o=>me.execute(o)),t(de.name,{description:de.description,inputSchema:de.inputSchemaZod},o=>de.execute(o)),t(ue.name,{description:ue.description,inputSchema:ue.inputSchemaZod},o=>ue.execute(o)),t(fe.name,{description:fe.description,inputSchema:fe.inputSchemaZod},o=>fe.execute(o)),t(ge.name,{description:ge.description,inputSchema:ge.inputSchemaZod},o=>ge.execute(o)),t(he.name,{description:he.description,inputSchema:he.inputSchemaZod},o=>he.execute(o)),t(ye.name,{description:ye.description,inputSchema:ye.inputSchemaZod},o=>ye.execute(o)),t(Se.name,{description:Se.description,inputSchema:Se.inputSchemaZod},o=>Se.execute(o)),t(Te.name,{description:Te.description,inputSchema:Te.inputSchemaZod},o=>Te.execute(o)),t(be.name,{description:be.description,inputSchema:be.inputSchemaZod},o=>be.execute(o)),t(ke.name,{description:ke.description,inputSchema:ke.inputSchemaZod},o=>ke.execute(o)),t(ve.name,{description:ve.description,inputSchema:ve.inputSchemaZod},o=>ve.execute(o)),t(we.name,{description:we.description,inputSchema:we.inputSchemaZod},o=>we.execute(o)),t(xe.name,{description:xe.description,inputSchema:xe.inputSchemaZod},o=>xe.execute(o)),t(Pe.name,{description:Pe.description,inputSchema:Pe.inputSchemaZod},o=>Pe.execute(o)),t(_e.name,{description:_e.description,inputSchema:_e.inputSchemaZod},o=>_e.execute(o)),t(Ie.name,{description:Ie.description,inputSchema:Ie.inputSchemaZod},o=>Ie.execute(o)),t(Ce.name,{description:Ce.description,inputSchema:Ce.inputSchemaZod},o=>Ce.execute(o)),t(Ae.name,{description:Ae.description,inputSchema:Ae.inputSchemaZod},o=>Ae.execute(o)),t(je.name,{description:je.description,inputSchema:je.inputSchemaZod},o=>je.execute(o)),t(Re.name,{description:Re.description,inputSchema:Re.inputSchemaZod},o=>Re.execute(o)),t(Ne.name,{description:Ne.description,inputSchema:Ne.inputSchemaZod},o=>Ne.execute(o)),t(Ke.name,{description:Ke.description,inputSchema:Ke.inputSchemaZod},o=>Ke.execute(o)),t(Ee.name,{description:Ee.description,inputSchema:Ee.inputSchemaZod},o=>Ee.execute(o)),t(Ze.name,{description:Ze.description,inputSchema:Ze.inputSchemaZod},o=>Ze.execute(o)),t(ze.name,{description:ze.description,inputSchema:ze.inputSchemaZod},o=>ze.execute(o)),t(Me.name,{description:Me.description,inputSchema:Me.inputSchemaZod},o=>Me.execute(o)),t(Oe.name,{description:Oe.description,inputSchema:Oe.inputSchemaZod},o=>Oe.execute(o)),t(qe.name,{description:qe.description,inputSchema:qe.inputSchemaZod},o=>qe.execute(o)),t(Le.name,{description:Le.description,inputSchema:Le.inputSchemaZod},o=>Le.execute(o)),t($e.name,{description:$e.description,inputSchema:$e.inputSchemaZod},o=>$e.execute(o)),t(Je.name,{description:Je.description,inputSchema:Je.inputSchemaZod},o=>Je.execute(o)),t(Ue.name,{description:Ue.description,inputSchema:Ue.inputSchemaZod},o=>Ue.execute(o)),t(Fe.name,{description:Fe.description,inputSchema:Fe.inputSchemaZod},o=>Fe.execute(o)),t(De.name,{description:De.description,inputSchema:De.inputSchemaZod},o=>De.execute(o)),t(Ve.name,{description:Ve.description,inputSchema:Ve.inputSchemaZod},o=>Ve.execute(o)),t(Be.name,{description:Be.description,inputSchema:Be.inputSchemaZod},o=>Be.execute(o)),t(We.name,{description:We.description,inputSchema:We.inputSchemaZod},o=>We.execute(o)),t(Xe.name,{description:Xe.description,inputSchema:Xe.inputSchemaZod},o=>Xe.execute(o)),t(Qe.name,{description:Qe.description,inputSchema:Qe.inputSchemaZod},o=>Qe.execute(o)),e}ct(process.argv.slice(2),import.meta.url,"kynver-mcp-estimator");var Ai=St(),ji=new Ci;await Ai.connect(ji);
package/dist/sse.js CHANGED
@@ -1 +1 @@
1
- import{createServer as Gt}from"node:http";import{randomUUID as Xt,timingSafeEqual as Wt}from"node:crypto";import{SSEServerTransport as Qt}from"@modelcontextprotocol/sdk/server/sse.js";import{StreamableHTTPServerTransport as eo}from"@modelcontextprotocol/sdk/server/streamableHttp.js";var to=1*1024*1024,oo=["apiKey","secret","userId"],io=new Set(["1","true","yes","on"]);function no(){let e=process.env.KYNVER_ALLOW_SERVICE_SECRET_IMPERSONATION?.trim().toLowerCase();return e!==void 0&&io.has(e)?!1:process.env.NODE_ENV==="production"||process.env.VERCEL_ENV==="production"}function B(e,t){let o=e.headers[t];return typeof o=="string"?o:Array.isArray(o)?o[0]??null:null}function Nt(e,t){let o=Buffer.from(e),i=Buffer.from(t);return o.length!==i.length?!1:Wt(o,i)}function Et(e){return oo.some(t=>e.searchParams.has(t))}function ao(){return process.env.MCP_SSE_HOST?.trim()||"127.0.0.1"}function yt(e,t){let o=process.env[e];if(!o)return t;let i=parseInt(o,10);return Number.isFinite(i)&&i>0?i:t}function so(){let e=process.env.KYNVER_API_URL?.trim();if(!e)return"https://kynver.com";let t;try{t=new URL(e)}catch{throw new Error(`Invalid KYNVER_API_URL: ${e}`)}let o=t.hostname==="localhost"||t.hostname==="127.0.0.1";if(t.protocol!=="https:"&&!(t.protocol==="http:"&&o))throw new Error(`KYNVER_API_URL must be https (got "${t.protocol}//${t.hostname}"); API keys are sent to this origin.`);return e.replace(/\/$/,"")}function Rt(e){let t=B(e,"x-forwarded-for");return t?t.split(",")[0].trim():e.socket?.remoteAddress??"unknown"}function ro(e,t){let o=new Map;return{check(i){let m=Date.now(),S=o.get(i);return!S||m>=S.resetAt?(o.set(i,{count:1,resetAt:m+t}),!0):S.count>=e?!1:(S.count+=1,!0)}}}function bt(e,t=to){return new Promise((o,i)=>{let m=[],S=0;e.on("data",h=>{if(S+=h.length,S>t){e.destroy(),i(new Error("Request body too large"));return}m.push(h)}),e.on("end",()=>{try{o(JSON.parse(Buffer.concat(m).toString()))}catch(h){i(h)}}),e.on("error",i)})}function vt(e){let t=B(e,"authorization");return t?/^Bearer\s+(.+)$/i.exec(t)?.[1]?.trim()??"":""}function wt(e,t){let o=B(e,"x-kynver-user-id"),i=B(e,"x-kynver-service-secret");return!o||!t||!i||no()||!Nt(i,t)?null:{userId:o,auth:{kind:"internal",userId:o}}}function jt(e,t,o,i){if(o.kind==="apiKey"){let S=vt(e);return S.length>0&&Nt(S,o.apiKey)}let m=wt(e,i);return m!==null&&m.userId===o.userId&&m.auth.kind==="internal"}async function Kt(e,t){try{let o=await fetch(`${e.replace(/\/$/,"")}/api/users/me`,{headers:{Authorization:`Bearer ${t}`}});return o.ok?(await o.json()).id??null:null}catch{return null}}function co(e,t){return new URL(e.url??"/",`http://localhost:${t}`)}function b(e,t,o){e.writeHead(t,{"Content-Type":"application/json"}),e.end(JSON.stringify(o))}function Mt(e){let t=parseInt(process.env.PORT??String(e.defaultPort),10),o=so(),i=process.env.KYNVER_SERVICE_SECRET??"",m=ao(),S={},h={},N=yt("MCP_SSE_MAX_SESSIONS",200),ut=yt("MCP_SSE_MAX_SESSIONS_PER_USER",20),q=yt("MCP_SSE_IDLE_TIMEOUT_MS",300*1e3),M=yt("MCP_SSE_RATE_LIMIT_PER_MIN",120),ft=ro(M,60*1e3);setInterval(()=>{let y=Date.now();for(let[f,C]of Object.entries(S))y-C.lastActivity>q&&(delete S[f],C.transport.close().catch(()=>{}));for(let[f,C]of Object.entries(h))y-C.lastActivity>q&&(delete h[f],C.transport.close().catch(()=>{}))},60*1e3).unref?.();let _t=()=>Object.keys(S).length+Object.keys(h).length,At=y=>Object.values(S).filter(f=>f.userId===y).length+Object.values(h).filter(f=>f.userId===y).length,Ct=Gt(async(y,f)=>{let C=co(y,t),gt=C.pathname;if(y.method==="GET"&&gt==="/sse"){if(!ft.check(`sse:${Rt(y)}`)){b(f,429,{error:"Too many requests. Slow down and retry."});return}if(Et(C)){b(f,400,{error:"Internal auth via query params is not allowed. Use X-Kynver-User-Id and X-Kynver-Service-Secret headers."});return}let _=null,v=null,A=i?wt(y,i):null;if(A)_=A.userId,v=A.auth;else{let $=vt(y);$&&(_=await Kt(o,$),_&&(v={kind:"apiKey",apiKey:$}))}if(!_||!v){b(f,401,{error:"Unauthorized. Pass API key via Authorization: Bearer <key> or internal X-Kynver-User-Id + X-Kynver-Service-Secret headers."});return}if(_t()>=N){b(f,503,{error:"Server at capacity. Try again shortly."});return}if(At(_)>=ut){b(f,429,{error:"Too many concurrent connections for this account."});return}console.log(`[MCP SSE] New connection (${e.label}, user: ${_.slice(0,8)}...)`);let Z=new Qt("/messages",f);S[Z.sessionId]={transport:Z,userId:_,auth:v,lastActivity:Date.now()},f.on("close",()=>{delete S[Z.sessionId]}),await(await e.createMcpServer()).connect(Z);return}if(y.method==="POST"&&gt==="/messages"){let _=C.searchParams.get("sessionId"),v=_?S[_]:void 0;if(!v){b(f,400,{error:"No active SSE session for this sessionId"});return}if(!jt(y,C,v.auth,i)){b(f,401,{error:"Unauthorized. Re-send the same credentials used to open the SSE session."});return}if(!ft.check(`msg:${v.userId}`)){b(f,429,{error:"Too many requests. Slow down and retry."});return}v.lastActivity=Date.now();try{let A=await bt(y);await e.requestContext.run({userId:v.userId,authKind:v.auth.kind,sessionCredential:v.auth.kind==="apiKey"?v.auth.apiKey:void 0},async()=>{await v.transport.handlePostMessage(y,f,A)})}catch(A){b(f,400,{error:A instanceof Error?A.message:"Invalid request body"})}return}if(gt==="/mcp"){if(y.method!=="POST"&&y.method!=="GET"&&y.method!=="DELETE"){f.writeHead(405,{"Content-Type":"application/json",Allow:"GET, POST, DELETE"}),f.end(JSON.stringify({error:"Method not allowed"}));return}if(Et(C)){b(f,400,{error:"Internal auth via query params is not allowed. Use X-Kynver-User-Id and X-Kynver-Service-Secret headers."});return}let _=B(y,"mcp-session-id");if(_){let k=h[_];if(!k){b(f,404,{error:"Unknown or expired MCP session. Re-initialize."});return}if(!jt(y,C,k.auth,i)){b(f,401,{error:"Unauthorized. Re-send the same credentials used to initialize the MCP session."});return}if(!ft.check(`msg:${k.userId}`)){b(f,429,{error:"Too many requests. Slow down and retry."});return}k.lastActivity=Date.now();try{let ht=y.method==="POST"?await bt(y):void 0;await e.requestContext.run({userId:k.userId},async()=>{await k.transport.handleRequest(y,f,ht)})}catch(ht){f.headersSent||b(f,400,{error:ht instanceof Error?ht.message:"Invalid request body"})}return}if(y.method!=="POST"){b(f,400,{error:"Missing mcp-session-id header. Open a session with an initialize POST first."});return}if(!ft.check(`mcp:${Rt(y)}`)){b(f,429,{error:"Too many requests. Slow down and retry."});return}let v=null,A=null,Z=i?wt(y,i):null;if(Z)v=Z.userId,A=Z.auth;else{let k=vt(y);k&&(v=await Kt(o,k),v&&(A={kind:"apiKey",apiKey:k}))}if(!v||!A){b(f,401,{error:"Unauthorized. Pass API key via Authorization: Bearer <key> or internal X-Kynver-User-Id + X-Kynver-Service-Secret headers."});return}if(_t()>=N){b(f,503,{error:"Server at capacity. Try again shortly."});return}if(At(v)>=ut){b(f,429,{error:"Too many concurrent connections for this account."});return}let Tt;try{Tt=await bt(y)}catch(k){b(f,400,{error:k instanceof Error?k.message:"Invalid request body"});return}let $=v,Ht=A,F=new eo({sessionIdGenerator:()=>Xt(),onsessioninitialized:k=>{h[k]={transport:F,userId:$,auth:Ht,lastActivity:Date.now()},console.log(`[MCP HTTP] New session (${e.label}, user: ${$.slice(0,8)}...)`)}});F.onclose=()=>{F.sessionId&&delete h[F.sessionId]},await(await e.createMcpServer()).connect(F);try{await e.requestContext.run({userId:$},async()=>{await F.handleRequest(y,f,Tt)})}catch(k){f.headersSent||b(f,400,{error:k instanceof Error?k.message:"Invalid request"})}return}if(y.method==="GET"&&gt==="/health"){f.writeHead(200,{"Content-Type":"text/plain"}),f.end("ok");return}b(f,404,{error:"Not found"})});return Ct.listen(t,m,()=>{console.log(`[MCP SSE] ${e.label} listening on ${m}:${t}`)}),Ct}import{McpServer as un}from"@modelcontextprotocol/sdk/server/mcp.js";import{z as p}from"zod";import{AsyncLocalStorage as po}from"node:async_hooks";var z=class extends Error{constructor(t){super(t),this.name="EstimatorApiError"}};function lo(e){if(!e||typeof e!="object")return"";let t=e.name;return typeof t=="string"?t:""}function Ot(e){if(!(!e||typeof e!="object"))return e.cause}function V(e){let t=lo(e);if(t==="TimeoutError"||t==="AbortError")return!0;let o=Ot(e);return o?V(o):!1}function L(e){let t=e instanceof Error?e.message:String(e);if(/MCP error -32000\b/i.test(t)||/connection closed/i.test(t)||/fetch failed|ECONNRESET|ECONNREFUSED|ENOTFOUND|socket hang up|network error|timed out fetching a new connection|P2024|connection pool/i.test(t))return!0;let o=Ot(e);return o?L(o):!1}function J(e,t="save"){return V(e)?t==="read"?"The estimate service did not respond in time. Nothing was loaded.":"The estimate service did not respond in time. The save was not confirmed.":t==="read"?"The estimate service could not be reached. Nothing was loaded.":"The estimate service could not be reached. The save was not confirmed."}var Zt=2e4,Pt=new po,mo=process.env.KYNVER_API_URL??"https://kynver.com",kt=process.env.KYNVER_API_KEY??"";function uo(e){let t=e.startsWith("/")?e:`/${e}`;if(t.includes("..")||t.includes("\0"))throw new Error("Invalid API path: path traversal is not allowed")}function fo(e){uo(e);let t=e.startsWith("/")?e:`/${e}`,o=t.startsWith("/api")?t:`/api${t}`;return`${mo.replace(/\/$/,"")}${o}`}function go(){let e={"Content-Type":"application/json"};kt&&(e.Authorization=`Bearer ${kt}`);let t=Pt.getStore(),o=t?.userId??process.env.KYNVER_USER_ID?.trim(),i=process.env.KYNVER_SERVICE_SECRET?.trim();if(o){if(t?.sessionCredential)return t.authKind==="runnerToken"?(delete e.Authorization,e["X-Kynver-Runner-Token"]=t.sessionCredential):e.Authorization=`Bearer ${t.sessionCredential}`,e;let m=process.env.KYNVER_RUNNER_TOKEN?.trim();if(m){if(t?.authKind==="apiKey"||t?.authKind==="runnerToken")throw new Error("User-authenticated session did not propagate its credential; refusing the deployment-wide KYNVER_RUNNER_TOKEN (cross-tenant risk).");if(t?.authKind!=="internal")return e["X-Kynver-Runner-Token"]=m,e}if(!i)throw new Error("User context requires authentication: set KYNVER_RUNNER_TOKEN (preferred) or KYNVER_SERVICE_SECRET in the MCP server environment.");e["X-Kynver-User-Id"]=o,e["X-Kynver-Service-Secret"]=i}return e}async function ho(e){let t=await e.text();if(!e.ok){let o=t;try{let i=JSON.parse(t);o=i.error??i.message??t}catch{}throw new Error(o||`HTTP ${e.status}`)}if(t)try{return JSON.parse(t)}catch{return t}}async function St(e,t,o,i="save",m=Zt){let S;try{S=await fetch(fo(t),{method:e,headers:go(),...o!==void 0?{body:JSON.stringify(o)}:{},signal:AbortSignal.timeout(m)})}catch(h){throw h instanceof z?h:L(h)||V(h)?new z(J(h,i)):h}try{return await ho(S)}catch(h){throw L(h)?new z(J(h,i)):h}}async function g(e){return St("GET",e,void 0,"read")}async function u(e,t){return St("POST",e,t,"save")}async function P(e,t,o=Zt){return St("PATCH",e,t,"save",o)}async function R(e){return St("DELETE",e,void 0,"save")}function a(){return!!kt}function s(){return"No API key configured. Set KYNVER_API_KEY in your MCP server config."}var yo=p.object({message:p.string()}),Y={name:"estimator_import_catalog",description:"Start a catalog import from a file. This is a placeholder \u2014 MCP cannot do file uploads. Describe the import request in the message field and the system will guide the user through the upload flow.",inputSchemaZod:yo,execute:async e=>{if(!a())return s();let t=await u("/v1/agents/kynver-estimator/chat",{message:e.message});return JSON.stringify(t)}},So=p.object({importId:p.string(),approved:p.boolean().optional(),changes:p.array(p.object({index:p.number(),action:p.enum(["keep","skip","edit"]),edits:p.record(p.string(),p.unknown()).optional()})).optional()}),H={name:"estimator_review_import",description:"Review a pending catalog import batch before committing. Shows parsed rows with suggested service codes, categories, and pricing. You can approve all, skip individual rows, or edit values before committing. Requires an importId from a previous import.",inputSchemaZod:So,execute:async e=>{if(!a())return s();let{importId:t,...o}=e,i=await P(`/estimator/import/${t}`,o);return JSON.stringify(i)}},To=p.object({importId:p.string()}),G={name:"estimator_commit_import",description:"Finalize a reviewed catalog import, writing all approved rows into the user's service catalog. Requires an importId that has been reviewed. This action cannot be undone \u2014 rows are added permanently.",inputSchemaZod:To,execute:async e=>{if(!a())return s();let t=await u(`/estimator/import/${e.importId}/commit`,{});return JSON.stringify(t)}},bo=p.object({name:p.string(),serviceCode:p.string(),category:p.string(),materialType:p.string().nullable().optional(),unit:p.string(),materialCost:p.number().min(0),laborCost:p.number().min(0),minCharge:p.number().min(0).optional(),notes:p.string().optional()}),X={name:"estimator_setup_price",description:"Add a new service to the user's price book (service catalog). Every service needs: a unique serviceCode (e.g., CABINET_INSTALL, PAINT_WALLS, ROOF_INSPECT), a display name, category (general construction: demolition, framing, plumbing, electrical, hvac, drywall, paint, flooring, cabinetry, countertops, tile, windows, doors, insulation, appliances, general_conditions; roofing: tear_off, decking, underlayment, field_install, ridge_hip, flashing, misc, and others), unit (sq_ft, linear_ft, each, sheet, roll, bundle, hour, lump), and material/labor costs. Items in the catalog are used when creating estimates.",inputSchemaZod:bo,execute:async e=>{if(!a())return s();let t=await u("/estimator/catalog",e);return JSON.stringify(t)}},vo=p.object({itemId:p.string(),name:p.string().optional(),serviceCode:p.string().optional(),category:p.string().optional(),materialType:p.string().nullable().optional(),unit:p.string().optional(),materialCost:p.number().min(0).optional(),laborCost:p.number().min(0).optional(),minCharge:p.number().min(0).optional(),notes:p.string().optional()}),W={name:"estimator_update_price",description:"Update an existing item in the service catalog. Use estimator_get_catalog first to find the itemId. You can change the name, service code, category, unit, material cost, labor cost, minimum charge, or notes.",inputSchemaZod:vo,execute:async e=>{if(!a())return s();let{itemId:t,...o}=e,i=await P(`/estimator/catalog/${t}`,o);return JSON.stringify(i)}},wo=p.object({category:p.string().optional(),materialType:p.string().optional(),limit:p.number().optional(),cursor:p.string().optional()}),Q={name:"estimator_get_catalog",description:"List all items in the user's service catalog (price book). Returns service codes, names, categories, units, and material/labor costs. IMPORTANT: Always call this before creating estimates to get valid service codes. Optional filters must match enum strings. Categories include general construction (demolition, framing, drywall, paint, flooring, cabinetry, tile, electrical, plumbing, general_conditions, \u2026) and roofing (tear_off, decking, underlayment, field_install, ridge_hip, flashing, misc, \u2026). Materials include general construction (drywall, interior_paint, hardwood, ceramic_tile, stock_cabinet, quartz, \u2026) and roofing (asphalt_architectural, asphalt_3tab, metal_standing_seam, tpo, \u2026). When unsure, omit both filters to fetch the full catalog.",inputSchemaZod:wo,execute:async e=>{if(!a())return s();let t=new URLSearchParams;e.category&&t.set("category",e.category),e.materialType&&t.set("materialType",e.materialType),e.limit!=null&&t.set("limit",String(e.limit)),e.cursor&&t.set("cursor",e.cursor);let o=t.toString(),i=o?`/estimator/catalog?${o}`:"/estimator/catalog",m=await g(i);return JSON.stringify(m)}},ko=p.object({}),ee={name:"estimator_get_config",description:"Get the user's estimator settings \u2014 company name, default markup/waste percentage, tax rate, currency, contact info, license number, and default notes/terms for estimates.",inputSchemaZod:ko,execute:async e=>{if(!a())return s();let t=await g("/estimator/config");return JSON.stringify(t)}},Po=p.object({defaultMarkup:p.number().optional(),taxRate:p.number().optional(),companyName:p.string().optional(),companyPhone:p.string().optional(),companyEmail:p.string().optional(),companyAddress:p.string().optional(),companyLogo:p.string().optional(),licenseNumber:p.string().optional(),defaultTerms:p.string().optional()}),te={name:"estimator_update_config",description:"Update the user's estimator settings. You can set: companyName, defaultMarkup (waste %), taxRate, companyPhone, companyEmail, companyAddress, companyLogo (URL), licenseNumber (contractor CCB/license number), and defaultTerms (terms & conditions appended to estimates).",inputSchemaZod:Po,execute:async e=>{if(!a())return s();let t=await P("/estimator/config",e);return JSON.stringify(t)}},Io=p.object({templateId:p.string().optional()}),oe={name:"estimator_import_template",description:"Import a pre-built pricing catalog template into the user's service catalog. Call without arguments to see available templates (returns id, name, description, item count). Call with a templateId to import all items. general-construction-2026 is the general-construction starter (demolition, framing, drywall, paint, flooring, cabinetry, and the other trades). dealers-supply-2025 is optional roofing material pricing. Existing rows are not deleted. A legacy Misc row with no material for the same service code is moved onto the template category and material; prices, names, and notes stay. Other existing rows are skipped.",inputSchemaZod:Io,execute:async e=>{if(!a())return s();if(!e.templateId){let o=await g("/estimator/catalog/templates");return JSON.stringify(o)}let t=await u("/estimator/catalog/templates",{templateId:e.templateId});return JSON.stringify(t)}};import{z as E}from"zod";var _o=E.object({}),ie={name:"estimator_list_price_catalogs",description:"List durable builtin price catalogs shipped with the estimator (region, effective date, markup policy). These are the MCP's stored price lists \u2014 not AgentOS memory. Use before seeding or resolving remodel defaults.",inputSchemaZod:_o,execute:async e=>{if(!a())return s();let t=await g("/estimator/price-catalog");return JSON.stringify(t)}},Ao=E.object({catalogId:E.string()}),ne={name:"estimator_get_price_catalog",description:"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.",inputSchemaZod:Ao,execute:async e=>{if(!a())return s();let t=await g(`/estimator/price-catalog/${encodeURIComponent(e.catalogId)}`);return JSON.stringify(t)}},Co=E.object({serviceCode:E.string(),materialType:E.string().nullable().optional(),region:E.string().optional(),overrideMaterialCost:E.number().min(0).optional(),overrideLaborCost:E.number().min(0).optional()}),ae={name:"estimator_resolve_price",description:"Resolve unit pricing for one service code with provenance: user price book \u2192 builtin catalog \u2192 optional intake override (overrideMaterialCost/overrideLaborCost). Returns source, region, effective date, and markupIncluded flag.",inputSchemaZod:Co,execute:async e=>{if(!a())return s();let t=new URLSearchParams({serviceCode:e.serviceCode});e.materialType&&t.set("materialType",e.materialType),e.region&&t.set("region",e.region),e.overrideMaterialCost!=null&&t.set("overrideMaterialCost",String(e.overrideMaterialCost)),e.overrideLaborCost!=null&&t.set("overrideLaborCost",String(e.overrideLaborCost));let o=await g(`/estimator/price-catalog/resolve?${t.toString()}`);return JSON.stringify(o)}},Eo=E.object({catalogId:E.string(),overwrite:E.boolean().optional()}),se={name:"estimator_seed_price_catalog",description:"Copy a builtin price catalog into the user's price book. Missing codes are inserted. A legacy Misc row with no material for that code is reclassified; prices stay unless overwrite=true. Rows are never deleted. Use portland-remodel-will-2026-06 for the Portland general-construction working rates.",inputSchemaZod:Eo,execute:async e=>{if(!a())return s();let t=await u("/estimator/price-catalog/seed",e);return JSON.stringify(t)}};import{z as r}from"zod";var jo=new Set(["draft","final_status","sent","accepted","declined"]),Ko=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,No=2e3,Mo=8e3,Oo="projectName, status, clientId, companyId, siteAddress, notes (client-facing), internalNotes (contractor-only internal note)";function zt(e){if(e!==void 0)return e===null?null:e}function Lt(e,t,o){if(e!==void 0){if(e===null||e==="")return null;if(!Ko.test(e)){o.push(`${t} was not a valid id, so it was left unchanged.`);return}return e}}function Zo(e){let t=typeof e.estimateId=="string"?e.estimateId.trim():"";if(!t)return{ok:!1,error:"estimateId is required to update an estimate."};let o=[],i={},m=[];if(e.projectName!==void 0&&e.projectName!==null){let M=e.projectName.trim();if(!M)o.push("projectName was empty, so it was left unchanged.");else{if(M.length>200)return{ok:!1,error:"projectName is too long to save."};i.projectName=M,m.push("projectName")}}if(e.status!==void 0&&e.status!==null&&e.status!==""){let M=e.status.trim().toLowerCase();jo.has(M)?(i.status=M,m.push("status")):o.push(`status "${e.status}" is not a draft, sent, accepted, or declined value, so it was left unchanged.`)}let S=Lt(e.clientId,"clientId",o);S!==void 0&&(i.clientId=S,m.push("clientId"));let h=Lt(e.companyId,"companyId",o);if(h!==void 0&&(i.companyId=h,m.push("companyId")),e.siteAddress!==void 0){if(e.siteAddress!==null&&e.siteAddress.length>500)return{ok:!1,error:"siteAddress is too long to save."};i.siteAddress=e.siteAddress,m.push("siteAddress")}let N=zt(e.notes);if(N!==void 0){if(N!==null&&N.length>No)return{ok:!1,error:"The client-facing note is too long to save."};i.notes=N,m.push("notes")}let ut=e.internalNotes!==void 0?e.internalNotes:e.internalNote,q=zt(ut);if(q!==void 0){if(q!==null&&q.length>Mo)return{ok:!1,error:"The internal note is too long to save."};i.internalNotes=q,m.push("internalNotes")}return m.length===0?{ok:!1,error:`No estimate fields to update.${o.length?` ${o.join(" ")}`:""} Send estimateId and at least one of: ${Oo}.`}:{ok:!0,estimateId:t,body:i,changed:m,warnings:o}}function zo(e,t,o){let i=e&&typeof e=="object"?e:{};return JSON.stringify({ok:!0,id:typeof i.id=="string"?i.id:null,projectName:i.projectName??null,status:i.status??null,notes:i.notes??null,internalNotes:i.internalNotes??null,changed:t,...o.length?{warnings:o}:{}})}function Lo(e){return e instanceof Error&&e.name==="EstimatorApiError"?e.message:V(e)||L(e)?J(e):(e instanceof Error?e.message:String(e)).replace(/MCP error\s*-32000:?/gi,"").replace(/connection closed/gi,"the save did not finish").trim()||"The estimate update did not finish. The save was not confirmed."}async function Ut(e,t){let o=Zo(e);if(!o.ok)return JSON.stringify({ok:!1,error:o.error});try{let i=await t(`/estimator/estimates/${o.estimateId}`,o.body);return zo(i,o.changed,o.warnings)}catch(i){return JSON.stringify({ok:!1,error:Lo(i)})}}var Uo=r.object({projectName:r.string(),roofMaterial:r.string(),footprintSqFt:r.number().optional(),roofAreaSqFt:r.number().optional(),pitch:r.string().optional(),layers:r.number().optional(),ridgeLengthFt:r.number(),hipLengthFt:r.number().optional(),valleyLengthFt:r.number().optional(),eavesFt:r.number(),rakeFt:r.number().optional(),wastePercent:r.number().optional(),clientId:r.string().optional(),companyId:r.string().optional(),notes:r.string().optional()}),re={name:"estimator_create_replacement",description:"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 \u2014 call estimator_get_catalog first to verify.",inputSchemaZod:Uo,execute:async e=>{if(!a())return s();let{projectName:t,clientId:o,companyId:i,notes:m,...S}=e,h=await u("/estimator/estimates",{type:"replacement",projectName:t,roofMaterial:S.roofMaterial,inputs:S,clientId:o,companyId:i,notes:m});return JSON.stringify(h)}},qo=r.object({serviceCode:r.string(),materialType:r.string().nullable().optional(),quantity:r.number(),notes:r.string().optional()}),$o=r.object({projectName:r.string(),roofMaterial:r.string().optional(),lineItems:r.array(qo),clientId:r.string().optional(),companyId:r.string().optional(),notes:r.string().optional()}),ce={name:"estimator_create_repair",description:"Create a repair/service estimate with specific line items. Each line item needs a serviceCode that exists in the user's catalog \u2014 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.",inputSchemaZod:$o,execute:async e=>{if(!a())return s();let t=await u("/estimator/estimates",{type:"repair_service",projectName:e.projectName,roofMaterial:e.roofMaterial,lineItems:e.lineItems,clientId:e.clientId,companyId:e.companyId,notes:e.notes});return JSON.stringify(t)}},Jo=r.object({id:r.string()}),le={name:"estimator_get_estimate",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.",inputSchemaZod:Jo,execute:async e=>{if(!a())return s();let t=await g(`/estimator/estimates/${e.id}`);return JSON.stringify(t)}},Do=r.object({status:r.string().optional(),type:r.string().optional(),clientId:r.string().optional(),companyId:r.string().optional(),from:r.string().optional(),to:r.string().optional(),limit:r.number().optional(),cursor:r.string().optional()}),pe={name:"estimator_list_estimates",description:"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 \u2014 use estimator_get_estimate for full details on a specific one.",inputSchemaZod:Do,execute:async e=>{if(!a())return s();let t=new URLSearchParams;e.status&&t.set("status",e.status),e.type&&t.set("type",e.type),e.clientId&&t.set("clientId",e.clientId),e.companyId&&t.set("companyId",e.companyId),e.from&&t.set("from",e.from),e.to&&t.set("to",e.to),e.limit!=null&&t.set("limit",String(e.limit)),e.cursor&&t.set("cursor",e.cursor);let o=t.toString(),i=o?`/estimator/estimates?${o}`:"/estimator/estimates",m=await g(i);return JSON.stringify(m)}},Fo=r.object({estimateId:r.string(),serviceCode:r.string(),materialType:r.string().nullable().optional(),quantity:r.number(),notes:r.string().optional()}),me={name:"estimator_add_line_item",description:"Add a line item to an existing estimate. The serviceCode must exist in the user's catalog \u2014 call estimator_get_catalog to verify. Pass: estimateId, serviceCode, quantity. Optionally add notes. The estimate totals are automatically recalculated.",inputSchemaZod:Fo,execute:async e=>{if(!a())return s();let{estimateId:t,...o}=e,i=await u(`/estimator/estimates/${t}/line-items`,o);return JSON.stringify(i)}},Vo=r.object({estimateId:r.string(),lineItemId:r.string()}),de={name:"estimator_remove_line_item",description:"Remove a line item from an estimate. Requires estimateId and lineItemId (get lineItemIds from estimator_get_estimate). The estimate totals are automatically recalculated.",inputSchemaZod:Vo,execute:async e=>{if(!a())return s();let t=await R(`/estimator/estimates/${e.estimateId}/line-items/${e.lineItemId}`);return JSON.stringify(t)}},Bo=r.object({estimateId:r.string(),lineItemId:r.string(),quantity:r.number().optional(),materialCost:r.number().optional(),laborCost:r.number().optional(),notes:r.string().optional()}),ue={name:"estimator_update_line_item",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.",inputSchemaZod:Bo,execute:async e=>{if(!a())return s();let{estimateId:t,lineItemId:o,...i}=e,m=await P(`/estimator/estimates/${t}/line-items/${o}`,i);return JSON.stringify(m)}},Yo=r.object({estimateId:r.string(),targetTotal:r.number(),strategy:r.enum(["uniform","labor_only","material_only"]).optional()}),fe={name:"estimator_adjust_to_target",description:"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 \u2014 scales all items proportionally), 'labor_only' (only adjusts labor costs), 'material_only' (only adjusts material costs). Requires estimateId and targetTotal.",inputSchemaZod:Yo,execute:async e=>{if(!a())return s();let{estimateId:t,...o}=e,i=await u(`/estimator/estimates/${t}/adjust`,o);return JSON.stringify(i)}},Ho=r.object({estimateId:r.string(),projectName:r.string().optional(),status:r.string().optional(),clientId:r.string().nullable().optional(),companyId:r.string().nullable().optional(),notes:r.string().nullable().optional(),internalNotes:r.string().nullable().optional(),internalNote:r.string().nullable().optional(),siteAddress:r.string().nullable().optional()}),ge={name:"estimator_update_estimate",description:"Update an estimate's project name, status (draft, sent, accepted, or declined), site address, client or company link, client-facing notes, or the contractor-only internal note. Use internalNotes for an internal note the customer must not see. Use notes for text that can be shown to the customer. internalNote is another name for internalNotes. Does not change line items. Pass estimateId and only the fields to change. A result with ok:false means the save did not stick.",inputSchemaZod:Ho,execute:async e=>a()?Ut(e,(t,o)=>P(t,o)):s()},Go=r.object({estimateId:r.string()}),he={name:"estimator_delete_estimate",description:"Soft-delete an estimate. The estimate data is preserved but hidden from list views. This cannot be easily undone.",inputSchemaZod:Go,execute:async e=>{if(!a())return s();let t=await R(`/estimator/estimates/${e.estimateId}`);return JSON.stringify(t)}},Xo=r.object({estimateId:r.string(),projectName:r.string().optional()}),ye={name:"estimator_duplicate_estimate",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.",inputSchemaZod:Xo,execute:async e=>{if(!a())return s();let{estimateId:t,...o}=e,i=await u(`/estimator/estimates/${t}/duplicate`,o);return JSON.stringify(i)}};import{z as j}from"zod";var Qo=j.object({estimateId:j.string(),name:j.string()}),Se={name:"estimator_save_template",description:"Save an existing estimate as a reusable template. Templates capture the estimate's line items, quantities, and structure so you can quickly create similar estimates in the future. Requires the estimateId of the source estimate and a name for the template.",inputSchemaZod:Qo,execute:async e=>{if(!a())return s();let t=await u("/estimator/templates",e);return JSON.stringify(t)}},ei=j.object({type:j.string().optional()}),Te={name:"estimator_list_templates",description:"List all saved estimate templates. Optionally filter by type: 'replacement' or 'repair_service'. Returns template names, types, and IDs. Use template IDs with estimator_create_from_template to create new estimates.",inputSchemaZod:ei,execute:async e=>{if(!a())return s();let t=new URLSearchParams;e.type&&t.set("type",e.type);let o=t.toString(),i=o?`/estimator/templates?${o}`:"/estimator/templates",m=await g(i);return JSON.stringify(m)}},ti=j.object({id:j.string()}),be={name:"estimator_delete_template",description:"Delete a saved estimate template permanently. This does not affect any estimates that were previously created from this template.",inputSchemaZod:ti,execute:async e=>{if(!a())return s();let t=await R(`/estimator/templates/${e.id}`);return JSON.stringify(t)}},oi=j.object({templateId:j.string(),projectName:j.string(),clientId:j.string().optional()}),ve={name:"estimator_create_from_template",description:"Create a new estimate from a saved template. The new estimate gets all the template's line items and structure. Requires: templateId (from estimator_list_templates), projectName. Optionally link to a clientId. The new estimate is created as a draft.",inputSchemaZod:oi,execute:async e=>{if(!a())return s();let t=await u("/estimator/estimates",e);return JSON.stringify(t)}};import{z as we}from"zod";var ni=we.object({estimateId:we.string()}),ke={name:"estimator_list_versions",description:"List all saved version snapshots of an estimate. Versions are created automatically when significant changes are made. Shows version numbers and timestamps. Use this to track the history of changes to an estimate.",inputSchemaZod:ni,execute:async e=>{if(!a())return s();let t=await g(`/estimator/estimates/${e.estimateId}/versions`);return JSON.stringify(t)}},ai=we.object({estimateId:we.string(),versionId:we.string()}),Pe={name:"estimator_get_version",description:"Get a specific historical version of an estimate. Returns the full estimate snapshot as it existed at that point in time \u2014 all line items, totals, and metadata. Useful for comparing current vs previous versions or reverting changes.",inputSchemaZod:ai,execute:async e=>{if(!a())return s();let t=await g(`/estimator/estimates/${e.estimateId}/versions/${e.versionId}`);return JSON.stringify(t)}};import{z as O}from"zod";var ri=O.object({estimateId:O.string(),imageUrl:O.string(),caption:O.string().optional()}),Ie={name:"estimator_upload_photo",description:"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.",inputSchemaZod:ri,execute:async e=>{if(!a())return s();let{estimateId:t,...o}=e,i=await u(`/estimator/estimates/${t}/photos`,o);return JSON.stringify(i)}},ci=O.object({estimateId:O.string()}),xe={name:"estimator_list_photos",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.",inputSchemaZod:ci,execute:async e=>{if(!a())return s();let t=await g(`/estimator/estimates/${e.estimateId}/photos`);return JSON.stringify(t)}},li=O.object({estimateId:O.string(),photoId:O.string()}),_e={name:"estimator_delete_photo",description:"Remove a photo from an estimate. Requires estimateId and photoId (get IDs from estimator_list_photos).",inputSchemaZod:li,execute:async e=>{if(!a())return s();let t=await R(`/estimator/estimates/${e.estimateId}/photos/${e.photoId}`);return JSON.stringify(t)}};import{z as x}from"zod";var mi=x.object({from:x.string().optional(),to:x.string().optional()}),Ae={name:"estimator_get_stats",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.",inputSchemaZod:mi,execute:async e=>{if(!a())return s();let t=new URLSearchParams;e.from&&t.set("from",e.from),e.to&&t.set("to",e.to);let o=t.toString(),i=o?`/estimator/stats?${o}`:"/estimator/stats",m=await g(i);return JSON.stringify(m)}},di=x.object({estimateId:x.string()}),Ce={name:"estimator_export_pdf",description:"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 \u2014 use estimator_update_output_config to customize which columns, sections, and details appear. The PDF includes company branding from estimator config.",inputSchemaZod:di,execute:async e=>{if(!a())return s();try{await g(`/estimator/estimates/${e.estimateId}/pdf`)}catch{}return JSON.stringify({message:"PDF generated successfully.",downloadUrl:`/api/estimator/estimates/${e.estimateId}/pdf`})}},ui=x.object({estimateId:x.string(),showMaterialCost:x.boolean().optional(),showLaborCost:x.boolean().optional(),showUnitPrice:x.boolean().optional(),showQuantity:x.boolean().optional(),showNotes:x.boolean().optional(),headerText:x.string().optional(),footerText:x.string().optional(),template:x.string().optional()}),Ee={name:"estimator_update_output_config",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 \u2014 the underlying estimate data is not changed.",inputSchemaZod:ui,execute:async e=>{if(!a())return s();let{estimateId:t,...o}=e,i=await P(`/estimator/estimates/${t}/output-config`,o);return JSON.stringify(i)}};import{z as U}from"zod";var gi=U.object({estimateId:U.string(),recipient:U.string().email().optional(),ccRecipients:U.array(U.string().email()).max(10).optional().default([]),subject:U.string().min(1).max(200).optional(),message:U.string().min(1).max(1e4).optional(),confirmExternalRecipient:U.boolean().optional().default(!1)}),Re={name:"estimator_email_pdf",description:"Email an estimate PDF to a primary recipient with optional CC recipients in one group email. Fetches and attaches the PDF server-side (no secrets exposed). Defaults to the account owner's email when recipient is omitted. Estimate client emails and linked client records are trusted without extra confirmation; any external To or CC address requires confirmExternalRecipient=true after explicit user confirmation. Respects the estimate approval gate before customer send. Returns send status and audit metadata.",inputSchemaZod:gi,execute:async e=>{if(!a())return s();let{estimateId:t,...o}=e,i=await u(`/estimator/estimates/${t}/email-pdf`,o);return JSON.stringify(i)}};import{z as D}from"zod";var yi=D.object({images:D.array(D.string()),prompt:D.string().optional()}),je={name:"estimator_analyze_photos",description:"Analyze roof photos using Claude's vision AI. Provide image URLs (can be multiple). The AI identifies: roofing materials, visible damage (missing shingles, cracks, moss, ponding), approximate measurements, and repair recommendations. Use the optional prompt parameter to ask specific questions about the photos. Results can inform estimate creation.",inputSchemaZod:yi,execute:async e=>{if(!a())return s();let t=await u("/estimator/analyze",{type:"photos",images:e.images,...e.prompt?{prompt:e.prompt}:{}});return JSON.stringify(t)}},Si=D.object({text:D.string(),prompt:D.string().optional()}),Ke={name:"estimator_analyze_document",description:"Analyze a text document (inspection report, insurance scope of work, contractor notes) and extract structured estimating data \u2014 identified services, quantities, materials, and damage descriptions. Pass the document text in the text field. Use the optional prompt parameter to focus the analysis. Results can be used to create estimates with accurate line items.",inputSchemaZod:Si,execute:async e=>{if(!a())return s();let t=await u("/estimator/analyze",{type:"document",text:e.text,...e.prompt?{prompt:e.prompt}:{}});return JSON.stringify(t)}};import{z as w}from"zod";var bi=w.object({name:w.string(),phone:w.string().optional(),email:w.string().optional(),address:w.string().optional(),notes:w.string().optional()}),Ne={name:"estimator_create_company",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.",inputSchemaZod:bi,execute:async e=>{if(!a())return s();let t=await u("/estimator/companies",e);return JSON.stringify(t)}},vi=w.object({search:w.string().optional(),limit:w.number().optional(),cursor:w.string().optional()}),Me={name:"estimator_list_companies",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.",inputSchemaZod:vi,execute:async e=>{if(!a())return s();let t=new URLSearchParams;e.search&&t.set("search",e.search),e.limit!=null&&t.set("limit",String(e.limit)),e.cursor&&t.set("cursor",e.cursor);let o=t.toString(),i=o?`/estimator/companies?${o}`:"/estimator/companies",m=await g(i);return JSON.stringify(m)}},wi=w.object({companyId:w.string()}),Oe={name:"estimator_get_company",description:"Get a specific company's full details including all its client contacts and a summary of linked estimates.",inputSchemaZod:wi,execute:async e=>{if(!a())return s();let t=await g(`/estimator/companies/${e.companyId}`);return JSON.stringify(t)}},ki=w.object({companyId:w.string(),name:w.string().optional(),phone:w.string().optional(),email:w.string().optional(),address:w.string().optional(),notes:w.string().optional()}),Ze={name:"estimator_update_company",description:"Update a company's details. Pass the companyId and any fields to change: name, phone, email, address, or notes.",inputSchemaZod:ki,execute:async e=>{if(!a())return s();let{companyId:t,...o}=e,i=await P(`/estimator/companies/${t}`,o);return JSON.stringify(i)}},Pi=w.object({companyId:w.string()}),ze={name:"estimator_delete_company",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 \u2014 they become unlinked.",inputSchemaZod:Pi,execute:async e=>{if(!a())return s();let t=await R(`/estimator/companies/${e.companyId}`);return JSON.stringify(t)}};import{z as T}from"zod";var xi=T.object({name:T.string(),companyId:T.string().optional(),title:T.string().optional(),phone:T.string().optional(),email:T.string().optional(),address:T.string().optional(),notes:T.string().optional()}),Le={name:"estimator_create_client",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.",inputSchemaZod:xi,execute:async e=>{if(!a())return s();let t=await u("/estimator/clients",e);return JSON.stringify(t)}},_i=T.object({search:T.string().optional(),companyId:T.string().optional(),limit:T.number().optional(),cursor:T.string().optional()}),Ue={name:"estimator_list_clients",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.",inputSchemaZod:_i,execute:async e=>{if(!a())return s();let t=new URLSearchParams;e.search&&t.set("search",e.search),e.companyId&&t.set("companyId",e.companyId),e.limit!=null&&t.set("limit",String(e.limit)),e.cursor&&t.set("cursor",e.cursor);let o=t.toString(),i=o?`/estimator/clients?${o}`:"/estimator/clients",m=await g(i);return JSON.stringify(m)}},Ai=T.object({clientId:T.string()}),qe={name:"estimator_get_client",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.",inputSchemaZod:Ai,execute:async e=>{if(!a())return s();let t=await g(`/estimator/clients/${e.clientId}`);return JSON.stringify(t)}},Ci=T.object({clientId:T.string(),name:T.string().optional(),companyId:T.string().optional(),title:T.string().optional(),phone:T.string().optional(),email:T.string().optional(),address:T.string().optional(),notes:T.string().optional()}),$e={name:"estimator_update_client",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).",inputSchemaZod:Ci,execute:async e=>{if(!a())return s();let{clientId:t,...o}=e,i=await P(`/estimator/clients/${t}`,o);return JSON.stringify(i)}},Ei=T.object({clientId:T.string()}),Je={name:"estimator_delete_client",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.",inputSchemaZod:Ei,execute:async e=>{if(!a())return s();let t=await R(`/estimator/clients/${e.clientId}`);return JSON.stringify(t)}};import{z as c}from"zod";var It=c.object({projectId:c.string().uuid()}),ji=c.object({projectName:c.string(),buildingType:c.enum(["commercial","residential"]),siteAddress:c.string().optional(),clientId:c.string().optional(),companyId:c.string().optional()}),Ki=c.object({}),Ni=c.object({cabinetLinearFt:c.number().nonnegative().optional(),rooms:c.array(c.object({name:c.string(),lengthFt:c.number().positive().optional(),widthFt:c.number().positive().optional()})).optional(),baths:c.array(c.object({type:c.enum(["full","half","three_quarter","unknown"]).optional(),count:c.number().int().positive().optional(),label:c.string().optional()})).optional(),flooringSqFt:c.number().nonnegative().optional(),drywallSqFt:c.number().nonnegative().optional(),paintSqFt:c.number().nonnegative().optional(),roofSqFt:c.number().nonnegative().optional(),flags:c.object({wallMovingRisk:c.boolean().optional(),electricalSurpriseRisk:c.boolean().optional(),plumbingSurpriseRisk:c.boolean().optional()}).optional(),notes:c.string().optional()}).optional(),Mi=c.object({projectId:c.string().uuid(),kind:c.string(),fileName:c.string(),base64:c.string(),mimeType:c.string().optional(),remodelHints:Ni}),Oi=c.object({projectId:c.string().uuid(),reviewState:c.string().optional()}),Zi=c.object({projectId:c.string().uuid(),rowId:c.string().uuid(),action:c.enum(["approve","skip","edit"]),edits:c.object({description:c.string().optional(),quantity:c.number().optional(),unit:c.string().optional(),serviceCode:c.string().nullable().optional()}).optional()}),zi=c.object({projectId:c.string().uuid(),overrideNote:c.string().optional()}),Li=c.object({segment:c.enum(["commercial","residential"]).optional()}),Ui=c.object({projectId:c.string().uuid(),systemPackSlug:c.string(),serviceProfileId:c.string().optional()}),qi=c.object({}),$i=c.object({slug:c.string(),name:c.string(),segment:c.enum(["commercial_service","commercial_production","residential_service","residential_production"]),laborMultiplier:c.number().optional(),defaultWastePercent:c.number().optional()}),Ji=c.object({estimateId:c.string().uuid(),note:c.string().optional()}),Di=c.object({estimateId:c.string().uuid(),approvalId:c.string().uuid(),action:c.enum(["approve","reject"]),note:c.string().optional()}),Fi=c.object({estimateId:c.string().uuid()}),De={name:"estimator_create_takeoff_project",description:"Create a takeoff project shell (commercial or residential). Upload inputs, run extract, review rows, commit, apply system pack, then generate estimate.",inputSchemaZod:ji,execute:async e=>{if(!a())return s();let t=await u("/estimator/takeoffs",e);return JSON.stringify(t)}},Fe={name:"estimator_get_takeoff_project",description:"Get takeoff project with inputs, rows, status, pack/profile, linked estimate.",inputSchemaZod:It,execute:async e=>{if(!a())return s();let t=await g(`/estimator/takeoffs/${e.projectId}`);return JSON.stringify(t)}},Ve={name:"estimator_list_takeoff_projects",description:"List takeoff projects for the workspace.",inputSchemaZod:Ki,execute:async e=>{if(!a())return s();let t=await g("/estimator/takeoffs");return JSON.stringify(t)}},Be={name:"estimator_upload_takeoff_input",description:"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) \u2014 quantities are reviewable, not auto-measured from pixels.",inputSchemaZod:Mi,execute:async e=>{if(!a())return s();let t=await u(`/estimator/takeoffs/${e.projectId}/inputs`,{kind:e.kind,fileName:e.fileName,base64:e.base64,...e.mimeType?{mimeType:e.mimeType}:{},...e.remodelHints?{remodelHints:e.remodelHints}:{}});return JSON.stringify(t)}},Ye={name:"estimator_run_takeoff_extract",description:"Run takeoff extractors on all uploaded inputs (idempotent).",inputSchemaZod:It,execute:async e=>{if(!a())return s();let t=await u(`/estimator/takeoffs/${e.projectId}/extract`,{});return JSON.stringify(t)}},He={name:"estimator_list_takeoff_rows",description:"List takeoff rows with evidence and review state.",inputSchemaZod:Oi,execute:async e=>{if(!a())return s();let t=e.reviewState?`?reviewState=${e.reviewState}`:"",o=await g(`/estimator/takeoffs/${e.projectId}/rows${t}`);return JSON.stringify(o)}},Ge={name:"estimator_review_takeoff_row",description:"Approve, skip, or edit a single takeoff row.",inputSchemaZod:Zi,execute:async e=>{if(!a())return s();let{projectId:t,rowId:o,...i}=e,m=await P(`/estimator/takeoffs/${t}/rows/${o}`,i);return JSON.stringify(m)}},Xe={name:"estimator_commit_takeoff",description:"Mark takeoff ready after all rows reviewed and area measurement approved (or override note).",inputSchemaZod:zi,execute:async e=>{if(!a())return s();let t=await u(`/estimator/takeoffs/${e.projectId}/commit`,{overrideNote:e.overrideNote});return JSON.stringify(t)}},We={name:"estimator_list_system_packs",description:"List built-in commercial and residential system packs.",inputSchemaZod:Li,execute:async e=>{if(!a())return s();let t=e.segment?`?segment=${e.segment}`:"",o=await g(`/estimator/system-packs${t}`);return JSON.stringify(o)}},Qe={name:"estimator_apply_system_pack",description:"Set system pack slug and optional service profile on takeoff project.",inputSchemaZod:Ui,execute:async e=>{if(!a())return s();let t=await P(`/estimator/takeoffs/${e.projectId}`,{systemPackSlug:e.systemPackSlug,serviceProfileId:e.serviceProfileId??null});return JSON.stringify(t)}},et={name:"estimator_list_service_profiles",description:"List service department profiles (includes v6 defaults).",inputSchemaZod:qi,execute:async e=>{if(!a())return s();let t=await g("/estimator/service-profiles");return JSON.stringify(t)}},tt={name:"estimator_upsert_service_profile",description:"Create or update a service department profile.",inputSchemaZod:$i,execute:async e=>{if(!a())return s();let t=await u("/estimator/service-profiles",e);return JSON.stringify(t)}},ot={name:"estimator_generate_estimate_from_takeoff",description:"Generate draft estimate from committed takeoff + system pack. Preflight fails if catalog codes missing.",inputSchemaZod:It,execute:async e=>{if(!a())return s();let t=await u(`/estimator/takeoffs/${e.projectId}/generate-estimate`,{});return JSON.stringify(t)}},it={name:"estimator_request_estimate_approval",description:"Request owner approval before customer send.",inputSchemaZod:Ji,execute:async e=>{if(!a())return s();let t=await u(`/estimator/estimates/${e.estimateId}/approval`,{note:e.note});return JSON.stringify(t)}},nt={name:"estimator_approve_estimate",description:"Approve or reject estimate (workspace owner).",inputSchemaZod:Di,execute:async e=>{if(!a())return s();let t=await u(`/estimator/estimates/${e.estimateId}/approval`,{approvalId:e.approvalId,action:e.action,note:e.note});return JSON.stringify(t)}},at={name:"estimator_get_estimate_approval",description:"Get latest approval status for an estimate.",inputSchemaZod:Fi,execute:async e=>{if(!a())return s();let t=await g(`/estimator/estimates/${e.estimateId}/approval`);return JSON.stringify(t)}};import{z as l}from"zod";var Bi=l.enum(["kitchen_remodel","bath_remodel","kitchen_and_bath_remodel","whole_home_remodel","other_remodel"]),Yi=l.object({projectType:Bi,projectName:l.string().optional(),siteAddress:l.string().optional(),baseScopes:l.array(l.object({key:l.string().optional(),label:l.string(),quantity:l.number().optional(),unit:l.string().optional(),serviceCode:l.string().optional(),budgetLump:l.number().optional(),notes:l.string().optional(),confidence:l.enum(["high","medium","low","unknown"]).optional(),isAlternate:l.boolean().optional(),alternateGroup:l.string().optional()})).optional(),alternates:l.array(l.object({key:l.string(),label:l.string(),scopeKeys:l.array(l.string()).optional(),lumpAdd:l.number().optional(),notes:l.string().optional()})).optional(),attachments:l.array(l.object({id:l.string(),kind:l.enum(["photo","drawing","plan_set","spec_pdf","spreadsheet","other"]),fileName:l.string().optional(),caption:l.string().optional(),url:l.string().optional(),mimeType:l.string().optional()})).optional(),exclusions:l.array(l.string()).optional(),surprises:l.array(l.string()).optional(),rateReferences:l.array(l.object({key:l.string(),label:l.string(),value:l.number(),unit:l.string().optional(),source:l.enum(["operating_rule","catalog","user","takeoff"]),catalogId:l.string().optional(),confidence:l.enum(["high","medium","low","unknown"]).optional()})).optional(),assumptionsNeedingReview:l.array(l.object({id:l.string(),text:l.string(),severity:l.enum(["info","warn","blocker"]),relatedScopeKey:l.string().optional()})).optional(),remodelHints:l.record(l.string(),l.unknown()).optional(),includeStandardAlternates:l.boolean().optional(),notes:l.string().optional()}),st={name:"estimator_normalize_remodel_intake",description:"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.",inputSchemaZod:Yi,execute:async e=>{if(!a())return s();let t=await u("/estimator/intake/normalize-remodel",e);return JSON.stringify(t,null,2)}};import{z as d}from"zod";var Gi=d.object({key:d.string(),label:d.string().optional(),quantity:d.number().optional(),unit:d.string().optional(),serviceCode:d.string().optional(),budgetLump:d.number().optional(),notes:d.string().optional()}),Xi=d.object({intake:d.object({projectName:d.string().optional(),scopes:d.array(Gi),alternates:d.array(d.object({key:d.string(),label:d.string(),scopeKeys:d.array(d.string()).optional(),lumpAdd:d.number().optional(),notes:d.string().optional()})).optional(),includeStandardAlternates:d.boolean().optional(),expandScopeComponents:d.boolean().optional(),exclusions:d.array(d.string()).optional(),assumptions:d.array(d.string()).optional(),notes:d.string().optional()}),takeoffRows:d.array(d.object({id:d.string().optional(),description:d.string(),quantity:d.number(),unit:d.string(),serviceCode:d.string().nullable().optional(),materialType:d.string().nullable().optional()})).optional(),overrides:d.array(d.object({scopeKey:d.string().optional(),serviceCode:d.string().optional(),description:d.string().optional(),quantity:d.number().optional(),unitMaterial:d.number().optional(),unitLabor:d.number().optional(),lumpTotal:d.number().optional(),notes:d.string().optional()})).optional(),pricing:d.object({contingencyPercent:d.number().optional(),markupPercent:d.number().optional(),profitPercent:d.number().optional()}).optional()}),rt={name:"estimator_build_remodel_estimate",description:"Build a reviewable residential remodel estimate from normalized intake scopes, optional takeoff quantities, catalog prices, and overrides. Lump room scopes (kitchen, baths) auto-expand into a per-trade breakdown of line items (demolition, cabinetry, countertops, plumbing, electrical, appliances, flooring, tile, finishes) \u2014 each with its own scope-of-work description carried in notes \u2014 so client-facing output is itemized, not a single vague lump (set intake.expandScopeComponents=false for the legacy single-line behavior). Returns line items with provenance, alternates (both baths, upstairs expansion, roof, wall moving), a default+custom assumptions list, default+custom exclusions, contingency/markup/profit breakdown, and reviewMarkdown. Pass project-specific items via intake.assumptions and intake.exclusions. Does not persist \u2014 use estimator_generate_estimate_from_takeoff or estimator_create_repair after review. Room scope keys: kitchen, bath_full, bath_half, bath_primary, bath_secondary. Trade/quantity scope keys: cabinets, drywall, paint, framing, roof, flooring, electrical_kitchen, electrical_bath. Whole-project phase scope keys: general_conditions, permits, demolition, protection, final_clean.",inputSchemaZod:Xi,execute:async e=>{if(!a())return s();let t=await u("/estimator/estimates/build-remodel",e);return JSON.stringify(t)}};import{z as n}from"zod";var qt=n.object({intake:n.object({projectName:n.string().optional(),scopes:n.array(n.object({key:n.string(),label:n.string().optional(),quantity:n.number().optional(),unit:n.string().optional(),serviceCode:n.string().optional(),budgetLump:n.number().optional(),notes:n.string().optional()})),expandScopeComponents:n.boolean().optional(),includeStandardAlternates:n.boolean().optional(),exclusions:n.array(n.string()).optional(),assumptions:n.array(n.string()).optional(),notes:n.string().optional()}),pricing:n.object({contingencyPercent:n.number().optional(),markupPercent:n.number().optional(),profitPercent:n.number().optional()}).optional()}),Qi=n.object({preset:n.enum(["matthew-portland-2026-06"]).optional(),client:n.object({projectTitle:n.string(),clientName:n.string().optional(),preparedBy:n.string().optional(),location:n.string().optional(),documentType:n.string().optional(),validityNote:n.string().optional()}),budgetPackage:qt.optional(),fullPackage:qt.optional(),alternates:n.array(n.object({key:n.string(),label:n.string(),description:n.string(),budgetPlaceholder:n.number().nullable()})).optional(),correctionNote:n.string().optional(),scopeBasis:n.array(n.string()).optional()}),ct={name:"estimator_build_remodel_deliverable",description:"Build a residential remodel deliverable package using the Matthew v3 golden output standard: contractor planning-budget tone, cover/header, executive summary option cards, scope basis, pricing summary, numbered detailed line items, key allowances, assumptions, exclusions, separated Option C alternates (never in headline totals), recommended next steps, and footer/validity language. Returns clientMarkdown + clientHtml + handoff JSON. Use preset matthew-portland-2026-06 for the reviewed Matthew Portland fixture ($339,025 full / $167,200 bank-budget; golden fixture at remodel-estimates/saved/2026-06-03-matthew-remodel-reviewed-v3/). Guards against superseded totals and generic AI prose. Does not persist \u2014 use clientHtml or handoff for PDF. For single-scope preview only, use estimator_build_remodel_estimate. Prefer estimator_draft_remodel_deliverable when you have scope notes and want the tool to assemble budget/full intakes.",inputSchemaZod:Qi,execute:async e=>{if(!a())return s();let t=await u("/estimator/estimates/build-remodel-deliverable",e);return JSON.stringify(t)}},$t=n.object({key:n.string().min(1),label:n.string().optional(),quantity:n.number().positive().optional(),unit:n.string().optional(),serviceCode:n.string().optional(),budgetLump:n.number().nonnegative().optional(),notes:n.string().optional()}),en=n.object({contingencyPercent:n.number().optional(),markupPercent:n.number().optional(),profitPercent:n.number().optional()}).optional(),Jt=n.object({label:n.string().optional(),whatsIncluded:n.string().optional(),projectName:n.string().optional(),scopes:n.array($t),assumptions:n.array(n.string()).optional(),exclusions:n.array(n.string()).optional(),notes:n.string().optional(),expandScopeComponents:n.boolean().optional(),pricing:en}),tn=Jt.extend({derivedFromFull:n.object({omitFromFullKeys:n.array(n.string()).optional(),replaceWithAllowanceLump:n.array(n.object({key:n.string().min(1),label:n.string().optional(),budgetLump:n.number().nonnegative(),notes:n.string().optional()})).optional(),additionalScopes:n.array($t).optional()}).optional()}),on=n.object({client:n.object({projectTitle:n.string().min(1),clientName:n.string().optional(),preparedBy:n.string().optional(),location:n.string().optional(),documentType:n.string().optional(),validityNote:n.string().optional()}),fullScope:Jt,budgetScope:tn.optional(),alternates:n.array(n.object({key:n.string(),label:n.string(),description:n.string(),budgetPlaceholder:n.number().nullable()})).optional(),correctionNote:n.string().optional(),scopeBasis:n.array(n.string()).optional()}),lt={name:"estimator_draft_remodel_deliverable",description:"Draft a sendable residential remodel deliverable from scope notes in a single call, following the Matthew v3 client-output section contract when scopeBasis/next steps are provided. Provide one fullScope.scopes list (each scope is a key like 'kitchen', 'bath_primary', 'drywall', or a custom slug, plus optional budgetLump/notes \u2014 the built-in catalog supplies label/unit/default rate). Budget package can be (a) explicit via budgetScope.scopes, (b) derived from full via budgetScope.derivedFromFull.omitFromFullKeys and/or replaceWithAllowanceLump, or (c) omitted to mirror full as collapsed allowances. Returns RemodelDeliverablePackage with clientMarkdown, clientHtml, handoff metadata, and reviewMarkdown (agent-facing, includes MCP guidance). Does not persist. Use for fresh remodel intakes instead of estimator_build_remodel_deliverable when you have not yet authored both intakes by hand.",inputSchemaZod:on,execute:async e=>{if(!a())return s();let t=await u("/estimator/estimates/draft-remodel-deliverable",e);return JSON.stringify(t)}};import{z as K}from"zod";var an=K.object({estimateId:K.string(),recipientEmail:K.string().email().optional(),recipientName:K.string().max(200).optional(),senderNote:K.string().max(2e3).optional(),expiresInDays:K.number().int().min(1).max(365).optional()}),pt={name:"estimator_create_signing_request",description:"Create a contract signing request for an estimate. Returns a tokenized signing URL that can be shared with the client. The client can review and electronically sign the estimate at that URL. The signing request expires after expiresInDays (default 30). Does not send emails during implementation.",inputSchemaZod:an,execute:async e=>{if(!a())return s();let{estimateId:t,...o}=e,i=await u(`/estimator/estimates/${t}/signing-requests`,o);return JSON.stringify(i)}},sn=K.object({estimateId:K.string()}),mt={name:"estimator_get_signing_requests",description:"List all signing requests for an estimate. Returns status (pending/signed/expired/cancelled), signing URL, recipient, and audit metadata for each request.",inputSchemaZod:sn,execute:async e=>{if(!a())return s();let t=await g(`/estimator/estimates/${e.estimateId}/signing-requests`);return JSON.stringify(t)}},rn=K.object({estimateId:K.string(),requestId:K.string()}),dt={name:"estimator_cancel_signing_request",description:"Cancel a pending signing request. Cannot cancel a request that has already been signed. The signing URL will become invalid immediately.",inputSchemaZod:rn,execute:async e=>{if(!a())return s();let{estimateId:t,requestId:o}=e,i=await P(`/estimator/estimates/${t}/signing-requests`,{requestId:o});return JSON.stringify(i)}};import{existsSync as ln,readFileSync as pn}from"node:fs";import{dirname as xt,join as Ft}from"node:path";import{fileURLToPath as Dt}from"node:url";function mn(e){let t=xt(Dt(e));for(let o=0;o<6;o+=1){if(ln(Ft(t,"package.json")))return t;let i=xt(t);if(i===t)break;t=i}throw new Error(`package.json not found above ${xt(Dt(e))}`)}function dn(e,t){let o=Ft(mn(e),"package.json"),m=JSON.parse(pn(o,"utf8"))[t];if(typeof m!="string"||!m.trim())throw new Error(`Missing package.json ${t} at ${o}`);return m}function Vt(e=import.meta.url){return dn(e,"version")}var fn=Vt(import.meta.url);function Bt(e,t=!1){return{content:[{type:"text",text:e}],...t?{isError:!0}:{}}}function Yt(){let e=new un({name:"kynver-mcp-estimator",version:fn},{capabilities:{tools:{}}});function t(o,i,m){e.registerTool(o,i,async S=>{try{return Bt(await m(S))}catch(h){let N=h instanceof z?h.message:L(h)?J(h):h instanceof Error?h.message:"The estimate service could not be reached. The save was not confirmed.";return Bt(N,!0)}})}return t(Y.name,{description:Y.description,inputSchema:Y.inputSchemaZod},o=>Y.execute(o)),t(H.name,{description:H.description,inputSchema:H.inputSchemaZod},o=>H.execute(o)),t(G.name,{description:G.description,inputSchema:G.inputSchemaZod},o=>G.execute(o)),t(X.name,{description:X.description,inputSchema:X.inputSchemaZod},o=>X.execute(o)),t(W.name,{description:W.description,inputSchema:W.inputSchemaZod},o=>W.execute(o)),t(Q.name,{description:Q.description,inputSchema:Q.inputSchemaZod},o=>Q.execute(o)),t(ee.name,{description:ee.description,inputSchema:ee.inputSchemaZod},o=>ee.execute(o)),t(te.name,{description:te.description,inputSchema:te.inputSchemaZod},o=>te.execute(o)),t(oe.name,{description:oe.description,inputSchema:oe.inputSchemaZod},o=>oe.execute(o)),t(ie.name,{description:ie.description,inputSchema:ie.inputSchemaZod},o=>ie.execute(o)),t(ne.name,{description:ne.description,inputSchema:ne.inputSchemaZod},o=>ne.execute(o)),t(ae.name,{description:ae.description,inputSchema:ae.inputSchemaZod},o=>ae.execute(o)),t(se.name,{description:se.description,inputSchema:se.inputSchemaZod},o=>se.execute(o)),t(re.name,{description:re.description,inputSchema:re.inputSchemaZod},o=>re.execute(o)),t(ce.name,{description:ce.description,inputSchema:ce.inputSchemaZod},o=>ce.execute(o)),t(le.name,{description:le.description,inputSchema:le.inputSchemaZod},o=>le.execute(o)),t(pe.name,{description:pe.description,inputSchema:pe.inputSchemaZod},o=>pe.execute(o)),t(me.name,{description:me.description,inputSchema:me.inputSchemaZod},o=>me.execute(o)),t(de.name,{description:de.description,inputSchema:de.inputSchemaZod},o=>de.execute(o)),t(ue.name,{description:ue.description,inputSchema:ue.inputSchemaZod},o=>ue.execute(o)),t(fe.name,{description:fe.description,inputSchema:fe.inputSchemaZod},o=>fe.execute(o)),t(ge.name,{description:ge.description,inputSchema:ge.inputSchemaZod},o=>ge.execute(o)),t(he.name,{description:he.description,inputSchema:he.inputSchemaZod},o=>he.execute(o)),t(ye.name,{description:ye.description,inputSchema:ye.inputSchemaZod},o=>ye.execute(o)),t(rt.name,{description:rt.description,inputSchema:rt.inputSchemaZod},o=>rt.execute(o)),t(ct.name,{description:ct.description,inputSchema:ct.inputSchemaZod},o=>ct.execute(o)),t(lt.name,{description:lt.description,inputSchema:lt.inputSchemaZod},o=>lt.execute(o)),t(Se.name,{description:Se.description,inputSchema:Se.inputSchemaZod},o=>Se.execute(o)),t(Te.name,{description:Te.description,inputSchema:Te.inputSchemaZod},o=>Te.execute(o)),t(be.name,{description:be.description,inputSchema:be.inputSchemaZod},o=>be.execute(o)),t(ve.name,{description:ve.description,inputSchema:ve.inputSchemaZod},o=>ve.execute(o)),t(ke.name,{description:ke.description,inputSchema:ke.inputSchemaZod},o=>ke.execute(o)),t(Pe.name,{description:Pe.description,inputSchema:Pe.inputSchemaZod},o=>Pe.execute(o)),t(Ie.name,{description:Ie.description,inputSchema:Ie.inputSchemaZod},o=>Ie.execute(o)),t(xe.name,{description:xe.description,inputSchema:xe.inputSchemaZod},o=>xe.execute(o)),t(_e.name,{description:_e.description,inputSchema:_e.inputSchemaZod},o=>_e.execute(o)),t(Ae.name,{description:Ae.description,inputSchema:Ae.inputSchemaZod},o=>Ae.execute(o)),t(Ce.name,{description:Ce.description,inputSchema:Ce.inputSchemaZod},o=>Ce.execute(o)),t(Ee.name,{description:Ee.description,inputSchema:Ee.inputSchemaZod},o=>Ee.execute(o)),t(Re.name,{description:Re.description,inputSchema:Re.inputSchemaZod},o=>Re.execute(o)),t(je.name,{description:je.description,inputSchema:je.inputSchemaZod},o=>je.execute(o)),t(Ke.name,{description:Ke.description,inputSchema:Ke.inputSchemaZod},o=>Ke.execute(o)),t(Ne.name,{description:Ne.description,inputSchema:Ne.inputSchemaZod},o=>Ne.execute(o)),t(Me.name,{description:Me.description,inputSchema:Me.inputSchemaZod},o=>Me.execute(o)),t(Oe.name,{description:Oe.description,inputSchema:Oe.inputSchemaZod},o=>Oe.execute(o)),t(Ze.name,{description:Ze.description,inputSchema:Ze.inputSchemaZod},o=>Ze.execute(o)),t(ze.name,{description:ze.description,inputSchema:ze.inputSchemaZod},o=>ze.execute(o)),t(Le.name,{description:Le.description,inputSchema:Le.inputSchemaZod},o=>Le.execute(o)),t(Ue.name,{description:Ue.description,inputSchema:Ue.inputSchemaZod},o=>Ue.execute(o)),t(qe.name,{description:qe.description,inputSchema:qe.inputSchemaZod},o=>qe.execute(o)),t($e.name,{description:$e.description,inputSchema:$e.inputSchemaZod},o=>$e.execute(o)),t(Je.name,{description:Je.description,inputSchema:Je.inputSchemaZod},o=>Je.execute(o)),t(De.name,{description:De.description,inputSchema:De.inputSchemaZod},o=>De.execute(o)),t(Fe.name,{description:Fe.description,inputSchema:Fe.inputSchemaZod},o=>Fe.execute(o)),t(Ve.name,{description:Ve.description,inputSchema:Ve.inputSchemaZod},o=>Ve.execute(o)),t(Be.name,{description:Be.description,inputSchema:Be.inputSchemaZod},o=>Be.execute(o)),t(Ye.name,{description:Ye.description,inputSchema:Ye.inputSchemaZod},o=>Ye.execute(o)),t(He.name,{description:He.description,inputSchema:He.inputSchemaZod},o=>He.execute(o)),t(Ge.name,{description:Ge.description,inputSchema:Ge.inputSchemaZod},o=>Ge.execute(o)),t(Xe.name,{description:Xe.description,inputSchema:Xe.inputSchemaZod},o=>Xe.execute(o)),t(We.name,{description:We.description,inputSchema:We.inputSchemaZod},o=>We.execute(o)),t(Qe.name,{description:Qe.description,inputSchema:Qe.inputSchemaZod},o=>Qe.execute(o)),t(et.name,{description:et.description,inputSchema:et.inputSchemaZod},o=>et.execute(o)),t(tt.name,{description:tt.description,inputSchema:tt.inputSchemaZod},o=>tt.execute(o)),t(ot.name,{description:ot.description,inputSchema:ot.inputSchemaZod},o=>ot.execute(o)),t(it.name,{description:it.description,inputSchema:it.inputSchemaZod},o=>it.execute(o)),t(nt.name,{description:nt.description,inputSchema:nt.inputSchemaZod},o=>nt.execute(o)),t(at.name,{description:at.description,inputSchema:at.inputSchemaZod},o=>at.execute(o)),t(st.name,{description:st.description,inputSchema:st.inputSchemaZod},o=>st.execute(o)),t(pt.name,{description:pt.description,inputSchema:pt.inputSchemaZod},o=>pt.execute(o)),t(mt.name,{description:mt.description,inputSchema:mt.inputSchemaZod},o=>mt.execute(o)),t(dt.name,{description:dt.description,inputSchema:dt.inputSchemaZod},o=>dt.execute(o)),e}Mt({label:"Estimator server",defaultPort:3104,createMcpServer:Yt,requestContext:Pt});
1
+ import{createServer as Ht}from"node:http";import{randomUUID as Xt,timingSafeEqual as Wt}from"node:crypto";import{SSEServerTransport as Qt}from"@modelcontextprotocol/sdk/server/sse.js";import{StreamableHTTPServerTransport as eo}from"@modelcontextprotocol/sdk/server/streamableHttp.js";var to=1*1024*1024,oo=["apiKey","secret","userId"],io=new Set(["1","true","yes","on"]);function no(){let e=process.env.KYNVER_ALLOW_SERVICE_SECRET_IMPERSONATION?.trim().toLowerCase();return e!==void 0&&io.has(e)?!1:process.env.NODE_ENV==="production"||process.env.VERCEL_ENV==="production"}function B(e,t){let o=e.headers[t];return typeof o=="string"?o:Array.isArray(o)?o[0]??null:null}function Nt(e,t){let o=Buffer.from(e),i=Buffer.from(t);return o.length!==i.length?!1:Wt(o,i)}function Et(e){return oo.some(t=>e.searchParams.has(t))}function ao(){return process.env.MCP_SSE_HOST?.trim()||"127.0.0.1"}function yt(e,t){let o=process.env[e];if(!o)return t;let i=parseInt(o,10);return Number.isFinite(i)&&i>0?i:t}function so(){let e=process.env.KYNVER_API_URL?.trim();if(!e)return"https://kynver.com";let t;try{t=new URL(e)}catch{throw new Error(`Invalid KYNVER_API_URL: ${e}`)}let o=t.hostname==="localhost"||t.hostname==="127.0.0.1";if(t.protocol!=="https:"&&!(t.protocol==="http:"&&o))throw new Error(`KYNVER_API_URL must be https (got "${t.protocol}//${t.hostname}"); API keys are sent to this origin.`);return e.replace(/\/$/,"")}function Rt(e){let t=B(e,"x-forwarded-for");return t?t.split(",")[0].trim():e.socket?.remoteAddress??"unknown"}function ro(e,t){let o=new Map;return{check(i){let p=Date.now(),S=o.get(i);return!S||p>=S.resetAt?(o.set(i,{count:1,resetAt:p+t}),!0):S.count>=e?!1:(S.count+=1,!0)}}}function bt(e,t=to){return new Promise((o,i)=>{let p=[],S=0;e.on("data",g=>{if(S+=g.length,S>t){e.destroy(),i(new Error("Request body too large"));return}p.push(g)}),e.on("end",()=>{try{o(JSON.parse(Buffer.concat(p).toString()))}catch(g){i(g)}}),e.on("error",i)})}function vt(e){let t=B(e,"authorization");return t?/^Bearer\s+(.+)$/i.exec(t)?.[1]?.trim()??"":""}function wt(e,t){let o=B(e,"x-kynver-user-id"),i=B(e,"x-kynver-service-secret");return!o||!t||!i||no()||!Nt(i,t)?null:{userId:o,auth:{kind:"internal",userId:o}}}function jt(e,t,o,i){if(o.kind==="apiKey"){let S=vt(e);return S.length>0&&Nt(S,o.apiKey)}let p=wt(e,i);return p!==null&&p.userId===o.userId&&p.auth.kind==="internal"}async function Kt(e,t){try{let o=await fetch(`${e.replace(/\/$/,"")}/api/users/me`,{headers:{Authorization:`Bearer ${t}`}});return o.ok?(await o.json()).id??null:null}catch{return null}}function co(e,t){return new URL(e.url??"/",`http://localhost:${t}`)}function b(e,t,o){e.writeHead(t,{"Content-Type":"application/json"}),e.end(JSON.stringify(o))}function Mt(e){let t=parseInt(process.env.PORT??String(e.defaultPort),10),o=so(),i=process.env.KYNVER_SERVICE_SECRET??"",p=ao(),S={},g={},R=yt("MCP_SSE_MAX_SESSIONS",200),ut=yt("MCP_SSE_MAX_SESSIONS_PER_USER",20),q=yt("MCP_SSE_IDLE_TIMEOUT_MS",300*1e3),M=yt("MCP_SSE_RATE_LIMIT_PER_MIN",120),ft=ro(M,60*1e3);setInterval(()=>{let y=Date.now();for(let[f,C]of Object.entries(S))y-C.lastActivity>q&&(delete S[f],C.transport.close().catch(()=>{}));for(let[f,C]of Object.entries(g))y-C.lastActivity>q&&(delete g[f],C.transport.close().catch(()=>{}))},60*1e3).unref?.();let _t=()=>Object.keys(S).length+Object.keys(g).length,At=y=>Object.values(S).filter(f=>f.userId===y).length+Object.values(g).filter(f=>f.userId===y).length,Ct=Ht(async(y,f)=>{let C=co(y,t),gt=C.pathname;if(y.method==="GET"&&gt==="/sse"){if(!ft.check(`sse:${Rt(y)}`)){b(f,429,{error:"Too many requests. Slow down and retry."});return}if(Et(C)){b(f,400,{error:"Internal auth via query params is not allowed. Use X-Kynver-User-Id and X-Kynver-Service-Secret headers."});return}let _=null,v=null,A=i?wt(y,i):null;if(A)_=A.userId,v=A.auth;else{let $=vt(y);$&&(_=await Kt(o,$),_&&(v={kind:"apiKey",apiKey:$}))}if(!_||!v){b(f,401,{error:"Unauthorized. Pass API key via Authorization: Bearer <key> or internal X-Kynver-User-Id + X-Kynver-Service-Secret headers."});return}if(_t()>=R){b(f,503,{error:"Server at capacity. Try again shortly."});return}if(At(_)>=ut){b(f,429,{error:"Too many concurrent connections for this account."});return}console.log(`[MCP SSE] New connection (${e.label}, user: ${_.slice(0,8)}...)`);let Z=new Qt("/messages",f);S[Z.sessionId]={transport:Z,userId:_,auth:v,lastActivity:Date.now()},f.on("close",()=>{delete S[Z.sessionId]}),await(await e.createMcpServer()).connect(Z);return}if(y.method==="POST"&&gt==="/messages"){let _=C.searchParams.get("sessionId"),v=_?S[_]:void 0;if(!v){b(f,400,{error:"No active SSE session for this sessionId"});return}if(!jt(y,C,v.auth,i)){b(f,401,{error:"Unauthorized. Re-send the same credentials used to open the SSE session."});return}if(!ft.check(`msg:${v.userId}`)){b(f,429,{error:"Too many requests. Slow down and retry."});return}v.lastActivity=Date.now();try{let A=await bt(y);await e.requestContext.run({userId:v.userId,authKind:v.auth.kind,sessionCredential:v.auth.kind==="apiKey"?v.auth.apiKey:void 0},async()=>{await v.transport.handlePostMessage(y,f,A)})}catch(A){b(f,400,{error:A instanceof Error?A.message:"Invalid request body"})}return}if(gt==="/mcp"){if(y.method!=="POST"&&y.method!=="GET"&&y.method!=="DELETE"){f.writeHead(405,{"Content-Type":"application/json",Allow:"GET, POST, DELETE"}),f.end(JSON.stringify({error:"Method not allowed"}));return}if(Et(C)){b(f,400,{error:"Internal auth via query params is not allowed. Use X-Kynver-User-Id and X-Kynver-Service-Secret headers."});return}let _=B(y,"mcp-session-id");if(_){let I=g[_];if(!I){b(f,404,{error:"Unknown or expired MCP session. Re-initialize."});return}if(!jt(y,C,I.auth,i)){b(f,401,{error:"Unauthorized. Re-send the same credentials used to initialize the MCP session."});return}if(!ft.check(`msg:${I.userId}`)){b(f,429,{error:"Too many requests. Slow down and retry."});return}I.lastActivity=Date.now();try{let ht=y.method==="POST"?await bt(y):void 0;await e.requestContext.run({userId:I.userId},async()=>{await I.transport.handleRequest(y,f,ht)})}catch(ht){f.headersSent||b(f,400,{error:ht instanceof Error?ht.message:"Invalid request body"})}return}if(y.method!=="POST"){b(f,400,{error:"Missing mcp-session-id header. Open a session with an initialize POST first."});return}if(!ft.check(`mcp:${Rt(y)}`)){b(f,429,{error:"Too many requests. Slow down and retry."});return}let v=null,A=null,Z=i?wt(y,i):null;if(Z)v=Z.userId,A=Z.auth;else{let I=vt(y);I&&(v=await Kt(o,I),v&&(A={kind:"apiKey",apiKey:I}))}if(!v||!A){b(f,401,{error:"Unauthorized. Pass API key via Authorization: Bearer <key> or internal X-Kynver-User-Id + X-Kynver-Service-Secret headers."});return}if(_t()>=R){b(f,503,{error:"Server at capacity. Try again shortly."});return}if(At(v)>=ut){b(f,429,{error:"Too many concurrent connections for this account."});return}let Tt;try{Tt=await bt(y)}catch(I){b(f,400,{error:I instanceof Error?I.message:"Invalid request body"});return}let $=v,Gt=A,F=new eo({sessionIdGenerator:()=>Xt(),onsessioninitialized:I=>{g[I]={transport:F,userId:$,auth:Gt,lastActivity:Date.now()},console.log(`[MCP HTTP] New session (${e.label}, user: ${$.slice(0,8)}...)`)}});F.onclose=()=>{F.sessionId&&delete g[F.sessionId]},await(await e.createMcpServer()).connect(F);try{await e.requestContext.run({userId:$},async()=>{await F.handleRequest(y,f,Tt)})}catch(I){f.headersSent||b(f,400,{error:I instanceof Error?I.message:"Invalid request"})}return}if(y.method==="GET"&&gt==="/health"){f.writeHead(200,{"Content-Type":"text/plain"}),f.end("ok");return}b(f,404,{error:"Not found"})});return Ct.listen(t,p,()=>{console.log(`[MCP SSE] ${e.label} listening on ${p}:${t}`)}),Ct}import{McpServer as un}from"@modelcontextprotocol/sdk/server/mcp.js";import{z as m}from"zod";import{AsyncLocalStorage as po}from"node:async_hooks";var z=class extends Error{constructor(t){super(t),this.name="EstimatorApiError"}};function lo(e){if(!e||typeof e!="object")return"";let t=e.name;return typeof t=="string"?t:""}function Ot(e){if(!(!e||typeof e!="object"))return e.cause}function V(e){let t=lo(e);if(t==="TimeoutError"||t==="AbortError")return!0;let o=Ot(e);return o?V(o):!1}function L(e){let t=e instanceof Error?e.message:String(e);if(/MCP error -32000\b/i.test(t)||/connection closed/i.test(t)||/fetch failed|ECONNRESET|ECONNREFUSED|ENOTFOUND|socket hang up|network error|timed out fetching a new connection|P2024|connection pool/i.test(t))return!0;let o=Ot(e);return o?L(o):!1}function D(e,t="save"){return V(e)?t==="read"?"The estimate service did not respond in time. Nothing was loaded.":"The estimate service did not respond in time. The save was not confirmed.":t==="read"?"The estimate service could not be reached. Nothing was loaded.":"The estimate service could not be reached. The save was not confirmed."}var Zt=2e4,It=new po,mo=process.env.KYNVER_API_URL??"https://kynver.com",kt=process.env.KYNVER_API_KEY??"";function uo(e){let t=e.startsWith("/")?e:`/${e}`;if(t.includes("..")||t.includes("\0"))throw new Error("Invalid API path: path traversal is not allowed")}function fo(e){uo(e);let t=e.startsWith("/")?e:`/${e}`,o=t.startsWith("/api")?t:`/api${t}`;return`${mo.replace(/\/$/,"")}${o}`}function go(){let e={"Content-Type":"application/json"};kt&&(e.Authorization=`Bearer ${kt}`);let t=It.getStore(),o=t?.userId??process.env.KYNVER_USER_ID?.trim(),i=process.env.KYNVER_SERVICE_SECRET?.trim();if(o){if(t?.sessionCredential)return t.authKind==="runnerToken"?(delete e.Authorization,e["X-Kynver-Runner-Token"]=t.sessionCredential):e.Authorization=`Bearer ${t.sessionCredential}`,e;let p=process.env.KYNVER_RUNNER_TOKEN?.trim();if(p){if(t?.authKind==="apiKey"||t?.authKind==="runnerToken")throw new Error("User-authenticated session did not propagate its credential; refusing the deployment-wide KYNVER_RUNNER_TOKEN (cross-tenant risk).");if(t?.authKind!=="internal")return e["X-Kynver-Runner-Token"]=p,e}if(!i)throw new Error("User context requires authentication: set KYNVER_RUNNER_TOKEN (preferred) or KYNVER_SERVICE_SECRET in the MCP server environment.");e["X-Kynver-User-Id"]=o,e["X-Kynver-Service-Secret"]=i}return e}async function ho(e){let t=await e.text();if(!e.ok){let o=t;try{let i=JSON.parse(t);o=i.error??i.message??t}catch{}throw new Error(o||`HTTP ${e.status}`)}if(t)try{return JSON.parse(t)}catch{return t}}async function St(e,t,o,i="save",p=Zt){let S;try{S=await fetch(fo(t),{method:e,headers:go(),...o!==void 0?{body:JSON.stringify(o)}:{},signal:AbortSignal.timeout(p)})}catch(g){throw g instanceof z?g:L(g)||V(g)?new z(D(g,i)):g}try{return await ho(S)}catch(g){throw L(g)?new z(D(g,i)):g}}async function h(e){return St("GET",e,void 0,"read")}async function u(e,t){return St("POST",e,t,"save")}async function P(e,t,o=Zt){return St("PATCH",e,t,"save",o)}async function j(e){return St("DELETE",e,void 0,"save")}function a(){return!!kt}function s(){return"No API key configured. Set KYNVER_API_KEY in your MCP server config."}var yo=m.object({message:m.string()}),Y={name:"estimator_import_catalog",description:"Start a catalog import from a file. This is a placeholder \u2014 MCP cannot do file uploads. Describe the import request in the message field and the system will guide the user through the upload flow.",inputSchemaZod:yo,execute:async e=>{if(!a())return s();let t=await u("/v1/agents/kynver-estimator/chat",{message:e.message});return JSON.stringify(t)}},So=m.object({importId:m.string(),approved:m.boolean().optional(),changes:m.array(m.object({index:m.number(),action:m.enum(["keep","skip","edit"]),edits:m.record(m.string(),m.unknown()).optional()})).optional()}),G={name:"estimator_review_import",description:"Review a pending catalog import batch before committing. Shows parsed rows with suggested service codes, categories, and pricing. You can approve all, skip individual rows, or edit values before committing. Requires an importId from a previous import.",inputSchemaZod:So,execute:async e=>{if(!a())return s();let{importId:t,...o}=e,i=await P(`/estimator/import/${t}`,o);return JSON.stringify(i)}},To=m.object({importId:m.string()}),H={name:"estimator_commit_import",description:"Finalize a reviewed catalog import, writing all approved rows into the user's service catalog. Requires an importId that has been reviewed. This action cannot be undone \u2014 rows are added permanently.",inputSchemaZod:To,execute:async e=>{if(!a())return s();let t=await u(`/estimator/import/${e.importId}/commit`,{});return JSON.stringify(t)}},bo=m.object({name:m.string(),serviceCode:m.string(),category:m.string(),materialType:m.string().nullable().optional(),unit:m.string(),materialCost:m.number().min(0),laborCost:m.number().min(0),minCharge:m.number().min(0).optional(),notes:m.string().optional()}),X={name:"estimator_setup_price",description:"Add a new service to the user's price book (service catalog). Every service needs: a unique serviceCode (e.g., CABINET_INSTALL, PAINT_WALLS, ROOF_INSPECT), a display name, category (general construction: demolition, framing, plumbing, electrical, hvac, drywall, paint, flooring, cabinetry, countertops, tile, windows, doors, insulation, appliances, general_conditions; roofing: tear_off, decking, underlayment, field_install, ridge_hip, flashing, misc, and others), unit (sq = one roofing square of 100 sq ft, sq_ft, linear_ft, each, sheet, roll, bundle, gallon, hour, lump; per-square roofing work is always sq), and material/labor costs. Items in the catalog are used when creating estimates.",inputSchemaZod:bo,execute:async e=>{if(!a())return s();let t=await u("/estimator/catalog",e);return JSON.stringify(t)}},vo=m.object({itemId:m.string(),name:m.string().optional(),serviceCode:m.string().optional(),category:m.string().optional(),materialType:m.string().nullable().optional(),unit:m.string().optional(),materialCost:m.number().min(0).optional(),laborCost:m.number().min(0).optional(),minCharge:m.number().min(0).optional(),notes:m.string().optional()}),W={name:"estimator_update_price",description:"Update an existing item in the service catalog. Use estimator_get_catalog first to find the itemId. You can change the name, service code, category, unit, material cost, labor cost, minimum charge, or notes.",inputSchemaZod:vo,execute:async e=>{if(!a())return s();let{itemId:t,...o}=e,i=await P(`/estimator/catalog/${t}`,o);return JSON.stringify(i)}},wo=m.object({category:m.string().optional(),materialType:m.string().optional(),limit:m.number().optional(),cursor:m.string().optional()}),Q={name:"estimator_get_catalog",description:"List all items in the user's service catalog (price book). Returns service codes, names, categories, units, and material/labor costs. IMPORTANT: Always call this before creating estimates to get valid service codes. Optional filters must match enum strings. Categories include general construction (demolition, framing, drywall, paint, flooring, cabinetry, tile, electrical, plumbing, general_conditions, \u2026) and roofing (tear_off, decking, underlayment, field_install, ridge_hip, flashing, misc, \u2026). Materials include general construction (drywall, interior_paint, hardwood, ceramic_tile, stock_cabinet, quartz, \u2026) and roofing (asphalt_architectural, asphalt_3tab, metal_standing_seam, tpo, \u2026). When unsure, omit both filters to fetch the full catalog.",inputSchemaZod:wo,execute:async e=>{if(!a())return s();let t=new URLSearchParams;e.category&&t.set("category",e.category),e.materialType&&t.set("materialType",e.materialType),e.limit!=null&&t.set("limit",String(e.limit)),e.cursor&&t.set("cursor",e.cursor);let o=t.toString(),i=o?`/estimator/catalog?${o}`:"/estimator/catalog",p=await h(i);return JSON.stringify(p)}},ko=m.object({}),ee={name:"estimator_get_config",description:"Get the user's estimator settings \u2014 company name, default markup/waste percentage, tax rate, currency, contact info, license number, and default notes/terms for estimates.",inputSchemaZod:ko,execute:async e=>{if(!a())return s();let t=await h("/estimator/config");return JSON.stringify(t)}},Io=m.object({defaultMarkup:m.number().optional(),taxRate:m.number().optional(),companyName:m.string().optional(),companyPhone:m.string().optional(),companyEmail:m.string().optional(),companyAddress:m.string().optional(),companyLogo:m.string().optional(),licenseNumber:m.string().optional(),defaultTerms:m.string().optional()}),te={name:"estimator_update_config",description:"Update the user's estimator settings. You can set: companyName, defaultMarkup (waste %), taxRate, companyPhone, companyEmail, companyAddress, companyLogo (URL), licenseNumber (contractor CCB/license number), and defaultTerms (terms & conditions appended to estimates).",inputSchemaZod:Io,execute:async e=>{if(!a())return s();let t=await P("/estimator/config",e);return JSON.stringify(t)}},Po=m.object({templateId:m.string().optional()}),oe={name:"estimator_import_template",description:"Import a pre-built pricing catalog template into the user's service catalog. Call without arguments to see available templates (returns id, name, description, item count). Call with a templateId to import all items. general-construction-2026 is the general-construction starter (demolition, framing, drywall, paint, flooring, cabinetry, and the other trades). dealers-supply-2025 is optional roofing material pricing. Existing rows are not deleted. A legacy Misc row with no material for the same service code is moved onto the template category and material; prices, names, and notes stay. Other existing rows are skipped.",inputSchemaZod:Po,execute:async e=>{if(!a())return s();if(!e.templateId){let o=await h("/estimator/catalog/templates");return JSON.stringify(o)}let t=await u("/estimator/catalog/templates",{templateId:e.templateId});return JSON.stringify(t)}};import{z as E}from"zod";var _o=E.object({}),ie={name:"estimator_list_price_catalogs",description:"List durable builtin price catalogs shipped with the estimator (region, effective date, markup policy). These are the MCP's stored price lists \u2014 not AgentOS memory. Use before seeding or resolving remodel defaults.",inputSchemaZod:_o,execute:async e=>{if(!a())return s();let t=await h("/estimator/price-catalog");return JSON.stringify(t)}},Ao=E.object({catalogId:E.string()}),ne={name:"estimator_get_price_catalog",description:"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.",inputSchemaZod:Ao,execute:async e=>{if(!a())return s();let t=await h(`/estimator/price-catalog/${encodeURIComponent(e.catalogId)}`);return JSON.stringify(t)}},Co=E.object({serviceCode:E.string(),materialType:E.string().nullable().optional(),region:E.string().optional(),overrideMaterialCost:E.number().min(0).optional(),overrideLaborCost:E.number().min(0).optional()}),ae={name:"estimator_resolve_price",description:"Resolve unit pricing for one service code with provenance: user price book \u2192 builtin catalog \u2192 optional intake override (overrideMaterialCost/overrideLaborCost). Returns source, region, effective date, and markupIncluded flag.",inputSchemaZod:Co,execute:async e=>{if(!a())return s();let t=new URLSearchParams({serviceCode:e.serviceCode});e.materialType&&t.set("materialType",e.materialType),e.region&&t.set("region",e.region),e.overrideMaterialCost!=null&&t.set("overrideMaterialCost",String(e.overrideMaterialCost)),e.overrideLaborCost!=null&&t.set("overrideLaborCost",String(e.overrideLaborCost));let o=await h(`/estimator/price-catalog/resolve?${t.toString()}`);return JSON.stringify(o)}},Eo=E.object({catalogId:E.string(),overwrite:E.boolean().optional()}),se={name:"estimator_seed_price_catalog",description:"Copy a builtin price catalog into the user's price book. Missing codes are inserted. A legacy Misc row with no material for that code is reclassified; prices stay unless overwrite=true. Rows are never deleted. Use portland-remodel-will-2026-06 for the Portland general-construction working rates.",inputSchemaZod:Eo,execute:async e=>{if(!a())return s();let t=await u("/estimator/price-catalog/seed",e);return JSON.stringify(t)}};import{z as r}from"zod";var jo=new Set(["draft","final_status","sent","accepted","declined"]),Ko=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,No=2e3,Mo=8e3,Oo="projectName, status, clientId, companyId, siteAddress, notes (client-facing), internalNotes (contractor-only internal note)";function zt(e){if(e!==void 0)return e===null?null:e}function Lt(e,t,o){if(e!==void 0){if(e===null||e==="")return null;if(!Ko.test(e)){o.push(`${t} was not a valid id, so it was left unchanged.`);return}return e}}function Zo(e){let t=typeof e.estimateId=="string"?e.estimateId.trim():"";if(!t)return{ok:!1,error:"estimateId is required to update an estimate."};let o=[],i={},p=[];if(e.projectName!==void 0&&e.projectName!==null){let M=e.projectName.trim();if(!M)o.push("projectName was empty, so it was left unchanged.");else{if(M.length>200)return{ok:!1,error:"projectName is too long to save."};i.projectName=M,p.push("projectName")}}if(e.status!==void 0&&e.status!==null&&e.status!==""){let M=e.status.trim().toLowerCase();jo.has(M)?(i.status=M,p.push("status")):o.push(`status "${e.status}" is not a draft, sent, accepted, or declined value, so it was left unchanged.`)}let S=Lt(e.clientId,"clientId",o);S!==void 0&&(i.clientId=S,p.push("clientId"));let g=Lt(e.companyId,"companyId",o);if(g!==void 0&&(i.companyId=g,p.push("companyId")),e.siteAddress!==void 0){if(e.siteAddress!==null&&e.siteAddress.length>500)return{ok:!1,error:"siteAddress is too long to save."};i.siteAddress=e.siteAddress,p.push("siteAddress")}let R=zt(e.notes);if(R!==void 0){if(R!==null&&R.length>No)return{ok:!1,error:"The client-facing note is too long to save."};i.notes=R,p.push("notes")}let ut=e.internalNotes!==void 0?e.internalNotes:e.internalNote,q=zt(ut);if(q!==void 0){if(q!==null&&q.length>Mo)return{ok:!1,error:"The internal note is too long to save."};i.internalNotes=q,p.push("internalNotes")}return p.length===0?{ok:!1,error:`No estimate fields to update.${o.length?` ${o.join(" ")}`:""} Send estimateId and at least one of: ${Oo}.`}:{ok:!0,estimateId:t,body:i,changed:p,warnings:o}}function zo(e,t,o){let i=e&&typeof e=="object"?e:{};return JSON.stringify({ok:!0,id:typeof i.id=="string"?i.id:null,projectName:i.projectName??null,status:i.status??null,notes:i.notes??null,internalNotes:i.internalNotes??null,changed:t,...o.length?{warnings:o}:{}})}function Lo(e){return e instanceof Error&&e.name==="EstimatorApiError"?e.message:V(e)||L(e)?D(e):(e instanceof Error?e.message:String(e)).replace(/MCP error\s*-32000:?/gi,"").replace(/connection closed/gi,"the save did not finish").trim()||"The estimate update did not finish. The save was not confirmed."}async function Ut(e,t){let o=Zo(e);if(!o.ok)return JSON.stringify({ok:!1,error:o.error});try{let i=await t(`/estimator/estimates/${o.estimateId}`,o.body);return zo(i,o.changed,o.warnings)}catch(i){return JSON.stringify({ok:!1,error:Lo(i)})}}var Uo=r.object({projectName:r.string(),roofMaterial:r.string(),footprintSqFt:r.number().optional(),roofAreaSqFt:r.number().optional(),pitch:r.string().optional(),layers:r.number().optional(),ridgeLengthFt:r.number(),hipLengthFt:r.number().optional(),valleyLengthFt:r.number().optional(),eavesFt:r.number(),rakeFt:r.number().optional(),wastePercent:r.number().optional(),roofSquares:r.number().optional(),pipeBoots:r.number().optional(),chimneys:r.number().optional(),stepFlashingFt:r.number().optional(),deckSheets:r.number().optional(),clientId:r.string().optional(),companyId:r.string().optional(),siteAddress:r.string().optional(),notes:r.string().optional()}),re={name:"estimator_create_replacement",description:`Create a full roof replacement estimate from measurements. The server computes every line from the user's price book: tear-off per layer, haul-off, deck inspection, ice & water, underlayment and shingles with waste, drip edge, starter, ridge cap, ridge vent, valleys, a steep charge at 8/12 and up, plus step flashing, pipe boots, chimney reflash and decking sheets when given, and a dumpster and permit when the book prices them. Requires: projectName, roofMaterial (asphalt_3tab, asphalt_architectural, asphalt_designer, metal_standing_seam, tpo, and the other roof systems), ridgeLengthFt, eavesFt. Give the roof size as roofSquares (squares) or roofAreaSqFt, or footprintSqFt + pitch. Optional: pitch ("7/12"), layers (tear-off layers), rakeFt, hipLengthFt, valleyLengthFt, wastePercent (defaults to the account waste), pipeBoots, chimneys, stepFlashingFt, deckSheets, clientId, siteAddress. Returns the estimate and warnings for any line the price book does not price.`,inputSchemaZod:Uo,execute:async e=>{if(!a())return s();let{projectName:t,clientId:o,companyId:i,siteAddress:p,notes:S,...g}=e,R=await u("/estimator/estimates",{type:"replacement",projectName:t,roofMaterial:g.roofMaterial,inputs:g,clientId:o,companyId:i,siteAddress:p,notes:S});return JSON.stringify(R)}},qo=r.object({serviceCode:r.string(),materialType:r.string().nullable().optional(),quantity:r.number(),notes:r.string().optional()}),$o=r.object({projectName:r.string(),roofMaterial:r.string().optional(),lineItems:r.array(qo),clientId:r.string().optional(),companyId:r.string().optional(),notes:r.string().optional()}),ce={name:"estimator_create_repair",description:"Create a repair/service estimate with specific line items. Each line item needs a serviceCode that exists in the user's catalog \u2014 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.",inputSchemaZod:$o,execute:async e=>{if(!a())return s();let t=await u("/estimator/estimates",{type:"repair_service",projectName:e.projectName,roofMaterial:e.roofMaterial,lineItems:e.lineItems,clientId:e.clientId,companyId:e.companyId,notes:e.notes});return JSON.stringify(t)}},Do=r.object({id:r.string()}),le={name:"estimator_get_estimate",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.",inputSchemaZod:Do,execute:async e=>{if(!a())return s();let t=await h(`/estimator/estimates/${e.id}`);return JSON.stringify(t)}},Jo=r.object({status:r.string().optional(),type:r.string().optional(),clientId:r.string().optional(),companyId:r.string().optional(),from:r.string().optional(),to:r.string().optional(),limit:r.number().optional(),cursor:r.string().optional()}),pe={name:"estimator_list_estimates",description:"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 \u2014 use estimator_get_estimate for full details on a specific one.",inputSchemaZod:Jo,execute:async e=>{if(!a())return s();let t=new URLSearchParams;e.status&&t.set("status",e.status),e.type&&t.set("type",e.type),e.clientId&&t.set("clientId",e.clientId),e.companyId&&t.set("companyId",e.companyId),e.from&&t.set("from",e.from),e.to&&t.set("to",e.to),e.limit!=null&&t.set("limit",String(e.limit)),e.cursor&&t.set("cursor",e.cursor);let o=t.toString(),i=o?`/estimator/estimates?${o}`:"/estimator/estimates",p=await h(i);return JSON.stringify(p)}},Fo=r.object({estimateId:r.string(),serviceCode:r.string(),materialType:r.string().nullable().optional(),quantity:r.number(),notes:r.string().optional()}),me={name:"estimator_add_line_item",description:"Add a line item to an existing estimate. The serviceCode must exist in the user's catalog \u2014 call estimator_get_catalog to verify. Pass: estimateId, serviceCode, quantity. Optionally add notes. The estimate totals are automatically recalculated.",inputSchemaZod:Fo,execute:async e=>{if(!a())return s();let{estimateId:t,...o}=e,i=await u(`/estimator/estimates/${t}/line-items`,o);return JSON.stringify(i)}},Vo=r.object({estimateId:r.string(),lineItemId:r.string()}),de={name:"estimator_remove_line_item",description:"Remove a line item from an estimate. Requires estimateId and lineItemId (get lineItemIds from estimator_get_estimate). The estimate totals are automatically recalculated.",inputSchemaZod:Vo,execute:async e=>{if(!a())return s();let t=await j(`/estimator/estimates/${e.estimateId}/line-items/${e.lineItemId}`);return JSON.stringify(t)}},Bo=r.object({estimateId:r.string(),lineItemId:r.string(),quantity:r.number().optional(),materialCost:r.number().optional(),laborCost:r.number().optional(),notes:r.string().optional()}),ue={name:"estimator_update_line_item",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.",inputSchemaZod:Bo,execute:async e=>{if(!a())return s();let{estimateId:t,lineItemId:o,...i}=e,p=await P(`/estimator/estimates/${t}/line-items/${o}`,i);return JSON.stringify(p)}},Yo=r.object({estimateId:r.string(),targetTotal:r.number(),strategy:r.enum(["uniform","labor_only","material_only"]).optional()}),fe={name:"estimator_adjust_to_target",description:"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 \u2014 scales all items proportionally), 'labor_only' (only adjusts labor costs), 'material_only' (only adjusts material costs). Requires estimateId and targetTotal.",inputSchemaZod:Yo,execute:async e=>{if(!a())return s();let{estimateId:t,...o}=e,i=await u(`/estimator/estimates/${t}/adjust`,o);return JSON.stringify(i)}},Go=r.object({estimateId:r.string(),projectName:r.string().optional(),status:r.string().optional(),clientId:r.string().nullable().optional(),companyId:r.string().nullable().optional(),notes:r.string().nullable().optional(),internalNotes:r.string().nullable().optional(),internalNote:r.string().nullable().optional(),siteAddress:r.string().nullable().optional()}),ge={name:"estimator_update_estimate",description:"Update an estimate's project name, status (draft, sent, accepted, or declined), site address, client or company link, client-facing notes, or the contractor-only internal note. Use internalNotes for an internal note the customer must not see. Use notes for text that can be shown to the customer. internalNote is another name for internalNotes. Does not change line items. Pass estimateId and only the fields to change. A result with ok:false means the save did not stick.",inputSchemaZod:Go,execute:async e=>a()?Ut(e,(t,o)=>P(t,o)):s()},Ho=r.object({estimateId:r.string()}),he={name:"estimator_delete_estimate",description:"Soft-delete an estimate. The estimate data is preserved but hidden from list views. This cannot be easily undone.",inputSchemaZod:Ho,execute:async e=>{if(!a())return s();let t=await j(`/estimator/estimates/${e.estimateId}`);return JSON.stringify(t)}},Xo=r.object({estimateId:r.string(),projectName:r.string().optional()}),ye={name:"estimator_duplicate_estimate",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.",inputSchemaZod:Xo,execute:async e=>{if(!a())return s();let{estimateId:t,...o}=e,i=await u(`/estimator/estimates/${t}/duplicate`,o);return JSON.stringify(i)}};import{z as K}from"zod";var Qo=K.object({estimateId:K.string(),name:K.string()}),Se={name:"estimator_save_template",description:"Save an existing estimate as a reusable template. Templates capture the estimate's line items, quantities, and structure so you can quickly create similar estimates in the future. Requires the estimateId of the source estimate and a name for the template.",inputSchemaZod:Qo,execute:async e=>{if(!a())return s();let t=await u("/estimator/templates",e);return JSON.stringify(t)}},ei=K.object({type:K.string().optional()}),Te={name:"estimator_list_templates",description:"List all saved estimate templates. Optionally filter by type: 'replacement' or 'repair_service'. Returns template names, types, and IDs. Use template IDs with estimator_create_from_template to create new estimates.",inputSchemaZod:ei,execute:async e=>{if(!a())return s();let t=new URLSearchParams;e.type&&t.set("type",e.type);let o=t.toString(),i=o?`/estimator/templates?${o}`:"/estimator/templates",p=await h(i);return JSON.stringify(p)}},ti=K.object({id:K.string()}),be={name:"estimator_delete_template",description:"Delete a saved estimate template permanently. This does not affect any estimates that were previously created from this template.",inputSchemaZod:ti,execute:async e=>{if(!a())return s();let t=await j(`/estimator/templates/${e.id}`);return JSON.stringify(t)}},oi=K.object({templateId:K.string(),projectName:K.string(),clientId:K.string().optional()}),ve={name:"estimator_create_from_template",description:"Create a new estimate from a saved template. The new estimate gets all the template's line items and structure. Requires: templateId (from estimator_list_templates), projectName. Optionally link to a clientId. The new estimate is created as a draft.",inputSchemaZod:oi,execute:async e=>{if(!a())return s();let{templateId:t,...o}=e,i=await u(`/estimator/templates/${encodeURIComponent(t)}/estimates`,o);return JSON.stringify(i)}};import{z as we}from"zod";var ni=we.object({estimateId:we.string()}),ke={name:"estimator_list_versions",description:"List all saved version snapshots of an estimate. Versions are created automatically when significant changes are made. Shows version numbers and timestamps. Use this to track the history of changes to an estimate.",inputSchemaZod:ni,execute:async e=>{if(!a())return s();let t=await h(`/estimator/estimates/${e.estimateId}/versions`);return JSON.stringify(t)}},ai=we.object({estimateId:we.string(),versionId:we.string()}),Ie={name:"estimator_get_version",description:"Get a specific historical version of an estimate. Returns the full estimate snapshot as it existed at that point in time \u2014 all line items, totals, and metadata. Useful for comparing current vs previous versions or reverting changes.",inputSchemaZod:ai,execute:async e=>{if(!a())return s();let t=await h(`/estimator/estimates/${e.estimateId}/versions/${e.versionId}`);return JSON.stringify(t)}};import{z as O}from"zod";var ri=O.object({estimateId:O.string(),imageUrl:O.string(),caption:O.string().optional()}),Pe={name:"estimator_upload_photo",description:"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.",inputSchemaZod:ri,execute:async e=>{if(!a())return s();let{estimateId:t,...o}=e,i=await u(`/estimator/estimates/${t}/photos`,o);return JSON.stringify(i)}},ci=O.object({estimateId:O.string()}),xe={name:"estimator_list_photos",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.",inputSchemaZod:ci,execute:async e=>{if(!a())return s();let t=await h(`/estimator/estimates/${e.estimateId}/photos`);return JSON.stringify(t)}},li=O.object({estimateId:O.string(),photoId:O.string()}),_e={name:"estimator_delete_photo",description:"Remove a photo from an estimate. Requires estimateId and photoId (get IDs from estimator_list_photos).",inputSchemaZod:li,execute:async e=>{if(!a())return s();let t=await j(`/estimator/estimates/${e.estimateId}/photos/${e.photoId}`);return JSON.stringify(t)}};import{z as k}from"zod";var mi=k.object({from:k.string().optional(),to:k.string().optional()}),Ae={name:"estimator_get_stats",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.",inputSchemaZod:mi,execute:async e=>{if(!a())return s();let t=new URLSearchParams;e.from&&t.set("from",e.from),e.to&&t.set("to",e.to);let o=t.toString(),i=o?`/estimator/stats?${o}`:"/estimator/stats",p=await h(i);return JSON.stringify(p)}},di=k.object({estimateId:k.string(),optionEstimateIds:k.array(k.string()).max(4).optional().describe("Good / Better / Best proposal: 2-4 estimate ids to put side by side in one PDF (one estimate per option). estimateId is the first option when it is not in this list."),optionLabels:k.array(k.string()).max(4).optional().describe("Names for the options, in the same order (default Good, Better, Best).")}),Ce={name:"estimator_export_pdf",description:"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 \u2014 use estimator_update_output_config to customize which columns, sections, and details appear. The PDF includes company branding from estimator config. Pass optionEstimateIds for one Good / Better / Best proposal that compares several estimates.",inputSchemaZod:di,execute:async e=>{if(!a())return s();let t=(e.optionEstimateIds??[]).filter(p=>p&&p!==e.estimateId),o=new URLSearchParams;t.length>0&&o.set("options",t.join(",")),t.length>0&&e.optionLabels?.length&&o.set("labels",e.optionLabels.join(","));let i=o.toString()?`?${o.toString()}`:"";try{await h(`/estimator/estimates/${e.estimateId}/pdf${i}`)}catch{}return JSON.stringify({message:t.length>0?"Options PDF generated successfully.":"PDF generated successfully.",downloadUrl:`/api/estimator/estimates/${e.estimateId}/pdf${i}`})}},ui=k.object({estimateId:k.string(),showMaterialCost:k.boolean().optional(),showLaborCost:k.boolean().optional(),showUnitPrice:k.boolean().optional(),showQuantity:k.boolean().optional(),showNotes:k.boolean().optional(),headerText:k.string().optional(),footerText:k.string().optional(),template:k.string().optional()}),Ee={name:"estimator_update_output_config",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 \u2014 the underlying estimate data is not changed.",inputSchemaZod:ui,execute:async e=>{if(!a())return s();let{estimateId:t,...o}=e,i=await P(`/estimator/estimates/${t}/output-config`,o);return JSON.stringify(i)}};import{z as U}from"zod";var gi=U.object({estimateId:U.string(),recipient:U.string().email().optional(),ccRecipients:U.array(U.string().email()).max(10).optional().default([]),subject:U.string().min(1).max(200).optional(),message:U.string().min(1).max(1e4).optional(),confirmExternalRecipient:U.boolean().optional().default(!1)}),Re={name:"estimator_email_pdf",description:"Email an estimate PDF to a primary recipient with optional CC recipients in one group email. Fetches and attaches the PDF server-side (no secrets exposed). Defaults to the account owner's email when recipient is omitted. Estimate client emails and linked client records are trusted without extra confirmation; any external To or CC address requires confirmExternalRecipient=true after explicit user confirmation. Respects the estimate approval gate before customer send. Returns send status and audit metadata.",inputSchemaZod:gi,execute:async e=>{if(!a())return s();let{estimateId:t,...o}=e,i=await u(`/estimator/estimates/${t}/email-pdf`,o);return JSON.stringify(i)}};import{z as J}from"zod";var yi=J.object({images:J.array(J.string()),prompt:J.string().optional()}),je={name:"estimator_analyze_photos",description:"Analyze roof photos using Claude's vision AI. Provide image URLs (can be multiple). The AI identifies: roofing materials, visible damage (missing shingles, cracks, moss, ponding), approximate measurements, and repair recommendations. Use the optional prompt parameter to ask specific questions about the photos. Results can inform estimate creation.",inputSchemaZod:yi,execute:async e=>{if(!a())return s();let t=await u("/estimator/analyze",{type:"photos",images:e.images,...e.prompt?{prompt:e.prompt}:{}});return JSON.stringify(t)}},Si=J.object({text:J.string(),prompt:J.string().optional()}),Ke={name:"estimator_analyze_document",description:"Analyze a text document (inspection report, insurance scope of work, contractor notes) and extract structured estimating data \u2014 identified services, quantities, materials, and damage descriptions. Pass the document text in the text field. Use the optional prompt parameter to focus the analysis. Results can be used to create estimates with accurate line items.",inputSchemaZod:Si,execute:async e=>{if(!a())return s();let t=await u("/estimator/analyze",{type:"document",text:e.text,...e.prompt?{prompt:e.prompt}:{}});return JSON.stringify(t)}};import{z as w}from"zod";var bi=w.object({name:w.string(),phone:w.string().optional(),email:w.string().optional(),address:w.string().optional(),notes:w.string().optional()}),Ne={name:"estimator_create_company",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.",inputSchemaZod:bi,execute:async e=>{if(!a())return s();let t=await u("/estimator/companies",e);return JSON.stringify(t)}},vi=w.object({search:w.string().optional(),limit:w.number().optional(),cursor:w.string().optional()}),Me={name:"estimator_list_companies",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.",inputSchemaZod:vi,execute:async e=>{if(!a())return s();let t=new URLSearchParams;e.search&&t.set("search",e.search),e.limit!=null&&t.set("limit",String(e.limit)),e.cursor&&t.set("cursor",e.cursor);let o=t.toString(),i=o?`/estimator/companies?${o}`:"/estimator/companies",p=await h(i);return JSON.stringify(p)}},wi=w.object({companyId:w.string()}),Oe={name:"estimator_get_company",description:"Get a specific company's full details including all its client contacts and a summary of linked estimates.",inputSchemaZod:wi,execute:async e=>{if(!a())return s();let t=await h(`/estimator/companies/${e.companyId}`);return JSON.stringify(t)}},ki=w.object({companyId:w.string(),name:w.string().optional(),phone:w.string().optional(),email:w.string().optional(),address:w.string().optional(),notes:w.string().optional()}),Ze={name:"estimator_update_company",description:"Update a company's details. Pass the companyId and any fields to change: name, phone, email, address, or notes.",inputSchemaZod:ki,execute:async e=>{if(!a())return s();let{companyId:t,...o}=e,i=await P(`/estimator/companies/${t}`,o);return JSON.stringify(i)}},Ii=w.object({companyId:w.string()}),ze={name:"estimator_delete_company",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 \u2014 they become unlinked.",inputSchemaZod:Ii,execute:async e=>{if(!a())return s();let t=await j(`/estimator/companies/${e.companyId}`);return JSON.stringify(t)}};import{z as T}from"zod";var xi=T.object({name:T.string(),companyId:T.string().optional(),title:T.string().optional(),phone:T.string().optional(),email:T.string().optional(),address:T.string().optional(),notes:T.string().optional()}),Le={name:"estimator_create_client",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.",inputSchemaZod:xi,execute:async e=>{if(!a())return s();let t=await u("/estimator/clients",e);return JSON.stringify(t)}},_i=T.object({search:T.string().optional(),companyId:T.string().optional(),limit:T.number().optional(),cursor:T.string().optional()}),Ue={name:"estimator_list_clients",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.",inputSchemaZod:_i,execute:async e=>{if(!a())return s();let t=new URLSearchParams;e.search&&t.set("search",e.search),e.companyId&&t.set("companyId",e.companyId),e.limit!=null&&t.set("limit",String(e.limit)),e.cursor&&t.set("cursor",e.cursor);let o=t.toString(),i=o?`/estimator/clients?${o}`:"/estimator/clients",p=await h(i);return JSON.stringify(p)}},Ai=T.object({clientId:T.string()}),qe={name:"estimator_get_client",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.",inputSchemaZod:Ai,execute:async e=>{if(!a())return s();let t=await h(`/estimator/clients/${e.clientId}`);return JSON.stringify(t)}},Ci=T.object({clientId:T.string(),name:T.string().optional(),companyId:T.string().optional(),title:T.string().optional(),phone:T.string().optional(),email:T.string().optional(),address:T.string().optional(),notes:T.string().optional()}),$e={name:"estimator_update_client",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).",inputSchemaZod:Ci,execute:async e=>{if(!a())return s();let{clientId:t,...o}=e,i=await P(`/estimator/clients/${t}`,o);return JSON.stringify(i)}},Ei=T.object({clientId:T.string()}),De={name:"estimator_delete_client",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.",inputSchemaZod:Ei,execute:async e=>{if(!a())return s();let t=await j(`/estimator/clients/${e.clientId}`);return JSON.stringify(t)}};import{z as c}from"zod";var Pt=c.object({projectId:c.string().uuid()}),ji=c.object({projectName:c.string(),buildingType:c.enum(["commercial","residential"]),siteAddress:c.string().optional(),clientId:c.string().optional(),companyId:c.string().optional()}),Ki=c.object({}),Ni=c.object({cabinetLinearFt:c.number().nonnegative().optional(),rooms:c.array(c.object({name:c.string(),lengthFt:c.number().positive().optional(),widthFt:c.number().positive().optional()})).optional(),baths:c.array(c.object({type:c.enum(["full","half","three_quarter","unknown"]).optional(),count:c.number().int().positive().optional(),label:c.string().optional()})).optional(),flooringSqFt:c.number().nonnegative().optional(),drywallSqFt:c.number().nonnegative().optional(),paintSqFt:c.number().nonnegative().optional(),roofSqFt:c.number().nonnegative().optional(),flags:c.object({wallMovingRisk:c.boolean().optional(),electricalSurpriseRisk:c.boolean().optional(),plumbingSurpriseRisk:c.boolean().optional()}).optional(),notes:c.string().optional()}).optional(),Mi=c.object({projectId:c.string().uuid(),kind:c.string(),fileName:c.string(),base64:c.string(),mimeType:c.string().optional(),remodelHints:Ni}),Oi=c.object({projectId:c.string().uuid(),reviewState:c.string().optional()}),Zi=c.object({projectId:c.string().uuid(),rowId:c.string().uuid(),action:c.enum(["approve","skip","edit"]),edits:c.object({description:c.string().optional(),quantity:c.number().optional(),unit:c.string().optional(),serviceCode:c.string().nullable().optional()}).optional()}),zi=c.object({projectId:c.string().uuid(),overrideNote:c.string().optional()}),Li=c.object({segment:c.enum(["commercial","residential"]).optional()}),Ui=c.object({projectId:c.string().uuid(),systemPackSlug:c.string(),serviceProfileId:c.string().optional()}),qi=c.object({}),$i=c.object({slug:c.string(),name:c.string(),segment:c.enum(["commercial_service","commercial_production","residential_service","residential_production"]),laborMultiplier:c.number().optional(),defaultWastePercent:c.number().optional()}),Di=c.object({estimateId:c.string().uuid(),note:c.string().optional()}),Ji=c.object({estimateId:c.string().uuid(),approvalId:c.string().uuid(),action:c.enum(["approve","reject"]),note:c.string().optional()}),Fi=c.object({estimateId:c.string().uuid()}),Je={name:"estimator_create_takeoff_project",description:"Create a takeoff project shell (commercial or residential). Upload inputs, run extract, review rows, commit, apply system pack, then generate estimate.",inputSchemaZod:ji,execute:async e=>{if(!a())return s();let t=await u("/estimator/takeoffs",e);return JSON.stringify(t)}},Fe={name:"estimator_get_takeoff_project",description:"Get takeoff project with inputs, rows, status, pack/profile, linked estimate.",inputSchemaZod:Pt,execute:async e=>{if(!a())return s();let t=await h(`/estimator/takeoffs/${e.projectId}`);return JSON.stringify(t)}},Ve={name:"estimator_list_takeoff_projects",description:"List takeoff projects for the workspace.",inputSchemaZod:Ki,execute:async e=>{if(!a())return s();let t=await h("/estimator/takeoffs");return JSON.stringify(t)}},Be={name:"estimator_upload_takeoff_input",description:"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) \u2014 quantities are reviewable, not auto-measured from pixels.",inputSchemaZod:Mi,execute:async e=>{if(!a())return s();let t=await u(`/estimator/takeoffs/${e.projectId}/inputs`,{kind:e.kind,fileName:e.fileName,base64:e.base64,...e.mimeType?{mimeType:e.mimeType}:{},...e.remodelHints?{remodelHints:e.remodelHints}:{}});return JSON.stringify(t)}},Ye={name:"estimator_run_takeoff_extract",description:"Run takeoff extractors on all uploaded inputs (idempotent).",inputSchemaZod:Pt,execute:async e=>{if(!a())return s();let t=await u(`/estimator/takeoffs/${e.projectId}/extract`,{});return JSON.stringify(t)}},Ge={name:"estimator_list_takeoff_rows",description:"List takeoff rows with evidence and review state.",inputSchemaZod:Oi,execute:async e=>{if(!a())return s();let t=e.reviewState?`?reviewState=${e.reviewState}`:"",o=await h(`/estimator/takeoffs/${e.projectId}/rows${t}`);return JSON.stringify(o)}},He={name:"estimator_review_takeoff_row",description:"Approve, skip, or edit a single takeoff row.",inputSchemaZod:Zi,execute:async e=>{if(!a())return s();let{projectId:t,rowId:o,...i}=e,p=await P(`/estimator/takeoffs/${t}/rows/${o}`,i);return JSON.stringify(p)}},Xe={name:"estimator_commit_takeoff",description:"Mark takeoff ready after all rows reviewed and area measurement approved (or override note).",inputSchemaZod:zi,execute:async e=>{if(!a())return s();let t=await u(`/estimator/takeoffs/${e.projectId}/commit`,{overrideNote:e.overrideNote});return JSON.stringify(t)}},We={name:"estimator_list_system_packs",description:"List built-in commercial and residential system packs.",inputSchemaZod:Li,execute:async e=>{if(!a())return s();let t=e.segment?`?segment=${e.segment}`:"",o=await h(`/estimator/system-packs${t}`);return JSON.stringify(o)}},Qe={name:"estimator_apply_system_pack",description:"Set system pack slug and optional service profile on takeoff project.",inputSchemaZod:Ui,execute:async e=>{if(!a())return s();let t=await P(`/estimator/takeoffs/${e.projectId}`,{systemPackSlug:e.systemPackSlug,serviceProfileId:e.serviceProfileId??null});return JSON.stringify(t)}},et={name:"estimator_list_service_profiles",description:"List service department profiles (includes v6 defaults).",inputSchemaZod:qi,execute:async e=>{if(!a())return s();let t=await h("/estimator/service-profiles");return JSON.stringify(t)}},tt={name:"estimator_upsert_service_profile",description:"Create or update a service department profile.",inputSchemaZod:$i,execute:async e=>{if(!a())return s();let t=await u("/estimator/service-profiles",e);return JSON.stringify(t)}},ot={name:"estimator_generate_estimate_from_takeoff",description:"Generate draft estimate from committed takeoff + system pack. Preflight fails if catalog codes missing.",inputSchemaZod:Pt,execute:async e=>{if(!a())return s();let t=await u(`/estimator/takeoffs/${e.projectId}/generate-estimate`,{});return JSON.stringify(t)}},it={name:"estimator_request_estimate_approval",description:"Request owner approval before customer send.",inputSchemaZod:Di,execute:async e=>{if(!a())return s();let t=await u(`/estimator/estimates/${e.estimateId}/approval`,{note:e.note});return JSON.stringify(t)}},nt={name:"estimator_approve_estimate",description:"Approve or reject estimate (workspace owner).",inputSchemaZod:Ji,execute:async e=>{if(!a())return s();let t=await u(`/estimator/estimates/${e.estimateId}/approval`,{approvalId:e.approvalId,action:e.action,note:e.note});return JSON.stringify(t)}},at={name:"estimator_get_estimate_approval",description:"Get latest approval status for an estimate.",inputSchemaZod:Fi,execute:async e=>{if(!a())return s();let t=await h(`/estimator/estimates/${e.estimateId}/approval`);return JSON.stringify(t)}};import{z as l}from"zod";var Bi=l.enum(["kitchen_remodel","bath_remodel","kitchen_and_bath_remodel","whole_home_remodel","other_remodel"]),Yi=l.object({projectType:Bi,projectName:l.string().optional(),siteAddress:l.string().optional(),baseScopes:l.array(l.object({key:l.string().optional(),label:l.string(),quantity:l.number().optional(),unit:l.string().optional(),serviceCode:l.string().optional(),budgetLump:l.number().optional(),notes:l.string().optional(),confidence:l.enum(["high","medium","low","unknown"]).optional(),isAlternate:l.boolean().optional(),alternateGroup:l.string().optional()})).optional(),alternates:l.array(l.object({key:l.string(),label:l.string(),scopeKeys:l.array(l.string()).optional(),lumpAdd:l.number().optional(),notes:l.string().optional()})).optional(),attachments:l.array(l.object({id:l.string(),kind:l.enum(["photo","drawing","plan_set","spec_pdf","spreadsheet","other"]),fileName:l.string().optional(),caption:l.string().optional(),url:l.string().optional(),mimeType:l.string().optional()})).optional(),exclusions:l.array(l.string()).optional(),surprises:l.array(l.string()).optional(),rateReferences:l.array(l.object({key:l.string(),label:l.string(),value:l.number(),unit:l.string().optional(),source:l.enum(["operating_rule","catalog","user","takeoff"]),catalogId:l.string().optional(),confidence:l.enum(["high","medium","low","unknown"]).optional()})).optional(),assumptionsNeedingReview:l.array(l.object({id:l.string(),text:l.string(),severity:l.enum(["info","warn","blocker"]),relatedScopeKey:l.string().optional()})).optional(),remodelHints:l.record(l.string(),l.unknown()).optional(),includeStandardAlternates:l.boolean().optional(),notes:l.string().optional()}),st={name:"estimator_normalize_remodel_intake",description:"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.",inputSchemaZod:Yi,execute:async e=>{if(!a())return s();let t=await u("/estimator/intake/normalize-remodel",e);return JSON.stringify(t,null,2)}};import{z as d}from"zod";var Hi=d.object({key:d.string(),label:d.string().optional(),quantity:d.number().optional(),unit:d.string().optional(),serviceCode:d.string().optional(),budgetLump:d.number().optional(),notes:d.string().optional()}),Xi=d.object({intake:d.object({projectName:d.string().optional(),scopes:d.array(Hi),alternates:d.array(d.object({key:d.string(),label:d.string(),scopeKeys:d.array(d.string()).optional(),lumpAdd:d.number().optional(),notes:d.string().optional()})).optional(),includeStandardAlternates:d.boolean().optional(),expandScopeComponents:d.boolean().optional(),exclusions:d.array(d.string()).optional(),assumptions:d.array(d.string()).optional(),notes:d.string().optional()}),takeoffRows:d.array(d.object({id:d.string().optional(),description:d.string(),quantity:d.number(),unit:d.string(),serviceCode:d.string().nullable().optional(),materialType:d.string().nullable().optional()})).optional(),overrides:d.array(d.object({scopeKey:d.string().optional(),serviceCode:d.string().optional(),description:d.string().optional(),quantity:d.number().optional(),unitMaterial:d.number().optional(),unitLabor:d.number().optional(),lumpTotal:d.number().optional(),notes:d.string().optional()})).optional(),pricing:d.object({contingencyPercent:d.number().optional(),markupPercent:d.number().optional(),profitPercent:d.number().optional()}).optional()}),rt={name:"estimator_build_remodel_estimate",description:"Build a reviewable residential remodel estimate from normalized intake scopes, optional takeoff quantities, catalog prices, and overrides. Lump room scopes (kitchen, baths) auto-expand into a per-trade breakdown of line items (demolition, cabinetry, countertops, plumbing, electrical, appliances, flooring, tile, finishes) \u2014 each with its own scope-of-work description carried in notes \u2014 so client-facing output is itemized, not a single vague lump (set intake.expandScopeComponents=false for the legacy single-line behavior). Returns line items with provenance, alternates (both baths, upstairs expansion, roof, wall moving), a default+custom assumptions list, default+custom exclusions, contingency/markup/profit breakdown, and reviewMarkdown. Pass project-specific items via intake.assumptions and intake.exclusions. Does not persist \u2014 use estimator_generate_estimate_from_takeoff or estimator_create_repair after review. Room scope keys: kitchen, bath_full, bath_half, bath_primary, bath_secondary. Trade/quantity scope keys: cabinets, drywall, paint, framing, roof, flooring, electrical_kitchen, electrical_bath. Whole-project phase scope keys: general_conditions, permits, demolition, protection, final_clean.",inputSchemaZod:Xi,execute:async e=>{if(!a())return s();let t=await u("/estimator/estimates/build-remodel",e);return JSON.stringify(t)}};import{z as n}from"zod";var qt=n.object({intake:n.object({projectName:n.string().optional(),scopes:n.array(n.object({key:n.string(),label:n.string().optional(),quantity:n.number().optional(),unit:n.string().optional(),serviceCode:n.string().optional(),budgetLump:n.number().optional(),notes:n.string().optional()})),expandScopeComponents:n.boolean().optional(),includeStandardAlternates:n.boolean().optional(),exclusions:n.array(n.string()).optional(),assumptions:n.array(n.string()).optional(),notes:n.string().optional()}),pricing:n.object({contingencyPercent:n.number().optional(),markupPercent:n.number().optional(),profitPercent:n.number().optional()}).optional()}),Qi=n.object({preset:n.enum(["matthew-portland-2026-06"]).optional(),client:n.object({projectTitle:n.string(),clientName:n.string().optional(),preparedBy:n.string().optional(),location:n.string().optional(),documentType:n.string().optional(),validityNote:n.string().optional()}),budgetPackage:qt.optional(),fullPackage:qt.optional(),alternates:n.array(n.object({key:n.string(),label:n.string(),description:n.string(),budgetPlaceholder:n.number().nullable()})).optional(),correctionNote:n.string().optional(),scopeBasis:n.array(n.string()).optional()}),ct={name:"estimator_build_remodel_deliverable",description:"Build a residential remodel deliverable package using the Matthew v3 golden output standard: contractor planning-budget tone, cover/header, executive summary option cards, scope basis, pricing summary, numbered detailed line items, key allowances, assumptions, exclusions, separated Option C alternates (never in headline totals), recommended next steps, and footer/validity language. Returns clientMarkdown + clientHtml + handoff JSON. Use preset matthew-portland-2026-06 for the reviewed Matthew Portland fixture ($339,025 full / $167,200 bank-budget; golden fixture at remodel-estimates/saved/2026-06-03-matthew-remodel-reviewed-v3/). Guards against superseded totals and generic AI prose. Does not persist \u2014 use clientHtml or handoff for PDF. For single-scope preview only, use estimator_build_remodel_estimate. Prefer estimator_draft_remodel_deliverable when you have scope notes and want the tool to assemble budget/full intakes.",inputSchemaZod:Qi,execute:async e=>{if(!a())return s();let t=await u("/estimator/estimates/build-remodel-deliverable",e);return JSON.stringify(t)}},$t=n.object({key:n.string().min(1),label:n.string().optional(),quantity:n.number().positive().optional(),unit:n.string().optional(),serviceCode:n.string().optional(),budgetLump:n.number().nonnegative().optional(),notes:n.string().optional()}),en=n.object({contingencyPercent:n.number().optional(),markupPercent:n.number().optional(),profitPercent:n.number().optional()}).optional(),Dt=n.object({label:n.string().optional(),whatsIncluded:n.string().optional(),projectName:n.string().optional(),scopes:n.array($t),assumptions:n.array(n.string()).optional(),exclusions:n.array(n.string()).optional(),notes:n.string().optional(),expandScopeComponents:n.boolean().optional(),pricing:en}),tn=Dt.extend({derivedFromFull:n.object({omitFromFullKeys:n.array(n.string()).optional(),replaceWithAllowanceLump:n.array(n.object({key:n.string().min(1),label:n.string().optional(),budgetLump:n.number().nonnegative(),notes:n.string().optional()})).optional(),additionalScopes:n.array($t).optional()}).optional()}),on=n.object({client:n.object({projectTitle:n.string().min(1),clientName:n.string().optional(),preparedBy:n.string().optional(),location:n.string().optional(),documentType:n.string().optional(),validityNote:n.string().optional()}),fullScope:Dt,budgetScope:tn.optional(),alternates:n.array(n.object({key:n.string(),label:n.string(),description:n.string(),budgetPlaceholder:n.number().nullable()})).optional(),correctionNote:n.string().optional(),scopeBasis:n.array(n.string()).optional()}),lt={name:"estimator_draft_remodel_deliverable",description:"Draft a sendable residential remodel deliverable from scope notes in a single call, following the Matthew v3 client-output section contract when scopeBasis/next steps are provided. Provide one fullScope.scopes list (each scope is a key like 'kitchen', 'bath_primary', 'drywall', or a custom slug, plus optional budgetLump/notes \u2014 the built-in catalog supplies label/unit/default rate). Budget package can be (a) explicit via budgetScope.scopes, (b) derived from full via budgetScope.derivedFromFull.omitFromFullKeys and/or replaceWithAllowanceLump, or (c) omitted to mirror full as collapsed allowances. Returns RemodelDeliverablePackage with clientMarkdown, clientHtml, handoff metadata, and reviewMarkdown (agent-facing, includes MCP guidance). Does not persist. Use for fresh remodel intakes instead of estimator_build_remodel_deliverable when you have not yet authored both intakes by hand.",inputSchemaZod:on,execute:async e=>{if(!a())return s();let t=await u("/estimator/estimates/draft-remodel-deliverable",e);return JSON.stringify(t)}};import{z as N}from"zod";var an=N.object({estimateId:N.string(),recipientEmail:N.string().email().optional(),recipientName:N.string().max(200).optional(),senderNote:N.string().max(2e3).optional(),expiresInDays:N.number().int().min(1).max(365).optional()}),pt={name:"estimator_create_signing_request",description:"Create a contract signing request for an estimate. Returns a tokenized signing URL that can be shared with the client. The client can review and electronically sign the estimate at that URL. The signing request expires after expiresInDays (default 30). Does not send emails during implementation.",inputSchemaZod:an,execute:async e=>{if(!a())return s();let{estimateId:t,...o}=e,i=await u(`/estimator/estimates/${t}/signing-requests`,o);return JSON.stringify(i)}},sn=N.object({estimateId:N.string()}),mt={name:"estimator_get_signing_requests",description:"List all signing requests for an estimate. Returns status (pending/signed/expired/cancelled), signing URL, recipient, and audit metadata for each request.",inputSchemaZod:sn,execute:async e=>{if(!a())return s();let t=await h(`/estimator/estimates/${e.estimateId}/signing-requests`);return JSON.stringify(t)}},rn=N.object({estimateId:N.string(),requestId:N.string()}),dt={name:"estimator_cancel_signing_request",description:"Cancel a pending signing request. Cannot cancel a request that has already been signed. The signing URL will become invalid immediately.",inputSchemaZod:rn,execute:async e=>{if(!a())return s();let{estimateId:t,requestId:o}=e,i=await P(`/estimator/estimates/${t}/signing-requests`,{requestId:o});return JSON.stringify(i)}};import{existsSync as ln,readFileSync as pn}from"node:fs";import{dirname as xt,join as Ft}from"node:path";import{fileURLToPath as Jt}from"node:url";function mn(e){let t=xt(Jt(e));for(let o=0;o<6;o+=1){if(ln(Ft(t,"package.json")))return t;let i=xt(t);if(i===t)break;t=i}throw new Error(`package.json not found above ${xt(Jt(e))}`)}function dn(e,t){let o=Ft(mn(e),"package.json"),p=JSON.parse(pn(o,"utf8"))[t];if(typeof p!="string"||!p.trim())throw new Error(`Missing package.json ${t} at ${o}`);return p}function Vt(e=import.meta.url){return dn(e,"version")}var fn=Vt(import.meta.url);function Bt(e,t=!1){return{content:[{type:"text",text:e}],...t?{isError:!0}:{}}}function Yt(){let e=new un({name:"kynver-mcp-estimator",version:fn},{capabilities:{tools:{}}});function t(o,i,p){e.registerTool(o,i,async S=>{try{return Bt(await p(S))}catch(g){let R=g instanceof z?g.message:L(g)?D(g):g instanceof Error?g.message:"The estimate service could not be reached. The save was not confirmed.";return Bt(R,!0)}})}return t(Y.name,{description:Y.description,inputSchema:Y.inputSchemaZod},o=>Y.execute(o)),t(G.name,{description:G.description,inputSchema:G.inputSchemaZod},o=>G.execute(o)),t(H.name,{description:H.description,inputSchema:H.inputSchemaZod},o=>H.execute(o)),t(X.name,{description:X.description,inputSchema:X.inputSchemaZod},o=>X.execute(o)),t(W.name,{description:W.description,inputSchema:W.inputSchemaZod},o=>W.execute(o)),t(Q.name,{description:Q.description,inputSchema:Q.inputSchemaZod},o=>Q.execute(o)),t(ee.name,{description:ee.description,inputSchema:ee.inputSchemaZod},o=>ee.execute(o)),t(te.name,{description:te.description,inputSchema:te.inputSchemaZod},o=>te.execute(o)),t(oe.name,{description:oe.description,inputSchema:oe.inputSchemaZod},o=>oe.execute(o)),t(ie.name,{description:ie.description,inputSchema:ie.inputSchemaZod},o=>ie.execute(o)),t(ne.name,{description:ne.description,inputSchema:ne.inputSchemaZod},o=>ne.execute(o)),t(ae.name,{description:ae.description,inputSchema:ae.inputSchemaZod},o=>ae.execute(o)),t(se.name,{description:se.description,inputSchema:se.inputSchemaZod},o=>se.execute(o)),t(re.name,{description:re.description,inputSchema:re.inputSchemaZod},o=>re.execute(o)),t(ce.name,{description:ce.description,inputSchema:ce.inputSchemaZod},o=>ce.execute(o)),t(le.name,{description:le.description,inputSchema:le.inputSchemaZod},o=>le.execute(o)),t(pe.name,{description:pe.description,inputSchema:pe.inputSchemaZod},o=>pe.execute(o)),t(me.name,{description:me.description,inputSchema:me.inputSchemaZod},o=>me.execute(o)),t(de.name,{description:de.description,inputSchema:de.inputSchemaZod},o=>de.execute(o)),t(ue.name,{description:ue.description,inputSchema:ue.inputSchemaZod},o=>ue.execute(o)),t(fe.name,{description:fe.description,inputSchema:fe.inputSchemaZod},o=>fe.execute(o)),t(ge.name,{description:ge.description,inputSchema:ge.inputSchemaZod},o=>ge.execute(o)),t(he.name,{description:he.description,inputSchema:he.inputSchemaZod},o=>he.execute(o)),t(ye.name,{description:ye.description,inputSchema:ye.inputSchemaZod},o=>ye.execute(o)),t(rt.name,{description:rt.description,inputSchema:rt.inputSchemaZod},o=>rt.execute(o)),t(ct.name,{description:ct.description,inputSchema:ct.inputSchemaZod},o=>ct.execute(o)),t(lt.name,{description:lt.description,inputSchema:lt.inputSchemaZod},o=>lt.execute(o)),t(Se.name,{description:Se.description,inputSchema:Se.inputSchemaZod},o=>Se.execute(o)),t(Te.name,{description:Te.description,inputSchema:Te.inputSchemaZod},o=>Te.execute(o)),t(be.name,{description:be.description,inputSchema:be.inputSchemaZod},o=>be.execute(o)),t(ve.name,{description:ve.description,inputSchema:ve.inputSchemaZod},o=>ve.execute(o)),t(ke.name,{description:ke.description,inputSchema:ke.inputSchemaZod},o=>ke.execute(o)),t(Ie.name,{description:Ie.description,inputSchema:Ie.inputSchemaZod},o=>Ie.execute(o)),t(Pe.name,{description:Pe.description,inputSchema:Pe.inputSchemaZod},o=>Pe.execute(o)),t(xe.name,{description:xe.description,inputSchema:xe.inputSchemaZod},o=>xe.execute(o)),t(_e.name,{description:_e.description,inputSchema:_e.inputSchemaZod},o=>_e.execute(o)),t(Ae.name,{description:Ae.description,inputSchema:Ae.inputSchemaZod},o=>Ae.execute(o)),t(Ce.name,{description:Ce.description,inputSchema:Ce.inputSchemaZod},o=>Ce.execute(o)),t(Ee.name,{description:Ee.description,inputSchema:Ee.inputSchemaZod},o=>Ee.execute(o)),t(Re.name,{description:Re.description,inputSchema:Re.inputSchemaZod},o=>Re.execute(o)),t(je.name,{description:je.description,inputSchema:je.inputSchemaZod},o=>je.execute(o)),t(Ke.name,{description:Ke.description,inputSchema:Ke.inputSchemaZod},o=>Ke.execute(o)),t(Ne.name,{description:Ne.description,inputSchema:Ne.inputSchemaZod},o=>Ne.execute(o)),t(Me.name,{description:Me.description,inputSchema:Me.inputSchemaZod},o=>Me.execute(o)),t(Oe.name,{description:Oe.description,inputSchema:Oe.inputSchemaZod},o=>Oe.execute(o)),t(Ze.name,{description:Ze.description,inputSchema:Ze.inputSchemaZod},o=>Ze.execute(o)),t(ze.name,{description:ze.description,inputSchema:ze.inputSchemaZod},o=>ze.execute(o)),t(Le.name,{description:Le.description,inputSchema:Le.inputSchemaZod},o=>Le.execute(o)),t(Ue.name,{description:Ue.description,inputSchema:Ue.inputSchemaZod},o=>Ue.execute(o)),t(qe.name,{description:qe.description,inputSchema:qe.inputSchemaZod},o=>qe.execute(o)),t($e.name,{description:$e.description,inputSchema:$e.inputSchemaZod},o=>$e.execute(o)),t(De.name,{description:De.description,inputSchema:De.inputSchemaZod},o=>De.execute(o)),t(Je.name,{description:Je.description,inputSchema:Je.inputSchemaZod},o=>Je.execute(o)),t(Fe.name,{description:Fe.description,inputSchema:Fe.inputSchemaZod},o=>Fe.execute(o)),t(Ve.name,{description:Ve.description,inputSchema:Ve.inputSchemaZod},o=>Ve.execute(o)),t(Be.name,{description:Be.description,inputSchema:Be.inputSchemaZod},o=>Be.execute(o)),t(Ye.name,{description:Ye.description,inputSchema:Ye.inputSchemaZod},o=>Ye.execute(o)),t(Ge.name,{description:Ge.description,inputSchema:Ge.inputSchemaZod},o=>Ge.execute(o)),t(He.name,{description:He.description,inputSchema:He.inputSchemaZod},o=>He.execute(o)),t(Xe.name,{description:Xe.description,inputSchema:Xe.inputSchemaZod},o=>Xe.execute(o)),t(We.name,{description:We.description,inputSchema:We.inputSchemaZod},o=>We.execute(o)),t(Qe.name,{description:Qe.description,inputSchema:Qe.inputSchemaZod},o=>Qe.execute(o)),t(et.name,{description:et.description,inputSchema:et.inputSchemaZod},o=>et.execute(o)),t(tt.name,{description:tt.description,inputSchema:tt.inputSchemaZod},o=>tt.execute(o)),t(ot.name,{description:ot.description,inputSchema:ot.inputSchemaZod},o=>ot.execute(o)),t(it.name,{description:it.description,inputSchema:it.inputSchemaZod},o=>it.execute(o)),t(nt.name,{description:nt.description,inputSchema:nt.inputSchemaZod},o=>nt.execute(o)),t(at.name,{description:at.description,inputSchema:at.inputSchemaZod},o=>at.execute(o)),t(st.name,{description:st.description,inputSchema:st.inputSchemaZod},o=>st.execute(o)),t(pt.name,{description:pt.description,inputSchema:pt.inputSchemaZod},o=>pt.execute(o)),t(mt.name,{description:mt.description,inputSchema:mt.inputSchemaZod},o=>mt.execute(o)),t(dt.name,{description:dt.description,inputSchema:dt.inputSchemaZod},o=>dt.execute(o)),e}Mt({label:"Estimator server",defaultPort:3104,createMcpServer:Yt,requestContext:It});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kynver-app/mcp-estimator",
3
- "version": "0.2.22",
3
+ "version": "0.2.23",
4
4
  "files": [
5
5
  "dist",
6
6
  "!dist/**/__tests__/**",