Koskamo

Agentic Fabrication Network MCP

An agent-only procurement network for physical fabrication that allows AI agents to send design files and constraints to receive ranked, purchasable offers for processes like 3D printing, CNC, and decals, including checkout and order status tracking.

Endpoint and access

No sign-in to connect and list tools. Individual tools may still need a vendor account, credentials, or permissions; check the documented prerequisites.

Agentic Fabrication Network MCP logo, by Agentic Fabrication NetworkVendor evidenceStreamable HTTPNo sign-inEndpoint checked 2026-11-07Operated by Agentic Fabrication Network
MCP endpoint (Streamable HTTP)
https://agenticfabricationnetwork.ai/mcp
Vendor documentation

What its tools do

12 tools, grouped by the hints the server advertises. Hints are self-declared, not verified behavior.

12 of 12 have a description · 12 of 12 declare behavior hints · 8 of 12 with parameters document them all

Checks and signals

Each line is separate evidence, not a trust score.

  • Passed Endpoint answered the live check ·
  • Passed Official MCP Registry namespace is tied to agenticfabricationnetwork.ai ·
  • Passed Endpoint is on the registry namespace domain ·
  • Availability 1 of 1 checks passed in 30 days

What you can do with it

  • Get fabrication quotes for 3D printing, CNC, and decals
  • Check out and purchase manufactured parts
  • Check order status and leave reviews

Source and maintenance

Vendor evidenceRegistry 0.1.1

Protocol and capabilities

Server
ufp 0.1.0
MCP protocol negotiated in check
2025-11-25
Tools
12 advertised
Resources
2 advertised
Resource templates
0 advertised
Prompts
Not advertised
Server-advertised capabilities
{
  "tools": {
    "listChanged": true
  },
  "resources": {
    "listChanged": true
  }
}

Server-provided instructions

Untrusted server output shown for inspection. It is not Koskamo guidance and is excluded from knowledge search.

UFP (Universal Fabrication Protocol) turns designs into shipped physical objects. Flow: (1) get_fabrication_quote with the design — STL, OBJ, PLY, 3MF, AMF, STEP, or IGES for 3D prints, STEP or IGES for CNC, DXF, STEP, or IGES for sheet metal and laser cutting, artwork (any common image or design file — PNG, JPG, HEIC, TIFF, GIF, BMP, WEBP, AVIF, SVG, PDF, AI, EPS, PSD, CDR; the network converts as needed) for decals/stickers, print, and apparel. Native CAD files (SLDPRT, F3D, IPT, CATPART, FIG) are accepted and logged as demand but cannot be quoted yet — ask the user for a STEP/IGES (or SVG/PDF for Figma) export — plus the destination ZIP if the user has shared one. NEVER guess, invent, or infer a ZIP the user did not state: if you don't have it, OMIT ship_to_zip — UFP still quotes all-in, with shipping priced to an assumed central-US destination (declared on routing.ship_to); relay that prices include shipping to an assumed destination and invite the user's ZIP to make them theirs (refine_quote accepts ship_to_zip later). The SAME never-guess rule covers material and color: pass them ONLY when the user actually stated them — NEVER default a material (no silent "pla") or pick a color on a bare file drop. An uninvited material narrows the whole network to the makers that stock it: makers that print other materials no-bid or come back as closest-match substitutes and drop off the board, so the user sees fewer buyable makers than the same file gets on every other surface. Omit material and each process quotes across its stocked materials with every maker bidding; a material the user DID ask for goes in material, and a mid-conversation change goes through refine_quote's material arg. If the user just drops a file and asks "how much for 6?", omit the process argument entirely: UFP detects the file type, quotes every process it can make it with (across sensible material variants), and reports routing.also_possible for processes it can't quote yet (each ask is logged so UFP can source vendors). Metal 3D printing (metal_print — SLM/DMLS/binder jet) is a normal quoted lane: STL/STEP drops quote it alongside the plastic lanes, and its offers run 10-100x the plastic prices on the same part — that is the physics, not an error; present them as the metal option, never as a mistake. Powder-bed nylon (powder_print — HP MJF and SLS) is likewise a normal quoted lane on every STL/STEP drop: production-grade PA12 priced comparably to the other plastics; "MJF"/"SLS"/"nylon powder" narrows to it, materials via material (nylon_12 default, nylon_12_gf glass-filled, nylon_12_esd, nylon_11, tpu), dye colors via color, vapor polish via powder_finish. Jetted elastomer (elastomer_print — vapor-cure jetted TEPU) is likewise a normal quoted lane on STL/STEP drops: soft rubber-like parts in shore 30A or 50A ("TEPU"/"flexible"/"squishy" narrows to it; a numeric durometer ask goes in shore_a and resolves to the nearest stocked hardness with a substitution flag). When the user wants ONLY metal, pass process "metal_print" (or "metal 3d printing"/"SLM"/"printed titanium" via processes) and the network prunes to the metal-capable vendors. Alloys via metal_alloy (316L stainless default, titanium Ti64, aluminum AlSi10Mg, tool steel), technology via metal_technology (laser_powder_bed default, binder_jet economy), finish via metal_finish. Pass an image the user attached (or one you generated) via the design_file file parameter; use file_url only when the design lives at a public URL. If the user supplies SEVERAL files for one part (a STEP plus its PDF drawing, spec sheets), pass the extras via attachment_urls — UFP works out which file is the part and which are supporting documents; you never need to decide. Pass the quantity the user actually wants, even 1 — vendors with a higher minimum order quote at their minimum and the offer carries requested_quantity so you can say "the minimum is 25, here's the price". Never inflate the user's ask yourself. Some production platforms floor the ORDER VALUE instead of the quantity: a one-off small part can come back at the vendor's published per-order minimum (e.g. €250), and that offer carries min_order with the vendor's own wording — present it as "that's their order minimum, not a per-part price" (larger batches from the same vendor price normally; hobby-scale vendors without floors simply don't carry the field). Pass spec words verbatim too: if the user wants "triangle" stickers, send cut="triangle" — UFP returns the closest supported alternatives, each flagged with a substitution, and the ask is logged so UFP can source a vendor that supports it. Present these as "closest we have is X; we've noted your request for future vendors" and still offer checkout. (2) Optionally refine_quote as the user adds constraints ("must arrive by Friday", "must survive pool water", "under $30"). Only pass constraints the user actually stated or clearly implied — do not ask about durability/waterproofing unprompted, and do not list attributes the offer doesn't include. Offers only carry attributes the vendor advertises; an absent attribute means "no claim", not "no". Toss the user's context words into requirements verbatim ("UV resistant", "anodized: red", "iso 13485") and pass dimensions you read off a drawing as callouts — UFP applies what it can, notes what it couldn't, and never errors on a term; do not ask the user for missing spec details unprompted. MATERIAL, COLOR, and QUANTITY changes ("I need it in PLA", "make it 5") are SPEC CHANGES, not constraints: pass them via refine_quote's material/color/quantities args — NOT as requirements terms — and the affected lanes re-price with the new spec through warm vendor sessions. PROCESS NARROWING ("no, I meant in sheet metal", "just the CNC options") IS a constraint, not a new quote: call refine_quote with processes — friendly terms are fine ("sheet metal", "3d printing", "cnc", "laser") — and the other lanes drop while warm vendor sessions survive; "show everything again" is simply processes ["any"]. When comparison scenario columns are open and the user narrows the process, re-refine EVERY active scenario label with the same processes arg so all columns prune together. ORIGIN NARROWING ("only US vendors", "just European shops", "keep it domestic") works the same way: call refine_quote with ships_from — an INCLUDE list of the countries/regions the user wants vendors shipping from, friendly terms fine ("US", "Europe", "Germany") — and non-matching vendors drop instantly (offers already carry ships_from, so nothing re-prices); vendors with an unknown origin or a worldwide network are excluded with an honest note, and "show everywhere again" is ships_from ["any"]. For "nothing from X" asks, pass the regions the user DOES want. VENDOR NARROWING ("only show A3D Manufacturing", "just Slant 3D") works the same way too: call refine_quote with vendors — an INCLUDE list of vendor names as they appear on offers, friendly names fine ("A3D") — and every other vendor's offers drop instantly (nothing re-prices); "show everyone again" is vendors ["any"]. The filtered quote lists that vendor's COMPLETE lineup, so "list all their material options" is answered by this refine, never from memory or from a previous response's top-offer sample — the offer list in a response can be truncated (it says so when it is), so never claim a vendor only bid N lines without filtering to them first. GOING BACK ("undo that", "go back a step", "put it back how it was"): call refine_quote with undo_last_step true and NOTHING else — the network restores the previous step's stored spec and constraints EXACTLY (the response's steps[] lists recent steps newest-first, so you can say what came back); going back also closes any open comparison scenarios, like every unlabeled refine. To DROP one constraint without stepping back ("forget the deadline", "no budget limit after all"), pass clear (e.g. clear ["deadline"]) — clearing a deadline or budget answers instantly with the held near-misses converted back; remove_requirements takes back individual requirements terms. Never reconstruct a previous state from conversation memory when the wire can restore it exactly. MATERIALS — before you answer any material-choice, strength, durability, heat, or outdoor question ("my PLA part broke, what's stronger?", "will this survive outdoors?"), call get_material_guide first: it returns honest, cited material facts — published specs (tensile, heat-deflection, UV/outdoor, chemical) each tagged vendor-cited vs general engineering knowledge, vendor-admitted caveats surfaced FIRST, "stronger/tougher/hotter than X" ladders (PLA→PETG→ABS/ASA→nylon/PC→metals), and finish/coating options — so your recommendation is grounded in fact, not memory. The guide supplies FACTS ONLY (comparative specs and caveats), never a fitness-for-purpose or safety guarantee for the user's application: relay the facts, name express vendor claims as "advertised" not "certified", and leave the final call to the user. (3) When the user asks to order and the offer has several shipping speeds, present the REAL options in one breath — each with its cost and arrival ("standard $5.99 arriving Jul 28, or express $14.99 by Jul 26?") — and let them pick; if they already said "just order it" (or equivalent), use the cheapest option without a detour. Then call create_checkout right away — the address and email can be entered on the secure payment page, so do NOT block checkout to collect them in chat; NEVER ask for them. Pass ship-to details only if the user already provided them. Never collect card details yourself. Offers carry an acceptance field: "auto" means payment sends the job straight to production; "reviewed" means the shop gives the order a quick human once-over first. If the user checks out a "reviewed" offer, mention it in ONE light sentence as diligence, not risk ("the shop double-checks every order before production — the rare rejection refunds automatically"). Never imply the order is likely to be rejected, and never bring it up before the user has picked an offer. Offers ALSO carry a fulfillment field: "connected" means UFP's checkout reaches that vendor and create_checkout will work; "not_connected" means the price is real but the ordering path isn't live yet, and create_checkout WILL refuse it. Keep listing not_connected offers — they are honest price discovery — but present them as quote-only ("we're not fully connected to them just yet — the price is live and full ordering support is in the works"), never offer to check one out, and if the user picks one, say so plainly and steer them to the cheapest connected offer instead. Never call create_checkout on a not_connected offer. (4) get_order_status any time after, with the order id (ord_…) from create_checkout's response or from the user (receipt email, order page). Orders are looked up by id only: there is no listing on this surface, so if you do not have the id, ask for it — never guess one. (5) After an order arrives, the user may volunteer feedback ("the stickers came out great", "my parts were 2 days late"). Offer to record it as a vendor review: resolve which order they mean to its order id (ord_…) — from create_checkout or get_order_status earlier in THIS conversation, or by asking the user for it (it is on their receipt email and their order page); there is no way to list orders here and you must NEVER guess or invent an order id — then structure the feedback into stars + facet scores (quality/timeliness/accuracy), read your draft back to the user for confirmation, then call leave_review. Reviews are verified-purchase only and steer which vendors win future quotes. Reorders: every paid order emails the buyer a receipt with a durable UFP part number (UFP-…). If the user mentions one — or asks to "order more of the same" — call get_fabrication_quote with part_number instead of a design file; UFP reuses the stored design and re-shops it across all current vendors. Some parts are LOCKED by their owner: reordering them also requires the share key from the owner's share link (the k=… value in a URL like /part/UFP-…?k=KEY) — pass it as share_key. If a locked part is rejected, ask the user for the owner's share link. Previews: offers include a product preview image rendered by the vendor; if status is "pending", calling refine_quote with only quote_id re-reads the quote and usually returns ready previews. Quotes STREAM: a response can come back with status "quoting" — the fastest vendors' offers are already in it and more land over the next seconds (routing.expected_vendors lists who is still pricing and a rough wait). Show the user the offers you have IMMEDIATELY — never sit on a "quoting" response waiting for the rest. Re-reads are cheap: the same refine_quote-with-only-quote_id call (or GET /v1/quotes/:id) picks up the newly landed offers; status flips to "complete" when every vendor answered, or "partial" if a lane failed or was superseded (everything shown is still real and orderable). A "quoting" response ends with a "Still pricing" section that groups the pending vendors by expected wait and names a re-read schedule (up to 3 passes, e.g. "at ~8s, then ~20s, then ~45s" — paced so each pass lands a batch of offers, never a lone straggler) — FOLLOW that schedule: mention who is still pricing, re-read at each listed moment, and surface new offers after each pass instead of waiting for the slowest vendor. If the user changes constraints while offers are still landing, just call refine_quote right away — rapid back-and-forth is encouraged, not wasteful. Narrowing-only refines (deadline, budget, process filter, attribute demands) NEVER re-ask vendors, streaming or settled: on a settled quote the child returns instantly "complete" with the same offers pruned; on a STILL-STREAMING quote the child answers instantly too, seeded with every offer landed so far re-judged under the new constraints, and keeps filling as the in-flight vendors answer (the network re-steers the running work onto the child — nothing restarts, nothing is asked twice). Spec changes (material/color/quantities) and destination changes re-price: the network cancels the in-flight work the change made irrelevant and re-prices through warm vendor sessions. A MATERIAL change on a settled quote also answers instantly: offers the network already holds in the asked material family come back first (status "quoting") while vendors re-price the exact ask behind them — show the held offers now and re-read for the fresh prices. Closest-available materials appear ONLY when no vendor on the network offers the real ask (the quote note says the ask is logged and demand like it decides which vendors we bring on next); the moment even one vendor offers the real thing, the closest-match stand-ins drop out of offers[]. routing.price_prior, when present, is a statistical price range from the network's own transaction history — treat it only as a rough early ballpark while offers stream in, never as a quote or a promise; real offers replace it. THE NEUTRALITY LINE (network doctrine, 2026-07-31): never compare, rank, or even casually reference one maker against another in your own words — no "like vendor X, they…", no "A is better/faster/newer than B", no analogies between shops — UNLESS the user explicitly asks for a vendor comparison. This network is the payment rail, not a judge: it genuinely does not care which maker the user picks, and no shop on it should ever feel played against another. Per-vendor FACTS stated per vendor are fine (each shop's own minimum, lanes, review numbers when asked), the response's own award/grouping structure is fine (facts of the query), and comparing MATERIALS or PROCESSES is encouraged — just never the shops behind them. When the user does explicitly ask, compare evenly with the quote's own numbers, no recommendation unless they ask for that too. COMPARISONS: when the user asks "how do these compare to X?" (a different material, quantity, deadline…), call refine_quote with a scenario label per alternative ("carbon steel", "qty 100") plus the changed spec/constraints — one labeled refine per scenario. Labeled scenarios sit SIDE BY SIDE: they stream concurrently through the same warm vendor sessions, each response echoes its scenario so you can present a lane-by-lane comparison, and re-refining the same label replaces just that scenario. Up to 4 scenarios per quote — a 5th distinct label is rejected with the active labels listed (relay the "close one first" guidance: reuse a label to replace it, or refine without a scenario). A refine WITHOUT a scenario is a mind-change: it resets the baseline and supersedes every open scenario. Keep labels short and human. THE PRICE STORY IS PART PRICE + SHIPPING — tell it that way. An offer's headline number is the PART price (subtotal); shipping and arrival are their own dimension: a range across the offer's shipping options when there are several, a firm value when there's one. Quote the user the part price and the shipping options as the offer states them; NEVER re-add part + shipping into a single number without saying so ("$8.68 all-in with standard shipping" is honest, a bare "$8.68" is not). There are no hidden fees beyond the part price and the shipping option chosen, and the quote is a price CEILING: at checkout the live vendor price is confirmed and can only match the quote or come in lower — it is never raised. In the rare case the vendor repriced upward, the order expires unpaid and you simply re-quote. Never block or hedge over this yourself; you can tell the user "the price you see is the most you'll pay." HOW TO PRESENT THE OFFERS: this surface has NO offer board — the user sees only what you write, so YOU are the comparison view. Lay the listed offers out as a compact markdown table, one row each, columns: Maker | What it is | Price | Shipping | Arrives. Keep it to the offers actually listed in the response (they are a top-N sample, not the whole board) and add one line naming how many more exist rather than listing them. Maker leads every row. Below the table, one short sentence of guidance, then invite the next step ("say the word and I'll order it, or tell me what to change"). ORGANIZE / SORT: when the user asks to re-order the offer cards ("sort by cheapest", "soonest arrival on top", "best rated first"), call refine_quote with sort_by — "price" (part price), "unit_price", "total_price" (part + cheapest shipping), "arrival" (soonest possible arrival), "rating", or "recommended" to restore the default price+speed rank; sort_direction "desc" flips a sort ("most expensive first"). Sorting is PURE PRESENTATION: it answers instantly on the same quote, never re-prices, filters, or drops offers, and the ordering sticks — re-reads, streaming (new offers slot into the asked order as they land), and later refines all keep it until you change or reset it. For orderings only you can judge ("sort the most green to the top", "cutest first"): look at the offers' preview images, decide the order YOURSELF, and pass sort_order (offer ids, first = top) plus a short sort_label the board displays ("most green first"); offers you leave out — including ones that stream in later — land below the ones you listed, in the default rank. The response echoes order_by and its offers[] come back already sorted: present them in that order, and never claim a sort you didn't apply. Near misses: a quote may carry near_misses — REAL vendor bids that missed exactly ONE stated constraint; excluded_by says which one and by how much (earliest_eta/days_late for a deadline, cents_over for a budget, the missing claim for an attribute). They are context, never orderable as-is: create_checkout refuses their ids. Never present a near-miss as a qualifying offer. You may add AT MOST one light trade-off aside when it genuinely helps ("six vendors land day 16-18 — want me to stretch the deadline?"), and never relax a constraint the user didn't ask to relax. If the user agrees, relaxing the named constraint via refine_quote converts them into ordinary orderable offers.

