X3DStudios

MCP server

X3D speaks Model Context Protocol at https://x3dstudios.com/api/mcp, so Claude, Claude Code and Cursor can quote a model, order a print and track it as tool calls instead of reading this page.

MCP (Model Context Protocol) is the standard way an AI model uses a service: the client asks the server what tools it has, gets typed schemas back, and calls them. X3D speaks it, which means an agent can price a model, place a print order and read the tracking number without a browser, without scraping this site and without anyone hand-writing tool definitions against our REST API.

Endpoint
POST https://x3dstudios.com/api/mcp

One URL. POST only, because that is what Streamable HTTP is: GET (the old event stream) and DELETE (the old session teardown) both answer 405, and there is no session id to carry. If your client wants to open a GET stream, it is speaking an older transport than this server does.

One tool here spends real money
create_print_order charges the card on the account and starts a print that cannot be un-printed. It refuses to run without a quote id from a previous quoting call and confirm set to true, so it cannot happen in one step by accident. Show the quote to a person and get a yes before you call it.

Add X3D to your client#

Two things go into the config: the URL, and your key as an Authorization header. You can leave the header out entirely and still read the materials and rates, price a weight you already know, check whether a file will print, read the current lead time and look up an order by its code, which is a reasonable way to try the server before signing up for anything.

Claude Code#

One command adds it. The name x3d is yours to choose; it is what the tools are prefixed with in the client.

From the terminal
claude mcp add --transport http x3d https://x3dstudios.com/api/mcp \
  --header "Authorization: Bearer $X3D_API_KEY"

Or put it in a project file, which is the better move for a repo where more than one person wants the server. Both files use the same shape.

Never commit the key itself
A project file is committed, and a key in it is then in your git history and on every machine that clones the repo, where rotating it is the only fix. Commit the URL and nothing else, and let each person add their own key in their own user-level config. If your client expands environment variables, ${X3D_API_KEY} in the header is the safer spelling.
{
  "mcpServers": {
    "x3d": {
      "type": "http",
      "url": "https://x3dstudios.com/api/mcp",
      "headers": {
        "Authorization": "Bearer x3d_live_YOUR_KEY_HERE"
      }
    }
  }
}
  • Claude Code requires "type": "http" on a remote server. An entry with a url and no type is treated as a configuration error and skipped.
  • Cursor identifies a remote server by the url alone and needs no type. The file is .cursor/mcp.json in a project, or ~/.cursor/mcp.json for every project.
  • Both files end up holding a live credential. Put them where you would put a .env, not in a public repository.

Claude on the web and in the desktop app#

The connector form has nowhere to put a key
Customize > Connectors > Add custom connector asks for a remote MCP server URL, and optionally an OAuth client id and secret. It does not take an arbitrary header. Add https://x3dstudios.com/api/mcp there and the public tools work; the account tools will keep answering that they need a key, because there is no way to send one. We do not support OAuth yet. Until we do, use Claude Code or Cursor for anything keyed.

Authentication#

POST/api/mcpNo auth

The MCP endpoint itself is public. Individual tools decide whether they need a key.

The key is the same x3d_live_ key the REST print API uses. Generate it at https://x3dstudios.com/profile, under Developer API key, once a card is on file. It is shown exactly once and stored only as a hash, so put it in your secret manager at the moment it is issued.

The header, on every request
Authorization: Bearer x3d_live_YOUR_KEY_HERE
  • A missing or unrecognised key is not a 401. The request succeeds, the caller is simply anonymous, and only the account tools notice.
  • An account tool called without a key returns a normal result with isError set and a sentence pointing at /profile. That is deliberate: as a protocol error the model would see "the connection failed" and give up, and the person would never learn what to do.
  • One key per account. Generating a new one at /profile invalidates the old one immediately, which is the fastest kill switch you have if an agent gets loose.
  • The key identifies the account whose card is charged and whose orders you can read. There are no scopes and no read-only key.

The tools#

Eleven tools. Five need no key and six take one. Nine of the eleven are free: only two spend anything, and both of them refuse to run without an explicit confirmation. tools/list returns all eleven tools to everybody, keyed or not, so an agent can tell its user what a key would unlock instead of finding out by failing.