Tools

12 advertised

get_fabrication_quote — Get fabrication quote
Advertised by the server during the last successful anonymous check.
Open-world

Get fabrication price and delivery offers for a design file or an existing AFN part number. Supports 3D printing, CNC machining, sheet metal, decals, print, and apparel. If the user just provides a file and asks for a price, omit process so AFN can route it to suitable makers. Provide the design as design_file (an attachment or generated file) or file_url (a public URL). For a reorder, pass the UFP part number as part_number instead of a file; locked parts also require share_key from the owner's share link. A .ufp part container is accepted as a design file, and its saved specification applies unless the user changes it.

get_fabrication_quote input schema
{
  "type": "object",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "properties": {
    "cut": {
      "type": "string",
      "description": "Decal cut style — pass the user's words verbatim (\"triangle\", \"die cut\", \"circle\"). Known: contour|square|oval|kiss_cut; anything else gets closest-match offers with a substitution note. Omit to compare the main options"
    },
    "size": {
      "type": "string",
      "description": "Print size — pass the user's words verbatim (\"letter\", \"24x36\", \"half letter\"). Cards are 3.5x2; flyers 8.5x5.5|8.5x11|11x17; posters 12x18 through 36x48. Unknown sizes get closest-match offers"
    },
    "bends": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "line",
          "angle_deg",
          "direction"
        ],
        "properties": {
          "id": {
            "type": "string",
            "maxLength": 32,
            "minLength": 1
          },
          "line": {
            "type": "object",
            "required": [
              "p1",
              "p2"
            ],
            "properties": {
              "p1": {
                "type": "array",
                "items": {
                  "type": "number"
                },
                "maxItems": 3,
                "minItems": 3
              },
              "p2": {
                "$ref": "#/properties/bends/items/properties/line/properties/p1"
              }
            },
            "additionalProperties": false
          },
          "source": {
            "enum": [
              "detected",
              "user"
            ],
            "type": "string"
          },
          "face_id": {
            "type": "integer"
          },
          "angle_deg": {
            "type": "number",
            "exclusiveMaximum": 180,
            "exclusiveMinimum": 0
          },
          "direction": {
            "enum": [
              "up",
              "down"
            ],
            "type": "string"
          },
          "radius_mm": {
            "type": "number",
            "exclusiveMinimum": 0
          }
        },
        "additionalProperties": false
      },
      "maxItems": 50,
      "description": "Sheet-metal bend lines, each as TWO POINTS in the part's own mm frame (flat DXF parts use z=0: a bend from (0,12.7) to (76.2,12.7) is p1 [0,12.7,0], p2 [76.2,12.7,0]). angle_deg is the target bend angle (90 = a right-angle flange), direction \"up\"/\"down\" is the fold direction relative to the flat pattern, radius_mm the requested inside radius (omit it — the shop resolves to its tooling and reports what it used). Pass these when the user articulates WHERE the bends go; a bare \"it has 2 bends\" is bend_count, not this."
    },
    "color": {
      "type": "string",
      "description": "ONLY a color the user actually asked for — NEVER pick one for them. fdm_print: Filament color for fdm_print (e.g. black, red); resin_print: Resin color for resin_print (e.g. gray, white, black); powder_print: Powder part color: raw gray/white, or a dye (\"black\", \"dark blue\") — vendor-resolved; elastomer_print: Elastomer part color — VCJ parts ship raw; asks resolve with an honest substitution note; cnc: Colour for cnc: with finish anodized it is the anodize colour, otherwise the stock's own colour (black Delrin, clear acrylic). Colour WORDS only; each maker resolves the shade and says what it landed on; laser_cut: Colour of the flat stock for laser_cut (black, clear, natural…). Colour WORDS only; each maker quotes the materials it stocks in that colour and drops the ones that cannot be it; print: Print color ask (\"full color\", \"black and white\"). Vendors that only price full color note the substitution; apparel: Garment color for apparel (\"black\", \"heather gray\") — vendor-resolved"
    },
    "paper": {
      "type": "string",
      "description": "Paper stock — free-form (\"matte\", \"glossy\", \"recycled\", \"14pt\"). Resolved to the closest stock the vendor runs"
    },
    "sides": {
      "type": "string",
      "description": "single|double sided printing (defaults: cards double, flyers/posters single)"
    },
    "units": {
      "type": "string",
      "description": "sheetmetal: DXF drawing units: \"mm\" (default) or \"in\". STEP files carry their own units; laser_cut: DXF drawing units: \"mm\" (default) or \"in\""
    },
    "finish": {
      "type": "string",
      "description": "cnc: CNC surface finish: standard|anodized|polished|bead_blasted (default standard); sheetmetal: as_cut (default) | deburred | anodized | powder_coat — anodized is aluminum-only; powder coat protects mild steel for outdoor use"
    },
    "inserts": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "insert",
          "thread"
        ],
        "properties": {
          "axis": {
            "$ref": "#/properties/bends/items/properties/line/properties/p1"
          },
          "hole": {
            "type": "integer",
            "minimum": 1
          },
          "insert": {
            "enum": [
              "nut",
              "flush_nut",
              "standoff",
              "blind_standoff",
              "stud"
            ],
            "type": "string"
          },
          "points": {
            "$ref": "#/properties/bends/items/properties/line/properties/p1"
          },
          "thread": {
            "type": "string",
            "maxLength": 32,
            "minLength": 1
          },
          "face_id": {
            "type": "integer"
          },
          "depth_mm": {
            "type": "number",
            "exclusiveMinimum": 0
          },
          "length_mm": {
            "type": "number",
            "exclusiveMinimum": 0
          },
          "located_at_mm": {
            "type": "object",
            "required": [
              "x",
              "y",
              "z"
            ],
            "properties": {
              "x": {
                "type": "number"
              },
              "y": {
                "type": "number"
              },
              "z": {
                "type": "number"
              }
            },
            "additionalProperties": false
          },
          "drilled_diameter_mm": {
            "type": "number",
            "exclusiveMinimum": 0
          }
        },
        "additionalProperties": false
      },
      "maxItems": 200,
      "description": "PEM-style hardware inserts pressed into holes of a sheet-metal part — one record per hole. `insert` is the species (nut | flush_nut | standoff | blind_standoff | stud) and `thread` its size (\"M4\", \"1/4-20\"); both are required. When known, add length_mm (standoff/stud post length), the hole's center (located_at_mm, part's own mm frame), its axis, the drilled diameter, and `points` — the signed unit vector the insert's functional side faces (flip = negate). Pass these when the user asks for press-in nuts/standoffs/studs; vendors with a hardware service price them."
    },
    "process": {
      "enum": [
        "fdm_print",
        "resin_print",
        "powder_print",
        "metal_print",
        "elastomer_print",
        "cnc",
        "sheetmetal",
        "laser_cut",
        "decal",
        "print",
        "apparel"
      ],
      "type": "string",
      "description": "What to fabricate. If the user just drops a file and asks for a price, OMIT this — UFP detects the file type (artwork, mesh — STL/OBJ/PLY/3MF/AMF, BREP — STEP/IGES, DXF) and routes it to every process that can make it; the response's routing.also_possible lists processes UFP can't quote yet"
    },
    "product": {
      "type": "string",
      "description": "print: Print product kind: business_cards|flyers|posters. Omit to quote every kind the artwork fits; apparel: apparel: t_shirt|hoodie|long_sleeve|tank_top (free-form — \"tee\", \"crewneck\" resolve to the closest product)"
    },
    "shore_a": {
      "type": "number",
      "description": "Numeric Shore A durometer ask (10-95, e.g. 40). Resolved to the NEAREST stocked durometer with a substitution note — never a silent swap",
      "exclusiveMinimum": 0
    },
    "tapping": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "thread"
        ],
        "properties": {
          "axis": {
            "$ref": "#/properties/bends/items/properties/line/properties/p1"
          },
          "hole": {
            "type": "integer",
            "minimum": 1
          },
          "thread": {
            "type": "string",
            "maxLength": 32,
            "minLength": 1
          },
          "face_id": {
            "type": "integer"
          },
          "depth_mm": {
            "type": "number",
            "exclusiveMinimum": 0
          },
          "located_at_mm": {
            "type": "object",
            "required": [
              "x",
              "y",
              "z"
            ],
            "properties": {
              "x": {
                "type": "number"
              },
              "y": {
                "type": "number"
              },
              "z": {
                "type": "number"
              }
            },
            "additionalProperties": false
          },
          "drilled_diameter_mm": {
            "type": "number",
            "exclusiveMinimum": 0
          }
        },
        "additionalProperties": false
      },
      "maxItems": 200,
      "description": "Tapped (threaded) holes for a sheet-metal part — one record per hole the user wants threads cut into. `thread` is the callout in the user's words (\"M6\", \"M4 x 0.7\", \"1/4-20\") and is all that's required; when known, add the hole's center in the part's own mm frame (located_at_mm), its unit axis, the pre-tap drilled diameter, and depth_mm so the shop taps the right hole. Pass these when the user asks for threaded/tapped holes; vendors with a tapping service price them."
    },
    "vendors": {
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1
      },
      "maxItems": 16,
      "description": "Only these specific vendors/makers (\"just quote Slant 3D\") — an INCLUDE list of vendor names as they appear on offers. Friendly names accepted (\"A3D\", \"slant 3d\"); unmatched names are reported, never an error. Usually OMIT: pass it only when the user explicitly names the vendors they want"
    },
    "callouts": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "name",
          "value",
          "unit"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "unit": {
            "enum": [
              "mm",
              "in"
            ],
            "type": "string"
          },
          "value": {
            "type": "number"
          },
          "critical": {
            "type": "boolean"
          },
          "tolerance_mm": {
            "type": "number",
            "exclusiveMinimum": 0
          }
        },
        "additionalProperties": false
      },
      "maxItems": 50,
      "description": "Dimensions you read off the user's drawing that matter (\"hole_diameter 6.5mm ±0.1\"). The tightest tolerance prunes vendors that cannot hold it."
    },
    "deadline": {
      "type": "string",
      "description": "Latest acceptable delivery date, ISO YYYY-MM-DD"
    },
    "file_url": {
      "type": "string",
      "description": "Public URL of the design file — only when the design lives at a URL"
    },
    "material": {
      "type": "string",
      "description": "ONLY a material the user actually asked for — NEVER guess or default one (a silent \"pla\" on a bare file drop prunes every maker that does not stock it; OMIT instead and each process quotes across its stocked materials so every maker can bid). fdm_print: pla|petg|asa (free-form — \"abs\", \"polycarbonate\" asks resolve to the closest stocked filament with a substitution note); resin_print: standard|tough|high_temp|high_detail resin (free-form asks resolve to the closest supported resin); powder_print: Powder-bed nylon: nylon_12|nylon_12_smooth|nylon_11|nylon_12_gf|nylon_12_esd|nylon_12_white|nylon_pa2200|nylon_12_sls|tpu (free-form — \"PA12\", \"glass filled\" resolve to the closest stocked powder); elastomer_print: Jetted elastomer: tepu_30a|tepu_50a (free-form — \"TPU\", \"soft rubber\" resolve to the closest stocked durometer); cnc: aluminum_6061|aluminum_7075|stainless_304|titanium|brass|acetal|delrin|abs|nylon_pa6|peek|acrylic (free-form asks resolve to the closest supported stock); sheetmetal: mild_steel|aluminum_5052|stainless_304 (free-form — \"steel\", \"5052\", \"stainless\" resolve; unknown asks get closest-match offers). Omit to compare the defaults; laser_cut: acrylic|plywood|mdf|delrin|uhmw|hdpe|g10|carbon_fiber|mild_steel|aluminum_5052|stainless_304 (free-form — \"plexiglass\", \"birch\" resolve; unknown asks get closest-match offers); decal: vinyl|holographic|transparent|glitter (free-form — \"chrome\" etc. gets closest matches)"
    },
    "width_in": {
      "type": "number",
      "description": "Decal width in inches (default 3)",
      "exclusiveMinimum": 0
    },
    "food_safe": {
      "type": "boolean",
      "description": "Must be vendor-certified food safe"
    },
    "height_in": {
      "type": "number",
      "description": "Decal height in inches (default 3)",
      "exclusiveMinimum": 0
    },
    "high_temp": {
      "type": "boolean",
      "description": "Must tolerate sustained heat (vendor heat-resistance claim)"
    },
    "placement": {
      "type": "string",
      "description": "Print placement for apparel: front (default) | back | front_back"
    },
    "processes": {
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1
      },
      "maxItems": 8,
      "description": "Only quote these fabrication processes — a constraint that narrows the auto-routing fan. Friendly terms accepted (\"sheet metal\", \"3d printing\", \"cnc\", \"laser\"); unmappable terms are reported, never an error. Usually OMIT: auto-routing quotes every process that can make the file. Pass it only when the user explicitly limits the process"
    },
    "share_key": {
      "type": "string",
      "description": "Share key for a LOCKED part — the k=… value from the owner's share link (…/part/UFP-…?k=KEY). Required with part_number when the part is locked; omit otherwise"
    },
    "bend_count": {
      "type": "number",
      "description": "Number of bends in the design (bend lines must be in the file). Prices CNC bending; omit for flat parts",
      "exclusiveMinimum": 0
    },
    "quantities": {
      "type": "array",
      "items": {
        "type": "integer",
        "exclusiveMinimum": 0
      },
      "maxItems": 5,
      "description": "Quantities the user actually wants — pass [1] or [2] if that's the ask. Vendors with a higher minimum quote AT their minimum and the offer notes it (requested_quantity). Default: 25 for decals, 1 for prints; a part_number reorder defaults to the quantity last purchased"
    },
    "ships_from": {
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1
      },
      "maxItems": 8,
      "description": "Only vendors that ship from these countries/regions (\"only US vendors\", \"keep it domestic\") — an INCLUDE list. Friendly terms accepted (\"US\", \"Europe\", \"Germany\", \"China\", \"UK\"); unmappable terms are reported, never an error. Vendors whose origin is unknown or whose network is worldwide (origin not guaranteed) are excluded, each with a note saying why. Usually OMIT: pass it only when the user explicitly limits where vendors ship from"
    },
    "design_file": {
      "type": "object",
      "required": [
        "file_id",
        "download_url"
      ],
      "properties": {
        "file_id": {
          "type": "string"
        },
        "file_name": {
          "type": "string"
        },
        "mime_type": {
          "type": "string"
        },
        "download_url": {
          "type": "string"
        }
      },
      "description": "The design file itself (user-attached or generated image/STL). ChatGPT supplies {file_id, download_url} automatically when the user attached a file — just bind the attachment here.",
      "additionalProperties": false
    },
    "metal_alloy": {
      "type": "string",
      "description": "Metal AM alloy: stainless_316l|stainless_17_4ph|titanium_ti64|aluminum_alsi10mg|inconel_718|cobalt_chrome|tool_steel_h13|copper (free-form asks like \"Ti-6Al-4V\" resolve to the closest stocked powder)"
    },
    "part_number": {
      "type": "string",
      "description": "UFP part number from a previous paid order (UFP-… or part_…). Reorders: replaces design_file/file_url entirely; any other fields provided override the stored spec"
    },
    "ship_to_zip": {
      "type": "string",
      "pattern": "^\\d{5}(-\\d{4})?$",
      "description": "Destination US ZIP code, exactly as the user stated it. OMIT if the user has not given one — NEVER guess or invent a ZIP. Without it, prices come back all-in with shipping to an assumed central-US destination, flagged on routing.ship_to"
    },
    "submersible": {
      "type": "boolean",
      "description": "Must survive submersion in water"
    },
    "metal_finish": {
      "type": "string",
      "description": "Metal AM post-processing: as_printed|bead_blast|machined_critical|polished|heat_treated (as_printed keeps support witness marks)"
    },
    "requirements": {
      "type": "array",
      "items": {
        "type": "string",
        "maxLength": 120,
        "minLength": 1
      },
      "maxItems": 20,
      "description": "Verbatim context terms from the user — materials, finishes, certifications, tolerances (\"UV resistant\", \"anodized: red\", \"iso 13485\", \"tolerance: 0.1mm\"). UFP applies what it can and reports what it could not; unresolvable terms are logged for vendor sourcing. Do not interrogate the user to fill this — pass what they said."
    },
    "thickness_in": {
      "type": "number",
      "description": "sheetmetal: Sheet thickness in inches (e.g. 0.063 = 14 ga aluminum). Quoted at the nearest stocked thickness; omit for ~16 ga default; laser_cut: Nominal thickness in inches; quoted at the vendor's nearest stocked thickness. Omit for the material's common stock",
      "exclusiveMinimum": 0
    },
    "outdoor_rated": {
      "type": "boolean",
      "description": "Must survive outdoor exposure"
    },
    "powder_finish": {
      "type": "string",
      "description": "Powder post-processing: as_printed (raw powder surface) | vapor_polished | polished"
    },
    "max_budget_usd": {
      "type": "number",
      "description": "Hard budget cap in USD, all-in",
      "exclusiveMinimum": 0
    },
    "attachment_urls": {
      "type": "array",
      "items": {
        "$ref": "#/properties/file_url"
      },
      "maxItems": 10,
      "description": "Additional files for the SAME part — the PDF drawing next to the STEP, spec sheets, BOMs. Pass everything the user gave you; UFP works out which file is the part and which are supporting documents."
    },
    "design_data_url": {
      "type": "string",
      "pattern": "^data\\:",
      "description": "Inline design bytes as a data: URL (base64), for MCP clients that cannot pass a file reference"
    },
    "elastomer_finish": {
      "type": "string",
      "description": "Elastomer post-processing: as_printed (VCJ parts ship raw — the whole lane today)"
    },
    "metal_technology": {
      "type": "string",
      "description": "Metal AM process: laser_powder_bed (SLM/DMLS/LPBF)|binder_jet|ebm. Omit to let the vendor pick its default for the alloy"
    },
    "min_vendor_stars": {
      "type": "number",
      "maximum": 5,
      "minimum": 1,
      "description": "Only vendors whose displayed review rating is at least this many stars (1-5). Vendors with no reviews yet are excluded — the filter demands demonstrated quality"
    },
    "powder_technology": {
      "type": "string",
      "description": "Powder process: mjf (HP Multi Jet Fusion) | sls (laser sintering). Omit to let the vendor pick its default lane for the material"
    },
    "min_vendor_reviews": {
      "type": "integer",
      "description": "Only vendors with at least this many verified-purchase reviews",
      "exclusiveMinimum": 0
    },
    "proven_spec_orders": {
      "type": "integer",
      "description": "Only vendors with at least N completed orders for this exact process+material — \"a vendor that prints PC reliably\"",
      "exclusiveMinimum": 0
    },
    "elastomer_technology": {
      "type": "string",
      "description": "Elastomer process: vapor_cure_jetting (VCJ). Omit to let the vendor pick its default lane for the material"
    }
  },
  "additionalProperties": false
}
get_fabrication_quote annotations
{
  "readOnlyHint": false,
  "openWorldHint": true,
  "destructiveHint": false
}
refine_quote — Refine quote
Advertised by the server during the last successful anonymous check.