ToolKeyCostWhat it does
list_materialsnofreeEvery stock material and its per-gram rate, the twelve shelf colours, what an off-palette colour costs, quality multipliers, quantity discounts, the per-part minimum, postage, the website's two print speeds and the largest build plate. Read live from the tables that charge the card.
estimate_pricenofreeArithmetic on a weight you already know. Grams, material, quality, quantity in, a full breakdown out. It never looks at a model.
check_printabilitynofreeDownloads a mesh and measures it: dimensions, watertightness, holes, non-manifold and degenerate geometry, plate fit, shallowest overhang. Keeps nothing.
get_lead_timenofreeHow soon work placed now would ship, from the state of the fleet this minute, for 1, 10, 100 and 1000 orders.
track_ordernofreeThe stage and tracking number of an existing order, given its code and the email it was placed with.
quote_printyesfreeThe real quote. Downloads the model, slices it to weigh it, returns the measurement, the full price and a quote_id that holds the price for 24 hours.
create_print_orderyesmoneyPlaces the order and charges the card on file. Needs a quote_id and confirm: true.
list_my_ordersyesfreeUp to 20 of this account's most recent orders, newest first, from every X3D order store.
get_orderyesfreeOne order on this account by its code.
generate_modelyescreditsTurns a text prompt into a printable model. Spends one AI generation credit and needs confirm: true. Asynchronous: returns a job_id.
get_generationyesfreeProgress of a generate_model job, and when it is done the model_url to hand to quote_print plus the printability verdict.
Order is fixed: the keyless tools first, then the keyed ones, and it never reshuffles between calls. Each entry also carries com.x3dstudios/cost and com.x3dstudios/auth in its _meta.

Public tools#

list_materials takes no arguments at all. Every tool on this page rejects an argument it does not know rather than ignoring it, so a misspelled field is an error you can see instead of a default you cannot.

Its shipping object carries domestic (standard and express, with rate, transit and freeOver), international (rate, freeOver and the list of countries), handlingDaysBeforeDispatch, and printSpeeds: normal and priority, each with its fee and shipsInBusinessDays. The print speeds describe the website checkout, where Priority print is 10% of the items, at least $10. An order placed through create_print_order cannot buy it and is queued as Normal print, so quote the normal figure, or better, call get_lead_time for the live one.