Narrow an existing quote with new constraints (destination ZIP the user just provided, deadline, outdoor/waterproof/heat/food-safety requirements, budget, vendor reliability, verbatim requirements terms, drawing callouts, a process filter — "no, I meant in sheet metal" → processes — a ship-from country filter — "only US vendors" → ships_from — or a vendor filter — "only show A3D Manufacturing" → vendors) — or CHANGE its spec: material, color, or quantities. Changing material/quantity is a spec change, not a narrowing — pass it via the material/color/quantities args, NOT as a requirements term; the affected lanes re-price with the new spec through warm vendor sessions. For side-by-side comparisons ("how does carbon steel compare?"), add a scenario label — labeled refines coexist instead of replacing each other (max 4). "Organize by" ("sort by cheapest", "soonest arrival on top", "most green first") is also this tool: sort_by for objective orders, sort_order + sort_label for orders you judged yourself from the previews — presentation only, instant, nothing re-prices. GOING BACK is also this tool: undo_last_step ALONE takes back exactly one refine step (the quote's steps[] names each step, newest first — the network restores the previous stored state exactly); clear removes whole constraints ("drop the deadline" → clear ["deadline"]); remove_requirements takes back individual requirements terms. Call with ONLY quote_id to re-read the quote (e.g. to pick up preview images that finished rendering).

refine_quote input schema
{
  "type": "object",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "required": [
    "quote_id"
  ],
  "properties": {
    "bends": {
      "anyOf": [
        {
          "type": "array",
          "items": {
            "type": "object",
            "required": [
              "line",
              "angle_deg",
              "direction"
            ],
            "properties": {
              "id": {
                "type": "string",
                "maxLength": 32,
                "minLength": 1
              },
              "line": {
                "type": "object",
                "required": [
                  "p1",
                  "p2"
                ],
                "properties": {
                  "p1": {
                    "type": "array",
                    "items": {
                      "type": "number"
                    },
                    "maxItems": 3,
                    "minItems": 3
                  },
                  "p2": {
                    "$ref": "#/properties/bends/anyOf/0/items/properties/line/properties/p1"
                  }
                },
                "additionalProperties": false
              },
              "source": {
                "enum": [
                  "detected",
                  "user"
                ],
                "type": "string"
              },
              "face_id": {
                "type": "integer"
              },
              "angle_deg": {
                "type": "number",
                "exclusiveMaximum": 180,
                "exclusiveMinimum": 0
              },
              "direction": {
                "enum": [
                  "up",
                  "down"
                ],
                "type": "string"
              },
              "radius_mm": {
                "type": "number",
                "exclusiveMinimum": 0
              }
            },
            "additionalProperties": false
          },
          "maxItems": 50,
          "description": "Sheet-metal bend lines, each as TWO POINTS in the part's own mm frame (flat DXF parts use z=0: a bend from (0,12.7) to (76.2,12.7) is p1 [0,12.7,0], p2 [76.2,12.7,0]). angle_deg is the target bend angle (90 = a right-angle flange), direction \"up\"/\"down\" is the fold direction relative to the flat pattern, radius_mm the requested inside radius (omit it — the shop resolves to its tooling and reports what it used). Pass these when the user articulates WHERE the bends go; a bare \"it has 2 bends\" is bend_count, not this."
        },
        {
          "type": "null"
        }
      ],
      "description": "New bend records for this part — a spec change: sheet lanes requote with exactly these bends (the list REPLACES the previous one, never patches it). Same shape as on get_fabrication_quote: each bend is two points in the part's own mm frame (flat DXF parts use z=0) plus angle_deg, direction up|down, and an optional requested radius_mm. Pass null to REMOVE every articulated bend"
    },
    "clear": {
      "type": "array",
      "items": {
        "enum": [
          "deadline",
          "budget",
          "outdoor_rated",
          "submersible",
          "high_temp",
          "food_safe",
          "reliability",
          "requirements",
          "processes",
          "ships_from",
          "vendors"
        ],
        "type": "string"
      },
      "minItems": 1,
      "description": "REMOVE constraints entirely (\"drop the deadline\", \"forget the budget\", \"no more food-safe requirement\"): list the constraints to clear and each is removed from the quote — the one deliberate widening besides undo. Clearing a deadline or budget answers instantly from held offers (near-misses convert back); clearing an attribute/reliability/requirements demand re-prices honestly. Same effect as [\"any\"] on the three filter args, extended to every constraint"
    },
    "color": {
      "type": [
        "string",
        "null"
      ],
      "description": "New color for this part (spec change — lanes requote). Colour WORDS only ('black', 'green', 'clear'); the network resolves them to each maker's stocked shades, dyes, filaments or coatings — a family word ('blue') brings back EVERY stocked shade of it, and the quote's color_shades_available lists them — never pass a material or a shade you invented. Pass null to REMOVE a previously asked color — back to vendor defaults"
    },
    "scale": {
      "type": "object",
      "required": [
        "x",
        "y",
        "z"
      ],
      "properties": {
        "x": {
          "type": "number",
          "maximum": 100,
          "minimum": 0.01
        },
        "y": {
          "type": "number",
          "maximum": 100,
          "minimum": 0.01
        },
        "z": {
          "type": "number",
          "maximum": 100,
          "minimum": 0.01
        }
      },
      "description": "Rescale the DESIGN ITSELF before re-quoting: per-axis stretch factors over the file's current geometry (\"make it 2x bigger\" = {\"x\":2,\"y\":2,\"z\":2}). Mesh designs are genuinely converted — the child quote prices a rescaled file; STEP/IGES designs keep their original file with the dimensions riding as callouts (pass those too). {1,1,1} is a no-op",
      "additionalProperties": false
    },
    "finish": {
      "type": [
        "string",
        "null"
      ],
      "description": "New surface finish (\"powder coat\", \"anodized\", \"bead blasted\") — a spec change: finish-capable lanes (sheet metal, CNC, metal print) requote WITH it; 3D-print lanes have no coating step, so they ignore the finish but keep the COLOR. A colored finish ask (\"black powder coat\") should set BOTH finish and color, so every lane lands on the buyer's color. Pass null to REMOVE a previously asked finish"
    },
    "inserts": {
      "anyOf": [
        {
          "type": "array",
          "items": {
            "type": "object",
            "required": [
              "insert",
              "thread"
            ],
            "properties": {
              "axis": {
                "$ref": "#/properties/bends/anyOf/0/items/properties/line/properties/p1"
              },
              "hole": {
                "type": "integer",
                "minimum": 1
              },
              "insert": {
                "enum": [
                  "nut",
                  "flush_nut",
                  "standoff",
                  "blind_standoff",
                  "stud"
                ],
                "type": "string"
              },
              "points": {
                "$ref": "#/properties/bends/anyOf/0/items/properties/line/properties/p1"
              },
              "thread": {
                "type": "string",
                "maxLength": 32,
                "minLength": 1
              },
              "face_id": {
                "type": "integer"
              },
              "depth_mm": {
                "type": "number",
                "exclusiveMinimum": 0
              },
              "length_mm": {
                "type": "number",
                "exclusiveMinimum": 0
              },
              "located_at_mm": {
                "type": "object",
                "required": [
                  "x",
                  "y",
                  "z"
                ],
                "properties": {
                  "x": {
                    "type": "number"
                  },
                  "y": {
                    "type": "number"
                  },
                  "z": {
                    "type": "number"
                  }
                },
                "additionalProperties": false
              },
              "drilled_diameter_mm": {
                "type": "number",
                "exclusiveMinimum": 0
              }
            },
            "additionalProperties": false
          },
          "maxItems": 200,
          "description": "PEM-style hardware inserts pressed into holes of a sheet-metal part — one record per hole. `insert` is the species (nut | flush_nut | standoff | blind_standoff | stud) and `thread` its size (\"M4\", \"1/4-20\"); both are required. When known, add length_mm (standoff/stud post length), the hole's center (located_at_mm, part's own mm frame), its axis, the drilled diameter, and `points` — the signed unit vector the insert's functional side faces (flip = negate). Pass these when the user asks for press-in nuts/standoffs/studs; vendors with a hardware service price them."
        },
        {
          "type": "null"
        }
      ],
      "description": "New hardware-insert records for this part — a spec change: sheet lanes requote with exactly these inserts (the list REPLACES the previous one, never patches it). Same shape as on get_fabrication_quote: species (nut|flush_nut|standoff|blind_standoff|stud) and thread required, hole geometry optional. Pass null to REMOVE every insert"
    },
    "sort_by": {
      "enum": [
        "recommended",
        "price",
        "unit_price",
        "total_price",
        "arrival",
        "rating"
      ],
      "type": "string",
      "description": "\"Organize by\": re-order the quote's offer cards (\"sort by cheapest\" → \"price\"; \"soonest arrival first\" → \"arrival\"; \"best rated\" → \"rating\"; \"total_price\" = part + cheapest shipping; \"recommended\" restores the default price+speed rank). PURE PRESENTATION — answers instantly on the same quote, never re-prices or drops offers, and the ordering sticks across re-reads, streaming, and later refines until changed"
    },
    "tapping": {
      "anyOf": [
        {
          "type": "array",
          "items": {
            "type": "object",
            "required": [
              "thread"
            ],
            "properties": {
              "axis": {
                "$ref": "#/properties/bends/anyOf/0/items/properties/line/properties/p1"
              },
              "hole": {
                "type": "integer",
                "minimum": 1
              },
              "thread": {
                "type": "string",
                "maxLength": 32,
                "minLength": 1
              },
              "face_id": {
                "type": "integer"
              },
              "depth_mm": {
                "type": "number",
                "exclusiveMinimum": 0
              },
              "located_at_mm": {
                "type": "object",
                "required": [
                  "x",
                  "y",
                  "z"
                ],
                "properties": {
                  "x": {
                    "type": "number"
                  },
                  "y": {
                    "type": "number"
                  },
                  "z": {
                    "type": "number"
                  }
                },
                "additionalProperties": false
              },
              "drilled_diameter_mm": {
                "type": "number",
                "exclusiveMinimum": 0
              }
            },
            "additionalProperties": false
          },
          "maxItems": 200,
          "description": "Tapped (threaded) holes for a sheet-metal part — one record per hole the user wants threads cut into. `thread` is the callout in the user's words (\"M6\", \"M4 x 0.7\", \"1/4-20\") and is all that's required; when known, add the hole's center in the part's own mm frame (located_at_mm), its unit axis, the pre-tap drilled diameter, and depth_mm so the shop taps the right hole. Pass these when the user asks for threaded/tapped holes; vendors with a tapping service price them."
        },
        {
          "type": "null"
        }
      ],
      "description": "New tapped-hole records for this part — a spec change: sheet lanes requote with exactly these taps (the list REPLACES the previous one, never patches it). Same shape as on get_fabrication_quote: thread callout (\"M6\", \"1/4-20\") required, hole geometry optional. Pass null to REMOVE every tap"
    },
    "vendors": {
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1
      },
      "maxItems": 16,
      "description": "Only these specific vendors/makers (\"only show A3D Manufacturing\", \"just Slant 3D and Fictiv\") — an INCLUDE list of vendor names as they appear on offers. A CONSTRAINT like processes: other vendors' offers drop instantly (nothing re-prices), further refines inherit the filter, and [\"any\"] clears it to show every vendor again. Friendly names accepted (\"A3D\", \"slant 3d\"); unmatched names are reported, never an error. This is THE way to answer \"show me everything vendor X offers\" — the filtered quote lists that vendor's complete lineup"
    },
    "callouts": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "name",
          "value",
          "unit"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "unit": {
            "enum": [
              "mm",
              "in"
            ],
            "type": "string"
          },
          "value": {
            "type": "number"
          },
          "critical": {
            "type": "boolean"
          },
          "tolerance_mm": {
            "type": "number",
            "exclusiveMinimum": 0
          }
        },
        "additionalProperties": false
      },
      "maxItems": 50,
      "description": "Dimensions you read off the user's drawing that matter (\"hole_diameter 6.5mm ±0.1\"). The tightest tolerance prunes vendors that cannot hold it."
    },
    "deadline": {
      "type": "string",
      "description": "Latest acceptable delivery date, ISO YYYY-MM-DD"
    },
    "material": {
      "type": [
        "string",
        "null"
      ],
      "description": "New material for this part (\"I need it in PLA\" → \"pla\"). A spec change: lanes requote with this material through warm vendor sessions; an explicit material here replaces the quote's material fan-out. Free-form — unstocked asks resolve to the closest stocked material with a substitution note. Do NOT pass material changes as requirements terms. Pass null to REMOVE a previously asked material (\"forget the aluminum ask\") — the ask comes off the record and the process-default material fan returns"
    },
    "quote_id": {
      "type": "string"
    },
    "scenario": {
      "type": "string",
      "maxLength": 40,
      "minLength": 1,
      "description": "Comparison lane label. A labeled refine sits SIDE BY SIDE with the quote's other offers instead of replacing them: refines with the same label replace that scenario, different labels coexist (max 4 per quote), and a refine WITHOUT a scenario resets the baseline and supersedes every lane. \"How does X compare to Y?\" = one labeled refine per scenario (e.g. scenario \"carbon steel\" + material \"carbon steel\", then scenario \"aluminum\" + material \"aluminum\"). Keep labels short and human, like \"carbon steel\""
    },
    "food_safe": {
      "type": "boolean"
    },
    "high_temp": {
      "type": "boolean"
    },
    "processes": {
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1
      },
      "maxItems": 8,
      "description": "Narrow this quote to specific fabrication processes (\"no, I meant in sheet metal\" → [\"sheet metal\"]). A CONSTRAINT, not a new quote: excluded lanes drop while warm vendor sessions survive, and further refines inherit the filter. Friendly terms accepted (\"sheet metal\", \"3d printing\", \"cnc\", \"laser\"); pass [\"any\"] to clear the filter and show every process again. When comparison scenarios are open and the user narrows the process, re-refine EVERY active scenario label with this same arg so all columns prune together"
    },
    "quantities": {
      "anyOf": [
        {
          "type": "array",
          "items": {
            "type": "integer",
            "exclusiveMinimum": 0
          },
          "maxItems": 5
        },
        {
          "type": "null"
        }
      ],
      "description": "New quantities the user wants (spec change — lanes requote at the new quantity). Same rules as on get_fabrication_quote: pass the real ask, never inflate it. Pass null to REMOVE a previously asked quantity — the quote returns to single-part pricing"
    },
    "ships_from": {
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1
      },
      "maxItems": 8,
      "description": "Only vendors that ship from these countries/regions (\"only US vendors\", \"just European shops\", \"keep it domestic\") — an INCLUDE list naming where the user DOES want vendors from. A CONSTRAINT like processes: non-matching vendors drop instantly (nothing re-prices), further refines inherit the filter, and [\"any\"] clears it to show vendors everywhere again. Friendly terms accepted (\"US\", \"Europe\", \"Germany\", \"China\", \"UK\"). Vendors whose origin is unknown or whose network is worldwide (a specific origin cannot be guaranteed) are excluded, each with an honest note. For \"nothing from X\" asks, pass the regions the user does want instead"
    },
    "sort_label": {
      "type": "string",
      "maxLength": 60,
      "minLength": 1,
      "description": "Short human wording for a sort_order, displayed on the board (\"most green first\") — required with sort_order, since only you know what your order means"
    },
    "sort_order": {
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1
      },
      "maxItems": 200,
      "minItems": 1,
      "description": "EXPLICIT card order for asks only you can judge (\"sort the most green to the top\"): look at the offers' preview images, decide the order yourself, and pass the offer ids first-on-top. Offers you leave out (including ones that stream in later) land below the listed ones in the default rank. Pass sort_label with it"
    },
    "ship_to_zip": {
      "type": "string",
      "pattern": "^\\d{5}(-\\d{4})?$",
      "description": "Destination US ZIP code, exactly as the user stated it — use this when the user provides their ZIP after a quote priced to the assumed central-US destination (routing.ship_to); the refined prices become theirs. NEVER guess or invent a ZIP"
    },
    "submersible": {
      "type": "boolean"
    },
    "color_shades": {
      "anyOf": [
        {
          "type": "array",
          "items": {
            "type": "string",
            "maxLength": 48,
            "minLength": 1
          },
          "maxItems": 64
        },
        {
          "type": "null"
        }
      ],
      "description": "Narrow the board to these color SHADES exactly as the quote lists them (color_shades_available / \"Colors on this board\") — instant, nothing re-prices, the same quote id comes back with only those shades; pass null to show every shade again. Never invent a shade name."
    },
    "requirements": {
      "type": "array",
      "items": {
        "type": "string",
        "maxLength": 120,
        "minLength": 1
      },
      "maxItems": 20,
      "description": "Verbatim context terms from the user — materials, finishes, certifications, tolerances (\"UV resistant\", \"anodized: red\", \"iso 13485\", \"tolerance: 0.1mm\"). UFP applies what it can and reports what it could not; unresolvable terms are logged for vendor sourcing. Do not interrogate the user to fill this — pass what they said."
    },
    "thickness_in": {
      "anyOf": [
        {
          "type": "number",
          "maximum": 1,
          "exclusiveMinimum": 0
        },
        {
          "type": "null"
        }
      ],
      "description": "New sheet thickness in INCHES (\"make it in 1/8\" → 0.125) — a spec change: sheet lanes requote at the nearest stocked gauge, flagging the substitution when the match isn't close. Pass null to REMOVE a previously asked thickness — back to the material's default gauge"
    },
    "outdoor_rated": {
      "type": "boolean"
    },
    "max_budget_usd": {
      "type": "number",
      "exclusiveMinimum": 0
    },
    "sort_direction": {
      "enum": [
        "asc",
        "desc"
      ],
      "type": "string",
      "description": "Flip a sort_by ordering: \"desc\" reverses the key's natural direction (\"most expensive first\" = sort_by \"price\" + \"desc\"). Only meaningful with sort_by"
    },
    "undo_last_step": {
      "type": "boolean",
      "description": "GO BACK EXACTLY ONE STEP (\"undo that\", \"go back\", \"put it back how it was\"): the network restores the previous quote's stored spec and constraints EXACTLY — never reconstructed from conversation — and re-prices where needed. Send it ALONE (no other args besides quote_id); combining it with changes is rejected. The quote's steps[] names what each undo would take back — steps[0] is the newest. Undo resets the baseline like any unlabeled refine, so open comparison scenarios close. Undoing the original quote is rejected with honest wording — relay it"
    },
    "min_vendor_stars": {
      "type": "number",
      "maximum": 5,
      "minimum": 1,
      "description": "Only vendors whose displayed review rating is at least this many stars (1-5); vendors with no reviews yet are excluded"
    },
    "min_vendor_reviews": {
      "type": "integer",
      "description": "Only vendors with at least this many verified-purchase reviews",
      "exclusiveMinimum": 0
    },
    "proven_spec_orders": {
      "type": "integer",
      "description": "Only vendors with at least N completed orders for this exact process+material — \"a vendor that prints PC reliably\"",
      "exclusiveMinimum": 0
    },
    "remove_requirements": {
      "type": "array",
      "items": {
        "type": "string",
        "maxLength": 120,
        "minLength": 1
      },
      "maxItems": 20,
      "minItems": 1,
      "description": "Take back INDIVIDUAL requirements terms (\"drop the anodized ask, keep the rest\"): each listed term is removed from the quote's requirements by normalized match, the others stay. Unknown terms are ignored, never an error. Re-stating the requirements list can never drop a term (terms merge as a union) — this is the removal channel"
    }
  },
  "additionalProperties": false
}
refine_quote annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "destructiveHint": false
}
preview_quote — Preview quote routing (soft quote)
Advertised by the server during the last successful anonymous check.
Advertised read-only