estimate_price
gramsnumberrequired
Filament weight of ONE part, not the batch. 0.1 to 50000.
materialstringoptionaldefault pla
Stock material.plapetgabsasatpu
qualitystringoptionaldefault standard
Layer height: standard is 0.2mm layers; premium is 0.1mm and costs 1.2x the per-gram rate.standardpremium
nozzlestringoptionaldefault standard
Nozzle size: standard is 0.4mm; premium is 0.2mm and costs 2.2x the per-gram rate, 2.5x with premium layers.standardpremium
quantityintegeroptionaldefault 1
1 to 10000. This tool is arithmetic, so it will price a run far larger than one order accepts.
colorstringoptional
Colour name or hex. A colour we do not stock is $0.20/g: call list_materials for the shelf.
check_printability
model_urlstringrequired
Direct https link to an .stl, .obj, .glb or .gltf file, 25 MB or under. It has to be a real download, not a viewer page and not anything behind a sign-in.
get_lead_time
gramsnumberoptional
Weight of one part. Heavier parts hold a printer longer. Omit it and you get the farm's standard order.
quantityintegeroptionaldefault 1
Parts in one order.
track_order
order_codestringrequired
An order number such as 2610014 (or #2610014) from the website, X3D-XXXXXXXX from the print API or /farm/submit, or an older CART-, ORD- or STORE- code from the web checkout.
emailstringrequired
The email the order was placed with, or one of the addresses the order copies on its emails. Both are required: a code with the wrong email is deliberately indistinguishable from a code that does not exist. After ten lookups that find nothing for one email in an hour, the tool answers not found without looking, unless your key belongs to that email.
track_order never returns an address
It answers with the stage, a one-line description of what was ordered, and tracking. Nothing else. It is the one tool that reads a real order with no key at all, so it hands back the least it can while still being useful. The stage is one of placed, queued, printed, shipped, delivered, picked_up (an order the customer collected) or cancelled. A print API order at ACCEPTED or PRINTING reads as queued, and one that was REJECTED as cancelled.

Account tools#

Every tool in this group needs the Authorization header. list_my_orders takes no arguments; the rest are below.

quote_print
model_urlstringrequired
Direct https link to an .stl, .glb or .gcode.3mf file, or a model_url X3D itself returned from get_generation. Unlike check_printability this one only accepts a link from a host we allow: a quote leads to a charge, so where a billable model may come from is the operator's decision. The refusal names the hosts we do take.
materialstringoptionaldefault pla
Stock material.plapetgabsasatpu
colorstringoptional
Filament colour. An off-palette colour is $0.20/g.
qualitystringoptionaldefault standard
Layer height: standard is 0.2mm layers; premium is 0.1mm (1.2x).standardpremium
nozzlestringoptionaldefault standard
Nozzle size: standard is 0.4mm; premium is 0.2mm (2.2x, 2.5x with premium layers). A sliced file prints on its own nozzle at the standard price.standardpremium
quantityintegeroptionaldefault 1
1 to 50, which is the cap on one order.
infillintegeroptionaldefault 20
10 to 100 percent.
countrystringoptionaldefault US
Two-letter ISO code of the delivery address. Postage is part of the total, $7 in the US and a flat $30 abroad, so the quote is only valid for the country it was priced for. Leave it out and you get a US price, said out loud in the result; create_print_order then refuses an address anywhere else against that quote rather than shipping abroad at the US rate.
create_print_order
quote_idstringrequired
From quote_print. It fixes the price, the file and every print option, so none of them is passed again here. A quote works once, belongs to one account, and expires 24 hours after it was made.
confirmbooleanrequired
Must be exactly true, and only after the person has seen that price and agreed to be charged.
recipient_namestringrequired
Who the parcel is addressed to.
emailstringrequired
The order's main contact, one address. It gets the confirmation and every update after it: printed, shipped with tracking, and delivered.
notify_emailsstring[]optional
Up to 4 more addresses, one per entry, that get a copy of every email about the order. Use it when someone besides the main contact is waiting on the part. Repeats and the address already in email are dropped. An entry that is not a valid address, or more than 4 entries, is refused before anything is charged.
street1stringrequired
Street address.
citystringrequired
City.
statestringrequired
State or province.
zipstringrequired
Postal code.
street2stringoptional
Second address line.
phonestringoptional
Contact number for the carrier.
countrystringrequired
Two-letter ISO code, and it must be the country the quote was priced for. There is deliberately no default: defaulting to US billed US postage on parcels that went abroad, and an agent that forgot the field had no way to find out. A country the quote was not priced for is refused, with the price difference explained, rather than charged at the wrong rate.
notestringoptional
Anything the operator should know, up to 1000 characters.
generate_model
promptstringrequired
What to build, in up to 500 characters.
confirmbooleanrequired
Must be exactly true. Spending a credit needs the person's yes.
qualitystringoptionaldefault draft
draft is fast and cheap; hifi runs the full pipeline.drafthifi
get_order and get_generation
codestringrequired
get_order: the order code. An order belonging to anyone else reports as not found.
job_idstringrequired
get_generation: the id generate_model returned. Free, and safe to poll.

Worked example: quote a file, then order it#

Two tool calls, with a person in between. Written out as curl so you can see exactly what a client sends; in Claude or Cursor you would just ask.

  1. 1
    Quote the file

    quote_print downloads the model, slices it for real, and prices it. Nothing is charged. Note the three Mcp-* headers: Mcp-Name has to equal params.name or the server answers 400.

    bash
    curl -s -X POST https://x3dstudios.com/api/mcp \
      -H "Content-Type: application/json" \
      -H "Accept: application/json, text/event-stream" \
      -H "Authorization: Bearer $X3D_API_KEY" \
      -H "MCP-Protocol-Version: 2026-07-28" \
      -H "Mcp-Method: tools/call" \
      -H "Mcp-Name: quote_print" \
      -d '{
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/call",
        "params": {
          "name": "quote_print",
          "arguments": {
            "model_url": "https://raw.githubusercontent.com/acme/parts/main/bracket.stl",
            "material": "pla",
            "color": "Black",
            "quality": "standard",
            "quantity": 1,
            "country": "US"
          },
          "_meta": {
            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
            "io.modelcontextprotocol/clientInfo": { "name": "acme-agent", "version": "1.4.0" },
            "io.modelcontextprotocol/clientCapabilities": {}
          }
        }
      }'
  2. 2
    Read the price out of the result

    content[0].text is the sentence to show a person. structuredContent is the same answer in fields: quote_id, expires_at, the measurement and the price breakdown. price.total is the whole thing including postage to price.ships_to, which is the country you asked for or US if you did not. Ordering to anywhere else needs a fresh quote, so if you do not have the address yet, get it before you quote. A .gcode.3mf whose plate uses five or more filaments also carries price.second_ams_fee, 25, once per order and already inside price.total. Postage is always the standard service: the MCP server does not sell express shipping or Priority print.

    json
    {
      "quote_id": "q_7f3c1a9e42",
      "expires_at": "2026-09-30T18:22:41.000Z",
      "model": { "file_name": "bracket.stl", "dimensions_mm": [62, 41, 18], "volume_cm3": 21.7 },
      "measurement": { "source": "slicer", "grams": 27.4, "print_hours": 1.31, "layers": 91 },
      "options": { "material": "pla", "color": "Black", "quality": "standard", "quantity": 1, "infill": 20 },
      "price": {
        "currency": "USD",
        "ships_to": "US",
        "print": 3.36,
        "shipping": 7,
        "total": 10.36,
        "billed_grams": 28,
        "note": "This total includes postage to US. create_print_order will only ship to US against this quote."
      }
    }
  3. 3
    Show it to the person and wait

    This is the only confirmation step that exists. Show the total, the material, the colour and the full delivery address as it will be printed, and wait for a yes. Nothing downstream asks again, and there is no cancel call.

  4. 4
    Place the order

    The price, the file and every print option come from the quote, so none of them is repeated. confirm: true is the record that somebody agreed. This charges the card on file. Here ada@example.com is the main contact and sam@example.com gets a copy of every email about the order. Leave notify_emails out when only one person needs them.

    bash
    curl -s -X POST https://x3dstudios.com/api/mcp \
      -H "Content-Type: application/json" \
      -H "Accept: application/json, text/event-stream" \
      -H "Authorization: Bearer $X3D_API_KEY" \
      -H "MCP-Protocol-Version: 2026-07-28" \
      -H "Mcp-Method: tools/call" \
      -H "Mcp-Name: create_print_order" \
      -d '{
        "jsonrpc": "2.0",
        "id": 2,
        "method": "tools/call",
        "params": {
          "name": "create_print_order",
          "arguments": {
            "quote_id": "q_7f3c1a9e42",
            "confirm": true,
            "recipient_name": "Ada Lovelace",
            "email": "ada@example.com",
            "notify_emails": ["sam@example.com"],
            "street1": "1 Main St",
            "city": "Austin",
            "state": "TX",
            "zip": "78750",
            "country": "US"
          },
          "_meta": {
            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
            "io.modelcontextprotocol/clientInfo": { "name": "acme-agent", "version": "1.4.0" },
            "io.modelcontextprotocol/clientCapabilities": {}
          }
        }
      }'
  5. 5
    Store the order code

    The result carries order_code, paid, amount, charged, status, status_url and notify_emails, the addresses copied on the order emails. Write the code down before doing anything else, and give the person the status URL. Read paid before you tell anyone the order went through: a declined card is a successful call and not an error, because the order row is real and waiting. On that result paid is false, charged is null, amount is what the order is for, and charge_outcome says what is known about the money. When it is "not-charged" the card was provably not charged, and payment_url is where the person pays or updates the card. When it is "unknown" the charge may have gone through with only its confirmation lost: payment_url is null, and you check status_url or get_order before saying anything about the money. Either way, do not call create_print_order again and do not quote again. The print is already ordered, and re-ordering would buy it twice.

    json
    {
      "order_code": "X3D-K7M2QP",
      "paid": true,
      "charged": 10.36,
      "amount": 10.36,
      "currency": "USD",
      "status": "RECEIVED",
      "status_url": "https://x3dstudios.com/print/order/X3D-K7M2QP",
      "payment_url": null,
      "quoted_total": 10.36,
      "notify_emails": ["sam@example.com"]
    }