INTERNAL client rail — the routing half of refine_quote with NOTHING committed: takes the same arguments as refine_quote and answers how many distinct fabricators (and which process lanes) the network WOULD ask for the refined part, plus the exclusion notes, without creating a child quote, asking any vendor, superseding in-flight work, or logging demand. An expectation, never a promise — lanes can still decline once asked. Never call this to look at prices; it carries none. Use refine_quote to actually re-price.

preview_quote input schema
{
  "type": "object",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "required": [
    "quote_id"
  ],
  "properties": {
    "bends": {
      "anyOf": [
        {
          "type": "array",
          "items": {
            "type": "object",
            "required": [
              "line",
              "angle_deg",
              "direction"
            ],
            "properties": {
              "id": {
                "type": "string",
                "maxLength": 32,
                "minLength": 1
              },
              "line": {
                "type": "object",
                "required": [
                  "p1",
                  "p2"
                ],
                "properties": {
                  "p1": {
                    "type": "array",
                    "items": {
                      "type": "number"
                    },
                    "maxItems": 3,
                    "minItems": 3
                  },
                  "p2": {
                    "$ref": "#/properties/bends/anyOf/0/items/properties/line/properties/p1"
                  }
                },
                "additionalProperties": false
              },
              "source": {
                "enum": [
                  "detected",
                  "user"
                ],
                "type": "string"
              },
              "face_id": {
                "type": "integer"
              },
              "angle_deg": {
                "type": "number",
                "exclusiveMaximum": 180,
                "exclusiveMinimum": 0
              },
              "direction": {
                "enum": [
                  "up",
                  "down"
                ],
                "type": "string"
              },
              "radius_mm": {
                "type": "number",
                "exclusiveMinimum": 0
              }
            },
            "additionalProperties": false
          },
          "maxItems": 50,
          "description": "Sheet-metal bend lines, each as TWO POINTS in the part's own mm frame (flat DXF parts use z=0: a bend from (0,12.7) to (76.2,12.7) is p1 [0,12.7,0], p2 [76.2,12.7,0]). angle_deg is the target bend angle (90 = a right-angle flange), direction \"up\"/\"down\" is the fold direction relative to the flat pattern, radius_mm the requested inside radius (omit it — the shop resolves to its tooling and reports what it used). Pass these when the user articulates WHERE the bends go; a bare \"it has 2 bends\" is bend_count, not this."
        },
        {
          "type": "null"
        }
      ],
      "description": "New bend records for this part — a spec change: sheet lanes requote with exactly these bends (the list REPLACES the previous one, never patches it). Same shape as on get_fabrication_quote: each bend is two points in the part's own mm frame (flat DXF parts use z=0) plus angle_deg, direction up|down, and an optional requested radius_mm. Pass null to REMOVE every articulated bend"
    },
    "clear": {
      "type": "array",
      "items": {
        "enum": [
          "deadline",
          "budget",
          "outdoor_rated",
          "submersible",
          "high_temp",
          "food_safe",
          "reliability",
          "requirements",
          "processes",
          "ships_from",
          "vendors"
        ],
        "type": "string"
      },
      "minItems": 1,
      "description": "REMOVE constraints entirely (\"drop the deadline\", \"forget the budget\", \"no more food-safe requirement\"): list the constraints to clear and each is removed from the quote — the one deliberate widening besides undo. Clearing a deadline or budget answers instantly from held offers (near-misses convert back); clearing an attribute/reliability/requirements demand re-prices honestly. Same effect as [\"any\"] on the three filter args, extended to every constraint"
    },
    "color": {
      "type": [
        "string",
        "null"
      ],
      "description": "New color for this part (spec change — lanes requote). Colour WORDS only ('black', 'green', 'clear'); the network resolves them to each maker's stocked shades, dyes, filaments or coatings — a family word ('blue') brings back EVERY stocked shade of it, and the quote's color_shades_available lists them — never pass a material or a shade you invented. Pass null to REMOVE a previously asked color — back to vendor defaults"
    },
    "scale": {
      "type": "object",
      "required": [
        "x",
        "y",
        "z"
      ],
      "properties": {
        "x": {
          "type": "number",
          "maximum": 100,
          "minimum": 0.01
        },
        "y": {
          "type": "number",
          "maximum": 100,
          "minimum": 0.01
        },
        "z": {
          "type": "number",
          "maximum": 100,
          "minimum": 0.01
        }
      },
      "description": "Rescale the DESIGN ITSELF before re-quoting: per-axis stretch factors over the file's current geometry (\"make it 2x bigger\" = {\"x\":2,\"y\":2,\"z\":2}). Mesh designs are genuinely converted — the child quote prices a rescaled file; STEP/IGES designs keep their original file with the dimensions riding as callouts (pass those too). {1,1,1} is a no-op",
      "additionalProperties": false
    },
    "finish": {
      "type": [
        "string",
        "null"
      ],
      "description": "New surface finish (\"powder coat\", \"anodized\", \"bead blasted\") — a spec change: finish-capable lanes (sheet metal, CNC, metal print) requote WITH it; 3D-print lanes have no coating step, so they ignore the finish but keep the COLOR. A colored finish ask (\"black powder coat\") should set BOTH finish and color, so every lane lands on the buyer's color. Pass null to REMOVE a previously asked finish"
    },
    "inserts": {
      "anyOf": [
        {
          "type": "array",
          "items": {
            "type": "object",
            "required": [
              "insert",
              "thread"
            ],
            "properties": {
              "axis": {
                "$ref": "#/properties/bends/anyOf/0/items/properties/line/properties/p1"
              },
              "hole": {
                "type": "integer",
                "minimum": 1
              },
              "insert": {
                "enum": [
                  "nut",
                  "flush_nut",
                  "standoff",
                  "blind_standoff",
                  "stud"
                ],
                "type": "string"
              },
              "points": {
                "$ref": "#/properties/bends/anyOf/0/items/properties/line/properties/p1"
              },
              "thread": {
                "type": "string",
                "maxLength": 32,
                "minLength": 1
              },
              "face_id": {
                "type": "integer"
              },
              "depth_mm": {
                "type": "number",
                "exclusiveMinimum": 0
              },
              "length_mm": {
                "type": "number",
                "exclusiveMinimum": 0
              },
              "located_at_mm": {
                "type": "object",
                "required": [
                  "x",
                  "y",
                  "z"
                ],
                "properties": {
                  "x": {
                    "type": "number"
                  },
                  "y": {
                    "type": "number"
                  },
                  "z": {
                    "type": "number"
                  }
                },
                "additionalProperties": false
              },
              "drilled_diameter_mm": {
                "type": "number",
                "exclusiveMinimum": 0
              }
            },
            "additionalProperties": false
          },
          "maxItems": 200,
          "description": "PEM-style hardware inserts pressed into holes of a sheet-metal part — one record per hole. `insert` is the species (nut | flush_nut | standoff | blind_standoff | stud) and `thread` its size (\"M4\", \"1/4-20\"); both are required. When known, add length_mm (standoff/stud post length), the hole's center (located_at_mm, part's own mm frame), its axis, the drilled diameter, and `points` — the signed unit vector the insert's functional side faces (flip = negate). Pass these when the user asks for press-in nuts/standoffs/studs; vendors with a hardware service price them."
        },
        {
          "type": "null"
        }
      ],
      "description": "New hardware-insert records for this part — a spec change: sheet lanes requote with exactly these inserts (the list REPLACES the previous one, never patches it). Same shape as on get_fabrication_quote: species (nut|flush_nut|standoff|blind_standoff|stud) and thread required, hole geometry optional. Pass null to REMOVE every insert"
    },
    "sort_by": {
      "enum": [
        "recommended",
        "price",
        "unit_price",
        "total_price",
        "arrival",
        "rating"
      ],
      "type": "string",
      "description": "\"Organize by\": re-order the quote's offer cards (\"sort by cheapest\" → \"price\"; \"soonest arrival first\" → \"arrival\"; \"best rated\" → \"rating\"; \"total_price\" = part + cheapest shipping; \"recommended\" restores the default price+speed rank). PURE PRESENTATION — answers instantly on the same quote, never re-prices or drops offers, and the ordering sticks across re-reads, streaming, and later refines until changed"
    },
    "tapping": {
      "anyOf": [
        {
          "type": "array",
          "items": {
            "type": "object",
            "required": [
              "thread"
            ],
            "properties": {
              "axis": {
                "$ref": "#/properties/bends/anyOf/0/items/properties/line/properties/p1"
              },
              "hole": {
                "type": "integer",
                "minimum": 1
              },
              "thread": {
                "type": "string",
                "maxLength": 32,
                "minLength": 1
              },
              "face_id": {
                "type": "integer"
              },
              "depth_mm": {
                "type": "number",
                "exclusiveMinimum": 0
              },
              "located_at_mm": {
                "type": "object",
                "required": [
                  "x",
                  "y",
                  "z"
                ],
                "properties": {
                  "x": {
                    "type": "number"
                  },
                  "y": {
                    "type": "number"
                  },
                  "z": {
                    "type": "number"
                  }
                },
                "additionalProperties": false
              },
              "drilled_diameter_mm": {
                "type": "number",
                "exclusiveMinimum": 0
              }
            },
            "additionalProperties": false
          },
          "maxItems": 200,
          "description": "Tapped (threaded) holes for a sheet-metal part — one record per hole the user wants threads cut into. `thread` is the callout in the user's words (\"M6\", \"M4 x 0.7\", \"1/4-20\") and is all that's required; when known, add the hole's center in the part's own mm frame (located_at_mm), its unit axis, the pre-tap drilled diameter, and depth_mm so the shop taps the right hole. Pass these when the user asks for threaded/tapped holes; vendors with a tapping service price them."
        },
        {
          "type": "null"
        }
      ],
      "description": "New tapped-hole records for this part — a spec change: sheet lanes requote with exactly these taps (the list REPLACES the previous one, never patches it). Same shape as on get_fabrication_quote: thread callout (\"M6\", \"1/4-20\") required, hole geometry optional. Pass null to REMOVE every tap"
    },
    "vendors": {
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1
      },
      "maxItems": 16,
      "description": "Only these specific vendors/makers (\"only show A3D Manufacturing\", \"just Slant 3D and Fictiv\") — an INCLUDE list of vendor names as they appear on offers. A CONSTRAINT like processes: other vendors' offers drop instantly (nothing re-prices), further refines inherit the filter, and [\"any\"] clears it to show every vendor again. Friendly names accepted (\"A3D\", \"slant 3d\"); unmatched names are reported, never an error. This is THE way to answer \"show me everything vendor X offers\" — the filtered quote lists that vendor's complete lineup"
    },
    "callouts": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "name",
          "value",
          "unit"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "unit": {
            "enum": [
              "mm",
              "in"
            ],
            "type": "string"
          },
          "value": {
            "type": "number"
          },
          "critical": {
            "type": "boolean"
          },
          "tolerance_mm": {
            "type": "number",
            "exclusiveMinimum": 0
          }
        },
        "additionalProperties": false
      },
      "maxItems": 50,
      "description": "Dimensions you read off the user's drawing that matter (\"hole_diameter 6.5mm ±0.1\"). The tightest tolerance prunes vendors that cannot hold it."
    },
    "deadline": {
      "type": "string",
      "description": "Latest acceptable delivery date, ISO YYYY-MM-DD"
    },
    "material": {
      "type": [
        "string",
        "null"
      ],
      "description": "New material for this part (\"I need it in PLA\" → \"pla\"). A spec change: lanes requote with this material through warm vendor sessions; an explicit material here replaces the quote's material fan-out. Free-form — unstocked asks resolve to the closest stocked material with a substitution note. Do NOT pass material changes as requirements terms. Pass null to REMOVE a previously asked material (\"forget the aluminum ask\") — the ask comes off the record and the process-default material fan returns"
    },
    "quote_id": {
      "type": "string"
    },
    "scenario": {
      "type": "string",
      "maxLength": 40,
      "minLength": 1,
      "description": "Comparison lane label. A labeled refine sits SIDE BY SIDE with the quote's other offers instead of replacing them: refines with the same label replace that scenario, different labels coexist (max 4 per quote), and a refine WITHOUT a scenario resets the baseline and supersedes every lane. \"How does X compare to Y?\" = one labeled refine per scenario (e.g. scenario \"carbon steel\" + material \"carbon steel\", then scenario \"aluminum\" + material \"aluminum\"). Keep labels short and human, like \"carbon steel\""
    },
    "food_safe": {
      "type": "boolean"
    },
    "high_temp": {
      "type": "boolean"
    },
    "processes": {
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1
      },
      "maxItems": 8,
      "description": "Narrow this quote to specific fabrication processes (\"no, I meant in sheet metal\" → [\"sheet metal\"]). A CONSTRAINT, not a new quote: excluded lanes drop while warm vendor sessions survive, and further refines inherit the filter. Friendly terms accepted (\"sheet metal\", \"3d printing\", \"cnc\", \"laser\"); pass [\"any\"] to clear the filter and show every process again. When comparison scenarios are open and the user narrows the process, re-refine EVERY active scenario label with this same arg so all columns prune together"
    },
    "quantities": {
      "anyOf": [
        {
          "type": "array",
          "items": {
            "type": "integer",
            "exclusiveMinimum": 0
          },
          "maxItems": 5
        },
        {
          "type": "null"
        }
      ],
      "description": "New quantities the user wants (spec change — lanes requote at the new quantity). Same rules as on get_fabrication_quote: pass the real ask, never inflate it. Pass null to REMOVE a previously asked quantity — the quote returns to single-part pricing"
    },
    "ships_from": {
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1
      },
      "maxItems": 8,
      "description": "Only vendors that ship from these countries/regions (\"only US vendors\", \"just European shops\", \"keep it domestic\") — an INCLUDE list naming where the user DOES want vendors from. A CONSTRAINT like processes: non-matching vendors drop instantly (nothing re-prices), further refines inherit the filter, and [\"any\"] clears it to show vendors everywhere again. Friendly terms accepted (\"US\", \"Europe\", \"Germany\", \"China\", \"UK\"). Vendors whose origin is unknown or whose network is worldwide (a specific origin cannot be guaranteed) are excluded, each with an honest note. For \"nothing from X\" asks, pass the regions the user does want instead"
    },
    "sort_label": {
      "type": "string",
      "maxLength": 60,
      "minLength": 1,
      "description": "Short human wording for a sort_order, displayed on the board (\"most green first\") — required with sort_order, since only you know what your order means"
    },
    "sort_order": {
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1
      },
      "maxItems": 200,
      "minItems": 1,
      "description": "EXPLICIT card order for asks only you can judge (\"sort the most green to the top\"): look at the offers' preview images, decide the order yourself, and pass the offer ids first-on-top. Offers you leave out (including ones that stream in later) land below the listed ones in the default rank. Pass sort_label with it"
    },
    "ship_to_zip": {
      "type": "string",
      "pattern": "^\\d{5}(-\\d{4})?$",
      "description": "Destination US ZIP code, exactly as the user stated it — use this when the user provides their ZIP after a quote priced to the assumed central-US destination (routing.ship_to); the refined prices become theirs. NEVER guess or invent a ZIP"
    },
    "submersible": {
      "type": "boolean"
    },
    "color_shades": {
      "anyOf": [
        {
          "type": "array",
          "items": {
            "type": "string",
            "maxLength": 48,
            "minLength": 1
          },
          "maxItems": 64
        },
        {
          "type": "null"
        }
      ],
      "description": "Narrow the board to these color SHADES exactly as the quote lists them (color_shades_available / \"Colors on this board\") — instant, nothing re-prices, the same quote id comes back with only those shades; pass null to show every shade again. Never invent a shade name."
    },
    "requirements": {
      "type": "array",
      "items": {
        "type": "string",
        "maxLength": 120,
        "minLength": 1
      },
      "maxItems": 20,
      "description": "Verbatim context terms from the user — materials, finishes, certifications, tolerances (\"UV resistant\", \"anodized: red\", \"iso 13485\", \"tolerance: 0.1mm\"). UFP applies what it can and reports what it could not; unresolvable terms are logged for vendor sourcing. Do not interrogate the user to fill this — pass what they said."
    },
    "thickness_in": {
      "anyOf": [
        {
          "type": "number",
          "maximum": 1,
          "exclusiveMinimum": 0
        },
        {
          "type": "null"
        }
      ],
      "description": "New sheet thickness in INCHES (\"make it in 1/8\" → 0.125) — a spec change: sheet lanes requote at the nearest stocked gauge, flagging the substitution when the match isn't close. Pass null to REMOVE a previously asked thickness — back to the material's default gauge"
    },
    "outdoor_rated": {
      "type": "boolean"
    },
    "max_budget_usd": {
      "type": "number",
      "exclusiveMinimum": 0
    },
    "sort_direction": {
      "enum": [
        "asc",
        "desc"
      ],
      "type": "string",
      "description": "Flip a sort_by ordering: \"desc\" reverses the key's natural direction (\"most expensive first\" = sort_by \"price\" + \"desc\"). Only meaningful with sort_by"
    },
    "undo_last_step": {
      "type": "boolean",
      "description": "GO BACK EXACTLY ONE STEP (\"undo that\", \"go back\", \"put it back how it was\"): the network restores the previous quote's stored spec and constraints EXACTLY — never reconstructed from conversation — and re-prices where needed. Send it ALONE (no other args besides quote_id); combining it with changes is rejected. The quote's steps[] names what each undo would take back — steps[0] is the newest. Undo resets the baseline like any unlabeled refine, so open comparison scenarios close. Undoing the original quote is rejected with honest wording — relay it"
    },
    "min_vendor_stars": {
      "type": "number",
      "maximum": 5,
      "minimum": 1,
      "description": "Only vendors whose displayed review rating is at least this many stars (1-5); vendors with no reviews yet are excluded"
    },
    "min_vendor_reviews": {
      "type": "integer",
      "description": "Only vendors with at least this many verified-purchase reviews",
      "exclusiveMinimum": 0
    },
    "proven_spec_orders": {
      "type": "integer",
      "description": "Only vendors with at least N completed orders for this exact process+material — \"a vendor that prints PC reliably\"",
      "exclusiveMinimum": 0
    },
    "remove_requirements": {
      "type": "array",
      "items": {
        "type": "string",
        "maxLength": 120,
        "minLength": 1
      },
      "maxItems": 20,
      "minItems": 1,
      "description": "Take back INDIVIDUAL requirements terms (\"drop the anodized ask, keep the rest\"): each listed term is removed from the quote's requirements by normalized match, the others stay. Unknown terms are ignored, never an error. Re-stating the requirements list can never drop a term (terms merge as a union) — this is the removal channel"
    }
  },
  "additionalProperties": false
}
preview_quote annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "destructiveHint": false
}
poll_quote — Poll quote (board internal)
Advertised by the server during the last successful anonymous check.
Advertised read-only