Do not retry an order on a timeout
create_print_order is marked idempotentHint: false because it is not idempotent. There is no idempotency key, and a retry that succeeds is a second part and a second charge. A quote can only be spent once, which limits the damage to one retry, but the safe move on an ambiguous failure is to stop and call list_my_orders to see whether the order exists.

Rate limits#

Every tool call is counted against an hourly allowance for the caller. A keyed caller is counted on their account, so one key's allowance follows it across machines; an anonymous caller is counted on the address we saw, which is the one thing in the request they cannot choose. The allowance is set by what the call costs us to serve, not by which tool it is.

AllowanceCalls an hourWhich tools
reads300Everything that only reads: list_materials, estimate_price, get_lead_time, track_order, list_my_orders, get_order, get_generation. High enough to be invisible to anything but a loop.
file fetches40check_printability and quote_print. Both pull a file off the internet and slice it, which is CPU bound and the cheapest way to hurt us. quote_print also has its own cap of 30 quotes an hour per account, and its refusal reminds you a quote_id you already hold is good for 24 hours.
credits20generate_model.
money10create_print_order. No honest agent needs to place a hundred orders in an hour.
the endpoint600Every request of every kind, tools/list and ping included, counted before anything else runs. It exists so a caller cannot make us work for free by never calling a tool at all.
A refusal is HTTP 200 and a normal result with isError, naming the seconds to wait. Never a bare 429: most clients hide a 429 from the model, so the agent reports that the integration is broken instead of waiting. And unlike a timeout, "nothing was charged" is literally true here, because the call was refused before the handler ran.