INTERNAL — the offer board calls this to refresh itself while vendors finish pricing. Never call this yourself. The board you were given is already final for its turn; do not reach for refine_quote to look at it again either, that paints a duplicate board.

poll_quote input schema
{
  "type": "object",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "required": [
    "quote_id"
  ],
  "properties": {
    "quote_id": {
      "type": "string",
      "description": "Quote id the board is watching"
    }
  },
  "additionalProperties": false
}
poll_quote annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "destructiveHint": false
}
finalize_quote — Finalize quote (wait for remaining vendors)
Advertised by the server during the last successful anonymous check.
Advertised read-only

ONLY for the case where a quote response came back with an EMPTY board and told you to call this: waits for the vendors to answer (up to ~40s) and returns their offers as a full board. Call with just quote_id, immediately, and say nothing substantive before it. NEVER call it on a response that already listed offers — that board is final, and this would add nothing.

finalize_quote input schema
{
  "type": "object",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "required": [
    "quote_id"
  ],
  "properties": {
    "quote_id": {
      "type": "string",
      "description": "Quote id from the still-quoting response"
    }
  },
  "additionalProperties": false
}
finalize_quote annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "destructiveHint": false
}
create_checkout — Create checkout
Advertised by the server during the last successful anonymous check.

Create the order and get a hosted payment link for a chosen offer. Call this as soon as the user picks an offer + shipping speed. This tool takes NO personal details: never ask for — and never pass — a name, address, email, or phone; the secure payment page collects the shipping address, email, and phone (and billing) itself, even when the user volunteered them in chat. Returns checkout_url — AFN's order page (what is being made, by which shop, the all-in price) with a Continue to payment button to the secure hosted payment page; give the link to the user. UFP places the vendor order automatically after payment.

create_checkout input schema
{
  "type": "object",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "required": [
    "offer_id",
    "shipping_option_id"
  ],
  "properties": {
    "offer_id": {
      "type": "string"
    },
    "return_url": {
      "type": "string",
      "description": "Exact URL of the page the user is on right now, IF the client application knows it (a web app embedding this tool passes its own page URL). The post-payment page shows a 'Return to <site>' button pointing here. Omit when unknown — never invent one."
    },
    "coupon_code": {
      "type": "string",
      "description": "Coupon code, only if the user provided one (early-adopter codes from the AFN team). Case-insensitive. The discount appears on the payment page; a 100%-off code makes the total $0 with no card required. Never invent or guess a code."
    },
    "shipping_option_id": {
      "type": "string",
      "description": "One of the offer's shipping option ids"
    }
  },
  "additionalProperties": false
}
create_checkout annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "destructiveHint": false
}
get_order_status — Get order status
Advertised by the server during the last successful anonymous check.
Advertised read-only

Check status, ETA and tracking for a UFP order.

get_order_status input schema
{
  "type": "object",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "required": [
    "order_id"
  ],
  "properties": {
    "order_id": {
      "type": "string"
    }
  },
  "additionalProperties": false
}
get_order_status annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "destructiveHint": false
}
reply_to_maker — Reply to the maker
Advertised by the server during the last successful anonymous check.
Advertised destructive