Writing your own client#

Skip this section if you are using Claude or Cursor, which handle all of it. It matters if you are talking to the endpoint directly.

The current MCP revision, 2026-07-28, is stateless. There is no initialize call, no session id and no stream: protocol version, client info and client capabilities ride in params._meta on every request, and server/discover replaces the handshake. Most clients in the field still send initialize instead, so this server answers both and decides which era a request belongs to from the request itself.

What you sendEraWhat comes back
params._meta["io.modelcontextprotocol/protocolVersion"] exactly equal to 2026-07-28modernResults carry resultType: "complete". server/discover works. The Mcp-* headers are required, and so is params._meta["io.modelcontextprotocol/clientCapabilities"].
params._meta["io.modelcontextprotocol/protocolVersion"] naming an older revisionlegacyTaken at its word. Those revisions never defined resultType, so nothing adds one, and no Mcp-* header is required. One sent anyway is still compared against the body.
method: "initialize"legacyprotocolVersion, capabilities and serverInfo, with no resultType on anything afterwards. No Mcp-* header is required, and any that arrives is still compared against the body.
The era is read off each request, and the modern era is an equality test against 2026-07-28, not merely "a version arrived in _meta". Nothing is remembered between requests.

Supported versions: 2026-07-28, the stateless one, plus the handshake revisions 2025-11-25, 2025-06-18 and 2025-03-26. A request that states no version anywhere is treated as 2025-03-26 rather than refused, because the revisions up to 2025-06-18 made the header optional. A version outside that list is a 400 with code -32022 and a data object listing what we do speak.

What a modern request must carry in params._meta
io.modelcontextprotocol/protocolVersionstringrequired
2026-07-28, and it must equal the MCP-Protocol-Version header. Anything else here is read as an older revision and answered as legacy, which means no resultType on the result.
io.modelcontextprotocol/clientCapabilitiesobjectrequired
Required on every modern request, not just the first. Send {} if your client has none. A server must never rely on a capability the client did not declare, and an absent field is indistinguishable from an empty one unless it is refused, so a modern request without it is 400 with code -32602. Notifications are exempt: they are not requests.
io.modelcontextprotocol/clientInfoobjectoptional
Your client's name and version. Not enforced, and worth sending anyway: it is what appears in our logs when something about your calls needs explaining.
Headers a modern request must carry
MCP-Protocol-Versionstringrequired
Must equal the version in params._meta. The point of repeating it is that a proxy can read it without opening the body, so a disagreement is a hard failure rather than a shrug.
Mcp-Methodstringrequired
Must equal the body's method exactly.
(on every era)noteoptional
Only 2026-07-28 makes these headers mandatory, so only a 2026-07-28 request is refused for leaving one out. A header that does arrive is compared against the body whatever era the sender claims, because the proxy this check exists for does not care which revision the sender claims either. The one exemption is initialize: there the body's protocolVersion is a proposal to be negotiated rather than a statement of what is in use, so a client that also sends the header with its own preference is not contradicting itself.
Mcp-Namestringoptional
Required only for the methods that name a subject: params.name for tools/call and prompts/get, params.uri for resources/read. A non-ASCII value arrives wrapped as =?base64?VALUE?= and is decoded before it is compared.
Acceptstringoptional
Send application/json, text/event-stream. This server always answers with a single JSON object, but the spec asks clients to accept either.
StatusCodeWhen
400-32020A required Mcp-* header is missing on a modern request, or any Mcp-* header on any request disagrees with the body.
400-32022A protocol version we do not speak. data carries supported and requested.
400-32600Not a JSON-RPC object, wrong jsonrpc value, no method, or a batch array.
400-32700The body did not parse as JSON.
400-32602tools/call named a tool that does not exist. data.available lists the real names.
400-32602A modern request with no params._meta["io.modelcontextprotocol/clientCapabilities"], or one that is not an object. Send {}.
413-32600A body over 1 MB. The declared Content-Length is refused before a byte is read, and the read is capped as well, so a chunked body that declares nothing gets no further.
429-32600More than 30 unreadable bodies in an hour from one caller. A client with a bug still gets a readable error; something looping on garbage gets cut off.
404-32601A method this server does not implement. We advertise tools only.
403noneAn Origin header that is not on our allowlist. A missing Origin is fine: that is the server-to-server case.
405-32600GET or DELETE. There is no stream to open and no session to delete.
202noneAn accepted notification, meaning a request with no id. The body is empty.
Every response is sent with cache-control: no-store. A cached MCP response would serve one account's order to another.
A failed tool call is a success at the protocol level
A file that will not print, an expired quote, a missing key: these come back as HTTP 200 with a normal result carrying isError: true and the reason in content[0].text, not as a JSON-RPC error. Read the text and fix the argument. JSON-RPC errors are reserved for the protocol itself, and most clients hide them from the model, so a tool that reported a bad dimension that way would just look broken.