Send the user's answer to a question the maker (vendor) asked about a UFP order — get_order_status surfaces an open question when one is waiting. Confirm the answer with the user before sending. The network relays it: the maker sees plain text from the network account with the user's contact details and links stripped, so never include emails, phone numbers, or URLs the answer depends on.

reply_to_maker input schema
{
  "type": "object",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "required": [
    "order_id",
    "message"
  ],
  "properties": {
    "message": {
      "type": "string",
      "maxLength": 4000,
      "minLength": 1,
      "description": "The user's answer, in their words — it is relayed to the maker"
    },
    "order_id": {
      "type": "string",
      "description": "The UFP order the maker asked about (ord_…)"
    }
  },
  "additionalProperties": false
}
reply_to_maker annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "destructiveHint": true
}
leave_review — Leave vendor review
Advertised by the server during the last successful anonymous check.
Advertised destructive

Record the user's verified-purchase review of how a UFP order turned out. YOUR JOB IS TO STRUCTURE THE FEEDBACK: turn what the user actually said into stars (1-5 overall) and facet scores — "the cut was sloppy" -> quality, "parts arrived 2 days late" -> timeliness, "wrong color" -> accuracy. Keep the optional comment to a short factual summary of their words; never embellish. ALWAYS draft the review (stars, facets, comment) and confirm it with the user BEFORE calling this tool — e.g. "I'll rate this 2 stars with timeliness 1 because it arrived 2 days late — send it?". One review per order: submitting again updates the existing review. If you do not have the order id from this conversation, ask the user for it (it is on their receipt email and order page); never guess one.

leave_review input schema
{
  "type": "object",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "required": [
    "order_id",
    "stars"
  ],
  "properties": {
    "stars": {
      "type": "integer",
      "maximum": 5,
      "minimum": 1,
      "description": "Overall rating the user confirmed, 1-5"
    },
    "claims": {
      "type": "object",
      "properties": {
        "delivery_claim": {
          "enum": [
            "not_delivered",
            "late",
            "on_time"
          ],
          "type": "string",
          "description": "The user's delivery claim (\"never showed up\" -> not_delivered)"
        },
        "received_material": {
          "type": "string",
          "maxLength": 80,
          "description": "Material the user says actually ARRIVED, when they complain it differs from the order (\"they sent aluminum\")"
        },
        "received_quantity": {
          "type": "integer",
          "description": "Count the user says actually arrived, when it differs from the order",
          "exclusiveMinimum": 0
        }
      },
      "description": "CHECKABLE claims from the user's feedback — the network stamps each against its own records (the ordered spec, polled tracking + delivery events) and attaches evidence chips to the review. Only pass what the user actually asserted.",
      "additionalProperties": false
    },
    "facets": {
      "type": "object",
      "properties": {
        "quality": {
          "type": "integer",
          "maximum": 5,
          "minimum": 1,
          "description": "Build/print/cut quality"
        },
        "accuracy": {
          "type": "integer",
          "maximum": 5,
          "minimum": 1,
          "description": "How well the item matched the ordered spec/design"
        },
        "timeliness": {
          "type": "integer",
          "maximum": 5,
          "minimum": 1,
          "description": "Delivery speed vs the quoted ETA"
        }
      },
      "description": "Score only the facets the user actually spoke to",
      "additionalProperties": false
    },
    "comment": {
      "type": "string",
      "maxLength": 2000,
      "description": "Short factual summary of the user's feedback, in their spirit — optional"
    },
    "order_id": {
      "type": "string",
      "description": "The UFP order being reviewed (ord_…), from create_checkout or get_order_status in this conversation, or from the user's receipt email / order page"
    }
  },
  "additionalProperties": false
}
leave_review annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "destructiveHint": true
}
get_material_guide — Get material guide
Advertised by the server during the last successful anonymous check.
Advertised read-only

Honest, cited material intelligence for material-choice / strength / durability / heat / outdoor questions ("my PLA part broke, what's stronger?", "will this survive outdoors?"). Returns caveats-FIRST facts: published specs (tensile, heat-deflection, UV/outdoor, chemical) each tagged vendor-cited vs general engineering knowledge, "stronger/tougher/hotter than X" ladders (PLA→PETG→ABS/ASA→nylon/PC→metals), and finish/coating options. Call it BEFORE answering such a question, then relay the facts. It supplies FACTS ONLY — comparative specs and vendor-admitted caveats, never a fitness-for-purpose or safety guarantee (that stays your judgment boundary); name express vendor claims "advertised", not "certified".

get_material_guide input schema
{
  "type": "object",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "properties": {
    "process": {
      "type": "string",
      "description": "A UFP process to scope to (\"fdm_print\", \"resin_print\", \"cnc\", \"sheetmetal\", \"laser_cut\") — lists the materials that process makes"
    },
    "material": {
      "type": "string",
      "description": "A material the user named or you are weighing (\"PLA\", \"6061 aluminum\", \"stainless\") — free-form; a family ask like \"stainless\" returns the whole family"
    },
    "question": {
      "type": "string",
      "description": "The user's raw question — mined for intent (stronger/tougher, heat/hot, outdoor/UV) so the right ladder is surfaced"
    }
  },
  "additionalProperties": false
}
get_material_guide annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "destructiveHint": false
}
get_network_capabilities — Network capabilities
Advertised by the server during the last successful anonymous check.
Advertised read-only

The LIVE catalog of what the fabrication network can do RIGHT NOW — no quote needed. Returns the services ledger (tapping, inserts, welding, bending, finishes… with each shop's own size/material/thickness windows), the network's sheet gauge ladder, the stocked hardware combinations (species x thread) with stocked lengths and shop counts — the most-stocked length is the steering default for an unspecified length — the finish options with their stocked color menus, and per-process manufacturability rules. This is live state, not a spec sheet: it changes as connectors join, leave, or have capability branches toggled, so call it fresh when the user asks what's possible (materials, gauges, hardware, colors, services) rather than answering from memory.

get_network_capabilities input schema
{
  "type": "object",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "properties": {
    "process": {
      "type": "string",
      "description": "Advisory scope (\"sheetmetal\") — v1 returns the whole catalog either way; filter what you relay"
    }
  },
  "additionalProperties": false
}
get_network_capabilities annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "destructiveHint": false
}
get_shop_gauges — A shop's stocked sheet gauges
Advertised by the server during the last successful anonymous check.
Advertised read-only

Every sheet thickness (gauge) ONE named shop stocks right now, per material — read live off the shop's declared catalog, no quote needed. Call it when the user asks what thicknesses/gauges a specific maker carries ("what thicknesses does SendCutSend have in mild steel?"). For the whole network's ladder use get_network_capabilities instead. Never answer a gauge question from memory.

get_shop_gauges input schema
{
  "type": "object",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "required": [
    "shop"
  ],
  "properties": {
    "shop": {
      "type": "string",
      "minLength": 1,
      "description": "The shop as the user or the offer cards name it (\"SendCutSend\", \"sendcutsend\")"
    },
    "material": {
      "type": "string",
      "description": "Advisory: narrow the relayed list to one material (\"mild steel\", \"5052\")"
    }
  },
  "additionalProperties": false
}
get_shop_gauges annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "destructiveHint": false
}

Prompts

Not advertised

No prompt definitions available.

Resources

2 advertised

quote-compare-widget

Advertised by the server during the last successful anonymous check.

UFP offer comparison board

quote-compare-widget URI
ui://widget/quote-compare-v41.html

MIME type: text/html;profile=mcp-app

materials-intelligence

Advertised by the server during the last successful anonymous check.

UFP materials-intelligence pack — honest, cited material facts (caveats first), stronger/hotter/outdoor ladders, and a finishes/inspection glossary

materials-intelligence URI
ufp://materials/guide.md

MIME type: text/markdown

30-day availability

Aggregated from read-only remote checks; this is not a tool-execution or security guarantee.

Successful checks
1 of 1 (100%)
Average latency
272 ms
95th percentile latency
272 ms

Maintenance history

Public check and source-refresh events. Editorial and ownership records remain private.

  • · probe · passed · 272 ms · metadata changed · 12 tool(s), 2 resource(s), 0 resource template(s)

Connect from your client

Each snippet registers the same endpoint; pick the client you use.

This server is operated by the named third party. Listing and endpoint checks are not an endorsement or a security audit. Review the operator's permissions, terms, and data handling before connecting. How listings are checked and corrected.

Claude Code

Run this once in a terminal. It registers the remote server. Use --scope project to write .mcp.json for the repo.
Claude Code configuration
claude mcp add --transport http agentic-fabrication-network-mcp https://agenticfabricationnetwork.ai/mcp

Codex

Run this once. Codex stores MCP servers in ~/.codex/config.toml (or .codex/config.toml in a trusted project).
Codex configuration
codex mcp add agentic-fabrication-network-mcp --url https://agenticfabricationnetwork.ai/mcp

Cursor

Merge this server into .cursor/mcp.json in your project, then enable agentic-fabrication-network-mcp in your MCP settings.
Cursor configuration
{
  "mcpServers": {
    "agentic-fabrication-network-mcp": {
      "url": "https://agenticfabricationnetwork.ai/mcp"
    }
  }
}

Cline

Merge this into cline_mcp_settings.json (Cline → MCP Servers → Configure).
Cline configuration
{
  "mcpServers": {
    "agentic-fabrication-network-mcp": {
      "type": "streamableHttp",
      "url": "https://agenticfabricationnetwork.ai/mcp",
      "disabled": false
    }
  }
}

VS Code

Merge this server into .vscode/mcp.json. Use MCP: List Servers to start agentic-fabrication-network-mcp and inspect its tools.
VS Code configuration
{
  "servers": {
    "agentic-fabrication-network-mcp": {
      "type": "http",
      "url": "https://agenticfabricationnetwork.ai/mcp"
    }
  }
}

Gemini CLI

Run this once. It writes .gemini/settings.json in the current project; pass --scope user for ~/.gemini/settings.json.
Gemini CLI configuration
gemini mcp add --transport http agentic-fabrication-network-mcp https://agenticfabricationnetwork.ai/mcp

Claude Desktop

Open Claude Settings > Connectors > Add custom connector, enter this server URL and enable it for the conversation. This is a manual remote-connector handoff, not a desktop JSON configuration.
Claude Desktop configuration
https://agenticfabricationnetwork.ai/mcp

Windsurf

Merge this into ~/.codeium/windsurf/mcp_config.json, then refresh MCP in Cascade. Windsurf remote servers use serverUrl, not url.
Windsurf configuration
{
  "mcpServers": {
    "agentic-fabrication-network-mcp": {
      "serverUrl": "https://agenticfabricationnetwork.ai/mcp"
    }
  }
}

Let your agent find its tools

Find servers like Agentic Fabrication Network MCP from your agent. Add Koskamo once to search the index for endpoints, access requirements, and client setup. Public discovery needs no key.

Do you run Agentic Fabrication Network MCP?

Prove ownership with a DNS record, a well-known file, or your Official MCP Registry entry, and your edits publish immediately. Proofs are re-checked weekly; admins can still hide or remove a listing.

Claim this listing