Each tool also carries our own facts in _meta on its tools/list entry: com.x3dstudios/cost is free, credits or money, and com.x3dstudios/auth is public or apiKey. MCP has no field for what a call costs, and a consent prompt is exactly where that belongs. Every modern result carries _meta["io.modelcontextprotocol/serverInfo"] as well, because with no handshake there is otherwise no request in which a stateless client learns who answered it.

How long a call gets depends on the work it does, not on its name. A tool that only reads, and generate_model, each get 25 seconds. create_print_order gets 45 seconds, because it is the one call where giving up early is worse than waiting: the expensive half has already committed. check_printability and quote_print get 90 seconds, because they pull a file off the internet and slice it for real, and the download alone is allowed 60. The route itself is allowed 120, so there is always room to send you a real answer instead of your own client timing out with nothing to show. One caveat for a caller coming through our public name: Cloudflare gives up at about 100 seconds, so a worst case slice answers only on the direct service host.

A timeout on a spending call is an unknown, not a failure
We stopped waiting; that does not stop the work. The result says so in as many words and tells you not to call it again. Where it sends you next depends on what the call could spend: create_print_order sends you to list_my_orders, or get_order if you already have a code; generate_model sends you to get_generation with the job id, or to your account page if no id came back, because no order tool can see a generation. Report it to the person as unconfirmed. Saying "it failed" about an order that exists is worse than saying you do not know.

Questions

Does X3D Studios have an MCP server?

Yes. POST https://x3dstudios.com/api/mcp. It is a Streamable HTTP MCP server on one endpoint, POST only, and it answers both the stateless 2026-07-28 revision and the older initialize handshake that most clients still send.

Do I need an API key to use the X3D MCP server?

Not to start. Materials and rates, price arithmetic on a weight you already know, whether a file will print, the current lead time and looking up an order by its code all answer with no key at all. A real quote needs one, because it downloads the model and slices it to weigh it, and so does anything that reads or changes one account's orders, models or credits: an x3d_live_ key sent as Authorization: Bearer, on an account with a card on file.

Can a tool call charge my card without asking?

Ordering takes two calls, and the second one needs a quote id from the first plus confirm set to true. There is no single call that goes from a file to a charge, and the ordering tool is marked destructive so a well-behaved client asks a person first.

Can I add the X3D MCP server to claude.ai as a connector?

You can add the URL, and the public tools work. The custom connector form asks for a server URL and optional OAuth credentials, not an arbitrary header, so there is nowhere to put an x3d_live_ key and the account tools stay out of reach. Use Claude Code or Cursor for those: both send headers.

Is there a sandbox for the MCP server?

No. There is one key prefix, x3d_live_, and one farm. An order placed through a tool call is a real order on a real printer charged to a real card. Quoting costs nothing, so quote as often as the hourly cap allows; only the order spends.

Related guides