Developer tools
Local MCP server
Run nace-mcp on your machine to give coding agents Nace's decisions, document processing, workspaces and search tools.
nace-mcp is the local MCP server for Nace. A coding agent such as Claude Code, Codex, OpenCode, Cursor, Pi or Hermes Agent starts it on your machine and talks to it over stdio. The server calls https://console.nace.ai with your API key through the Python SDK: decisions, document processing and search run in Nace's cloud.
Choose a server
| Server | Tools | Setup |
|---|---|---|
| Local MCP server (this page) | Decisions with text or images, document processing, uploads and jobs, saved schemas, workspaces, domains and search. | Run nace-mcp on your machine and add a Nace API key. Only the offline get_documentation tool works without a key. |
| Remote MCP server | decide for text or JSON state, and list_models. | Connect to Nace's hosted URL. No installation; an API key is optional within the free limits. |
Choose the local server when you need the broader tool set or local file access. For decisions without installing a package, use the remote server.
Quick start
You need a Nace API key, which starts with nace_sk_. In the dashboard, open API Keys and click Create key (see Create an API key). Then install the nace CLI, save the key once so the CLI and every agent find it, add the server, and install the agent skills:
uv tool install nace-cli # or: pipx install nace-cli
nace login # paste the nace_sk_ key; one sign-in for the CLI and the server
claude mcp add nace -- uvx nace-mcp # Claude Code
nace skill install --agent claude-code # or codex, cursor, pi, hermes, agents; see Agent skillsThe skills teach the agent to set up a workspace (drex-workspace) and to search it itself (drex-search), so install them with the server. Skills load when a session starts, so start a new one afterwards.
The skill, locate, read-file and run-sql commands need nace-cli 0.5.0 or later. Attachment uploads and the other MCP tools work without the CLI.
To put the key in the agent's config instead, as the NACE_API_KEY environment variable of the server, replace the claude mcp add line with the one below. Keep nace login: the skills run nace in the agent's shell, which doesn't see the server's config.
claude mcp add nace --env NACE_API_KEY=nace_sk_... -- uvx nace-mcpThen ask the agent for what you need, such as "extract the invoice number and total from ~/Downloads/invoice.pdf": it picks the tools and their arguments from the descriptions the server gives it. Add your API key compares the two ways to pass the key, Install and connect covers Codex, OpenCode, Cursor, Pi, Hermes Agent, Claude Desktop and other ways to install, and Tools lists what the agent can call.
Install and connect
The commands on this page use uvx, which comes with uv. It runs nace-mcp in an environment of its own, downloading it the first time. To install it once instead, run uv tool install nace-mcp (or pipx install nace-mcp) and use nace-mcp as the command, with no uvx. nace-mcp needs Python 3.11 or later. It checks TLS certificates against your operating system's certificate store, so a proxy certificate your system trusts works too.
The server doesn't put the nace command on your PATH. Install the nace CLI beside it: each agent's setup below ends with nace skill install, which writes the agent skills the agent uses to set up a workspace and search it itself:
uv tool install nace-cli # or: pipx install nace-cli
nace login # one sign-in for the CLI and the server
nace skill install --agent claude-code # or codex, cursor, pi, hermes, agents; see Agent skills| Command | What it does |
|---|---|
nace-mcp | Serve over stdio. Your agent runs it, not you. Stdout carries the protocol, so the server writes nothing else there; its messages go to stderr, which the agent keeps in its MCP log. |
nace-mcp login | Check an API key and save it. See Sign in. |
nace-mcp --version | Print the server and SDK versions. |
nace-mcp -h, --help | Print the usage and exit. |
nace-mcp 0.5.1
nace-sdk 0.7.0If an agent started from your desktop can't find uvx, put its full path (from which uvx) in command.
Add your API key
Every tool except get_documentation needs a Nace API key: nace_sk_ followed by 43 characters. Create one in the dashboard under API Keys; the dialog shows the full key once, so copy it then. Give it to the server in one of two ways.
| Way | How | Use it when |
|---|---|---|
| Save it once (recommended) | Run uvx nace-mcp login, or nace login if you have the CLI, and paste the key. Either checks the key and saves it to ~/.nace/config.toml, readable only by you. | Almost always. Every agent below finds it, so their configs need no key, and the nace commands your agent runs in its shell use the same key. |
| Put it in the agent's config (advanced) | Set NACE_API_KEY in the server's environment, as the examples below do with nace_sk_.... | You want a different key per agent or project, or the agent runs where you can't sign in. It reaches only the server: the nace commands the agent runs in its shell don't see it, so for them run nace login too. |
A key in the agent's config wins over the saved one. Replace nace_sk_... in the examples with your key, or leave the key out of them if you signed in.
The agent starts the server, so exporting NACE_API_KEY in your shell isn't enough on its own: an agent started from your desktop never sees your shell, and Hermes Agent passes a server only the variables its config names. To keep the key out of the config file, name the variable there and let the agent read it from its own environment: env_vars in Codex, ${env:NACE_API_KEY} in Cursor, {env:NACE_API_KEY} in OpenCode, ${NACE_API_KEY} in Pi and Hermes Agent. Don't write a key into a project config you commit, such as .mcp.json, .cursor/mcp.json, opencode.json or .pi/mcp.json: use the user-level file, nace-mcp login, or a variable reference.
Claude Code and Claude Desktop
claude mcp add nace --env NACE_API_KEY=nace_sk_... -- uvx nace-mcp
nace skill install --agent claude-code # the drex-search and drex-workspace skillsThat adds the server to the current project, with your key. Without --env NACE_API_KEY=..., the server uses the key nace-mcp login saved. claude mcp add --scope user nace --env NACE_API_KEY=nace_sk_... -- uvx nace-mcp adds it to every project. Run /mcp in Claude Code to see whether it connected.
Claude Desktop reads servers from claude_desktop_config.json (Settings, Developer, Edit Config) when it starts, so restart it after an edit:
{
"mcpServers": {
"nace": {
"command": "uvx",
"args": ["nace-mcp"],
"env": { "NACE_API_KEY": "nace_sk_..." }
}
}
}Leave out env if you ran nace-mcp login. The nace skill install --agent claude-code line above is for Claude Code only: Claude Desktop doesn't load those skills, and its agent searches with the server's tools, such as automatic_search and qa.
Codex
codex mcp add nace --env NACE_API_KEY=nace_sk_... -- uvx nace-mcp
nace skill install --agent codex # the drex-search and drex-workspace skillsLeave out --env NACE_API_KEY=... if you ran nace-mcp login. The command writes the server to ~/.codex/config.toml, which the Codex CLI, IDE extension and desktop app share. To edit the file yourself, or to add the server to one trusted project in its .codex/config.toml:
[mcp_servers.nace]
command = "uvx"
args = ["nace-mcp"]
[mcp_servers.nace.env]
NACE_API_KEY = "nace_sk_..."The env table is optional. To keep the key out of the file, drop the table and put env_vars = ["NACE_API_KEY"] under [mcp_servers.nace]: Codex then forwards NACE_API_KEY from the environment it runs in.
OpenCode
OpenCode reads servers from the mcp key of opencode.json, in the project root or at ~/.config/opencode/opencode.json for every project:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"nace": {
"type": "local",
"command": ["uvx", "nace-mcp"],
"environment": { "NACE_API_KEY": "nace_sk_..." },
"enabled": true
}
}
}Then install the skills where OpenCode reads them:
nace skill install --agent agents # the drex-search and drex-workspace skills, in ~/.agents/skillscommand holds the command and its arguments as one array, and environment holds the server's variables. Write "{env:NACE_API_KEY}" in place of the key to read it from the environment OpenCode runs in, or leave out environment if you ran nace-mcp login. opencode mcp list shows whether the server connected.
Cursor
Cursor reads servers from .cursor/mcp.json in a project, or ~/.cursor/mcp.json for every project:
{
"mcpServers": {
"nace": {
"type": "stdio",
"command": "uvx",
"args": ["nace-mcp"],
"env": { "NACE_API_KEY": "nace_sk_..." }
}
}
}Then install the skills where Cursor reads them:
nace skill install --agent cursor # the drex-search and drex-workspace skillsWrite "${env:NACE_API_KEY}" in place of the key to read it from your environment, or leave out env if you ran nace-mcp login. The server appears in Cursor's MCP settings, where a toggle turns it off without removing it.
Pi
Pi 0.99 and later connects to MCP servers on its own:
pi mcp add nace --exposure direct --env NACE_API_KEY=nace_sk_... -- uvx nace-mcp
nace skill install --agent pi # the drex-search and drex-workspace skills
pi mcp listpi mcp add writes the server to ~/.pi/agent/mcp.json, for every project; add -l to write the project's .pi/mcp.json instead, which Pi reads only once you trust the project. Leave out --env NACE_API_KEY=... if you ran nace-mcp login, or pass --env NACE_API_KEY='${NACE_API_KEY}', in single quotes so your shell doesn't expand it, to read the key from the environment Pi runs in. pi mcp list connects and prints each server's state and tools. In a running session, /reload picks up the change and /mcp shows whether the server connected. To edit the file yourself:
{
"mcpServers": {
"nace": {
"command": "uvx",
"args": ["nace-mcp"],
"env": { "NACE_API_KEY": "nace_sk_..." },
"exposure": "direct"
}
}
}Pi names the tools mcp__nace__decide, mcp__nace__parse_document and so on. By default it reaches MCP tools from its codemode scripts without showing them to the model; --exposure direct ("exposure": "direct" in the file) declares them to the model like built-in tools, with the descriptions the server gives, so the agent calls them directly instead of from scripts it writes. With the pi-mcp-adapter extension installed, which replaces Pi's own MCP support, put the same mcpServers entry in .mcp.json or ~/.config/mcp/mcp.json.
Hermes Agent
Hermes Agent reads servers from the mcp_servers key of ~/.hermes/config.yaml:
hermes mcp add nace --command uvx --env NACE_API_KEY=nace_sk_... --args nace-mcp
nace skill install --agent hermes # the drex-search and drex-workspace skills, in ~/.hermes/skills
hermes mcp test nacehermes mcp add connects to the server, lists its tools, asks which to enable, and then saves the server. --args must come last. Leave out --env NACE_API_KEY=... if you ran nace-mcp login. To edit the file yourself:
mcp_servers:
nace:
command: "uvx"
args: ["nace-mcp"]
env:
NACE_API_KEY: "nace_sk_..."args is a flat list of strings, ["nace-mcp"]: a list inside it, such as [["nace-mcp"]] or a YAML item - - nace-mcp, fails to connect with Input should be a valid string. Hermes gives a server only a few variables, such as PATH and HOME, plus the ones under its env, so a NACE_API_KEY or NACE_BASE_URL you export never reaches the server unless it is listed there. To keep the key out of config.yaml, put NACE_API_KEY=nace_sk_... in ~/.hermes/.env and write NACE_API_KEY: "${NACE_API_KEY}" under env; Hermes fills it in from that file or its own environment when it connects. A saved nace-mcp login key needs no env, since the server still finds ~/.nace/config.toml under HOME. Hermes names the tools mcp__nace__decide, mcp__nace__parse_document and so on. Run /reload-mcp in a running session to pick up a change.
Install the agent skills
Two agent skills ship with the nace CLI: drex-workspace sets a workspace up and drex-search searches it with the CLI's navigate tools. They're for Claude Code and other code agents, not Claude Desktop. Install them next to the server:
uv tool install nace-cli
nace skill install --agent claude-code # or codex, cursor, pi, hermes, agentsUntil the agent you're using has both skills in a folder it loads skills from, the server offers them on its first workspace or search call of a session. In an agent that shows MCP dialogs (elicitation), such as Claude Code, Codex, Cursor or Hermes Agent, you get a Yes/No dialog, and Yes runs nace skill install for that agent; Hermes Agent gets ~/.hermes/skills, and any agent it doesn't recognise the shared ~/.agents/skills folder. Elsewhere, such as Pi or OpenCode (1.19 doesn't show MCP dialogs), without nace on the PATH, or when the agent declines the dialog without showing it (Codex does under some approval settings), the agent is asked to suggest the command to you. nace workspace-create and nace workspace-use print the same suggestion until an agent has them. Skills load when a session starts, so start a new one after installing.
Sign in
nace-mcp login # type the key; it isn't echoed
nace-mcp login --api-key - < key.txt # or read it from stdin| Flag | What it does |
|---|---|
--api-key KEY | The nace_sk_ key. - reads the first line of stdin, or asks at a hidden prompt at a terminal. Omit KEY, or the whole flag, to type the key at a hidden prompt. |
nace-mcp login then:
- Checks that the key looks like a Nace API key:
nace_sk_followed by 43 characters. Anything else exits2before any request. - Checks it against Drex with
GET /v1/models, which is free at any balance. It asks$NACE_BASE_URL, else the base URL saved in the config file, elsehttps://console.nace.ai. A refused key exits1with the API's error. So does a host it can't reach, witherror: could not reach <host>: ... Check $NACE_BASE_URL (else base_url in <config path>) and your network; nothing was saved. - Saves it to
~/.nace/config.toml, readable only by you, and printssaved API key to <path>on stderr.
That is the file nace login writes, so signing in with either command signs in both. nace-mcp login has no --base-url: to save a custom or on-prem API root, use nace login --base-url, or set NACE_BASE_URL when running nace-mcp login. Login saves the actual validated URL with the key. Without --api-key and without a terminal, nace-mcp login exits 2: pass --api-key KEY or --api-key -. Errors go to stderr as error: ....
Credentials and settings
| Variable | What it does |
|---|---|
NACE_API_KEY | The API key. Wins over api_key in the config file. |
NACE_BASE_URL | The API host. Wins over base_url in the config file; without either, https://console.nace.ai. Use the key for that deployment; see below. |
NACE_ATTACHMENT_HOSTS | Comma-separated exact DNS hostnames that upload_attachment may download from. Empty by default, which refuses generic downloads; an invalid entry refuses them all. No wildcards, IP addresses, localhost, ports or URLs. It applies to the direct download_url input; native file references use their built-in host policy. See Upload an attachment. |
NACE_CONFIG_PATH | Where the config file is, instead of ~/.nace/config.toml. nace-mcp login uses it too. |
NACE_DEFAULT_DECISION_MODEL | The model decide asks when the agent names none. Default drex-latest. |
NACE_LOG_LEVEL | Log each request to stderr, as the CLI does. |
NACE_WORKSPACE_ID | The workspace a workspace tool uses when the agent passes no workspace_id. Wins over workspace_id in the config file. See Workspaces. |
NACE_API_KEY and NACE_BASE_URL work together for cloud, custom and on-prem deployments. Environment values override the corresponding saved values; setting both requires no login or profile. The legacy DREX_* names remain supported. Treat the API root as trusted configuration and select its matching key. URLs with user information are refused.
For an on-prem server, validate and save its key and URL together:
nace login --base-url https://nace.customer.example --api-key - < key.txtThe profile works for both the CLI and MCP, but is optional: the environment pair works directly. For unattended profile use, mount the mode-0600 file and set NACE_CONFIG_PATH to it. Environment values still take precedence over the saved configuration.
The names from before the packages were renamed from drex-* still work when the new ones are unset or blank: DREX_API_KEY, DREX_BASE_URL, DREX_CONFIG_PATH, DREX_DEFAULT_MODEL and DREX_LOG_LEVEL. So does ~/.drex/config.toml, read when ~/.nace/config.toml doesn't exist and no path variable is set. nace-mcp login always writes the new file.
The agent starts the server, so these come from the agent's environment, not your shell's: set them where Install and connect sets NACE_API_KEY for your agent, such as env in its JSON or --env on claude mcp add.
Without a key, the server still starts, and every tool except get_documentation, which makes no request, answers with an error: no API key: set $NACE_API_KEY or run `nace-mcp login` . It reads the credentials again on each call, so running nace-mcp login while the agent runs, to sign in or to replace a key Drex refuses, is enough: the next call uses it without a restart. A NACE_API_KEY in the server's environment wins over the saved key and changes only with a restart. An unreadable config file is reported the same way, unless NACE_API_KEY is set.
Tools
| Tool | What it does | Billing |
|---|---|---|
decide | Ask typed questions about a state and get calibrated probabilities. | Per input token |
list_models | List the models decide can use. | Free |
parse_document | Turn a document into Markdown, text or layout blocks. | Per page, in USD |
split_document | Cut a packet into page-range segments, each with one of your classes. | Per page, in USD |
classify_document | Label a document, or each page, with your classes. | Per page, in USD |
extract_data | Fill a JSON Schema, inline or saved, with a status and a citation for each field. | Per page, in USD |
ground_items | Find where each quote appears, with its exact location. | Per target, in USD |
upload_document | Upload a local file, into a workspace, or to the server without one. | Free |
upload_attachment | Transfer a remote attachment from a trusted HTTPS link or a native file reference. | Free |
delete_uploaded_file | Delete a file you uploaded, and everything derived from it. | Free |
get_job | Read a job and a summary of its result. | Free |
wait_for_job | Wait for a job to finish. | Free |
list_jobs | List your jobs, newest first. | Free |
cancel_job | Cancel a job, or drop it from the list. | Free |
get_job_request | Read the request a job ran under. | Free |
download_job_file | Save a file a job produced. | Free |
get_job_rows | Read a page of a parsed sheet's rows. | Free |
list_extraction_schemas | List saved extraction schemas. | Free |
get_extraction_schema | Read a saved schema, at its latest version or another. | Free |
list_extraction_schema_versions | List a saved schema's versions. | Free |
create_extraction_schema | Save a new extraction schema. | Free |
add_extraction_schema_version | Add a version to a saved schema. | Free |
delete_extraction_schema | Delete a saved schema with every version. | Free |
create_workspace | Create a workspace. | Free |
list_workspaces | List your workspaces. | Free |
get_workspace | Read a workspace and its settings. | Free |
workspace_stats | Summarize what a workspace holds. | Free |
use_workspace | Save a workspace as the default, for these tools and the nace CLI. | Free |
delete_workspace | Delete a workspace and everything in it. | Free |
list_domains | List the domains a workspace can classify its files against. | Free |
get_domain | Read a domain with its taxonomy. | Free |
create_domain | Create a draft domain of your own. | Free |
update_domain | Rename a draft or replace its taxonomy. | Free |
publish_domain | Freeze a draft so workspaces can use it. | Free |
duplicate_domain | Copy a domain as a draft you can edit. | Free |
list_workspace_files | List a workspace's files. | Free |
get_workspace_file | Read a file, with links to its bytes, its metadata and its versions. | Free |
delete_workspace_file | Delete a file from a workspace. | Free |
ingest_workspace | Make a workspace's files searchable. | TBD |
automatic_search | Answer one question from a workspace's files. | TBD |
qa | Ask one turn of a conversation about a workspace's files. | TBD |
get_documentation | Read the usage notes shipped with the server, by topic. | No request |
High effort increases the USD price: Parse costs twice its standard price, Split and Classify cost 50% more, and Extract costs one-third more. With a parse_job_id, the parse part is waived. The free tools work at any balance. See Pricing and Models & pricing in the console for USD rates. Pricing for ingest_workspace, automatic_search and qa is to be announced.
Ask questions
decide(state, questions, model=None, images=None) calls POST /v1/systemone: it asks named, typed questions about one state and returns a calibrated answer to each.
| Argument | What it is |
|---|---|
state | What every question is about: text, or any JSON value such as an object or an array, sent as it is. Media inside it (images, audio, video and PDFs) is refused with 422; send images in images. |
questions | An object of name to question, up to 512. The name keys the answer, so name it for what it decides (wants_refund, not q1). |
model | The model id. Default drex-max with images, else $NACE_DEFAULT_DECISION_MODEL, else drex-latest. An active pinned id stays fixed until retirement; retired ids resolve to their successor; list_models lists the models available to your API key in the target environment. |
images | Up to 8 images for Drex Max, which every question sees in order. Each is a local path, a data URL, or {workspace_id, file_id}. See Images. |
Each question has a type, optional instructions (the question, as text) and criteria:
type | criteria | Answer |
|---|---|---|
noul | Optional: {"true": "...", "false": "..."}, what counts as yes and as no. Needs instructions or a criterion. | noul: the probability of yes, from 0 to 1. |
choice | Required: an object of label to description, or to null. The answer is one of the labels, which have no order. | choice, confidence, and probabilities for each label. |
score | Required: an array of levels, low to high. A level's index is its score. | score (the expected level, which can fall between levels), confidence, legend and probabilities for each level. |
The server checks every question before sending and names the one it refuses, such as question 'topic' (choice): criteria: Field required. The agent passes the arguments as JSON:
{
"state": "I was charged twice for my March invoice.",
"questions": {
"wants_refund": { "type": "noul", "instructions": "Is the customer asking for a refund?" },
"topic": {
"type": "choice",
"instructions": "Which topic is it?",
"criteria": { "billing": "Payments, invoices or charges", "shipping": "Delivery and tracking", "other": null }
},
"urgency": { "type": "score", "instructions": "How urgent is it?", "criteria": ["low", "medium", "high"] }
}
}It gets back the model that answered, one answer per name, the usage and the request id:
{
"model": "drex-v2.0",
"answers": {
"wants_refund": { "type": "noul", "noul": 0.93, "confidence": 0.93 },
"topic": {
"type": "choice",
"choice": "billing",
"confidence": 0.88,
"probabilities": { "billing": 0.94, "shipping": 0.01, "other": 0.05 }
},
"urgency": {
"type": "score",
"score": 1.4,
"confidence": 0.5,
"legend": ["low", "medium", "high"],
"probabilities": { "0": 0.1, "1": 0.4, "2": 0.5 }
}
},
"usage": { "input_tokens": 61, "output_tokens": null },
"evaluation_time_ms": 143.0,
"request_id": "req_..."
}model is the pinned version that answered, even when the agent sent an alias. Ask every question about one state in one call: the state is billed once, per input token. The answers are probabilities, not verdicts, so pick a threshold by what each mistake costs. See Questions for what each type accepts and Reading answers for the answers.
Images
The served Drex Max models read images. Retired ids that resolve to Max v1.1 accept images too. Pass them in images and refer to them as "Image 1", "Image 2" and so on in the state or the instructions:
{
"state": "Image 1 is a scanned receipt. Image 2 is the shop's logo.",
"images": ["~/Downloads/receipt.jpg", "~/Downloads/logo.png"],
"questions": {
"same_shop": { "type": "noul", "instructions": "Was the receipt in Image 1 issued by the shop whose logo is Image 2?" }
}
}A local path of 256 KiB or less is sent inline, base64 in the request body. A larger file is first uploaded under drex-max-images/, named by its content so the same image isn't stored twice, and sent as a file reference. {workspace_id, file_id} names a file already there, such as what upload_document returned without a workspace_id; its result works as it is. A file in one of your Workspaces returns 422. More than 8 images, a missing file, or one that isn't a PNG, JPEG or WebP image is refused before anything is uploaded or sent.
With images and no model, decide sends the original Max id, which resolves to Max v1.1. Set model to a served id from Models to select it explicitly. Other served models return 422 for images. Images are at most 16 MiB each and 32 MiB in all, and usage.image_tokens counts their tokens, already included in input_tokens. The remote MCP server's decide takes no images.
List models
list_models() returns {models: [{name, alias_for, release_date, description}]}: the models the key can call, for decide's model. alias_for names the model an alias such as drex-latest serves, and is null for a pinned model. It is free at any balance. See Models.
Sources
The five document tools take the document as exactly one of these:
| Arguments | What it is |
|---|---|
url, with file_name | A public https:// link the document service downloads. See URLs. |
path, with workspace_path and on_conflict | A file on your machine, uploaded to the server first. See Local files. |
workspace_id and file_id | A file already in one of your workspaces: what upload_document returns, or a payload's uploaded. |
parse_job_id | One of your finished parse jobs, so the document isn't parsed and paid for again. Not on classify_document, which reads the original file. Not with page_ranges on parse_document or extract_data, parse_mode medium or high, or password. |
Two sources, or none, are refused before any request: pass exactly one source: url, path, workspace_id+file_id, or parse_job_id; got url, path. A text argument sent as an empty string counts as not sent; one with a fixed set of values, such as parse_mode or on_conflict, must be one of them or left out. workspace_id, file_id and parse_job_id must be UUIDs.
URLs
url must start with https://; an http:// link is refused. The document service picks the parser from the file name's extension, so a URL source carries a file_name. The server takes it from the URL's last path segment, without the query and percent-decoded, when that segment has an extension: https://example.com/files/Q3%20report.pdf gives Q3 report.pdf. When the segment has none, the call is refused before any request with can't tell the file type of ... and asks for file_name, such as "report.pdf".
Pass file_name too when the last segment only looks like it has an extension, as in https://arxiv.org/pdf/1706.03762. file_name applies only to a url source.
Local files
path is a file on the machine the server runs on: ~ is your home directory, and a relative path is relative to the directory the agent started the server in, so pass an absolute path when in doubt. The file is uploaded first, like upload_document without a workspace_id, to the server, then read from there. The payload's uploaded holds {workspace_id, file_id, path}: pass that workspace_id and file_id next time instead of uploading again.
| Argument | What it does |
|---|---|
workspace_path | The file's path in your workspace. Default: its file name. Keep the file's extension: the document service types an upload from its first bytes, and from this extension when those don't settle it. |
on_conflict | What happens when a different file already has that path. Left out, or reject, the upload fails with 409 path_conflict and nothing is created. new_version adds a version of that file, which jobs on it that haven't started yet then read. |
Uploading the same bytes to the same path again reuses the file already there. Both arguments apply only to a path source. Every argument the server can check is checked before the upload, so a refused call uploads nothing. When the create fails after the upload went through, the error names the workspace_id and file_id to pass to try again without uploading again. See Same path again.
Arguments every document tool shares
Besides its source, each of the five tools takes:
| Argument | What it does |
|---|---|
parse_mode | How much effort parsing takes: low (the default), medium or high, which increases the USD price (see Pricing). Not on ground_items. medium and high parse again, so not with parse_job_id. |
page_ranges | 1-based pages to read, inclusive, as a string: "3" or "1-5,8". Default: every page. At most 200 ranges and 500 pages. Not on ground_items, and not with parse_job_id on parse_document or extract_data. |
options | Any other request field, as an object. See Request fields. |
idempotency_key | Up to 200 characters. The same key and body return the same job and bill once; the same key with a different body is refused with 409 idempotency_conflict. Default: one minted per call. |
wait_seconds | Hold the create open up to this many seconds, 0 to 60 (default 30), so a short job comes back finished. See Waiting and payloads. |
max_inline_chars | Text or JSON in the result longer than this many characters goes to a file instead (default 20,000). See Spill files. |
A minted key is new on each call: the SDK's retries inside one call return the same job, but calling the tool again without a key creates, and bills, a new one. So when a create fails after a connection failure, a timeout, a 408, a 429 or a 5xx, such as 503 job_pending, the error names the key it carried: To try again, call the tool with idempotency_key="..." and the same arguments. Called again with that key, the tool returns the job if it was created instead of creating, and billing, another. After a path source, the error also names the uploaded file's workspace_id and file_id, to pass in place of path with that key. Pass your own idempotency_key to choose the key. See Retry safely.
Request fields
options goes into the request body as given, for the fields a tool has no argument for: {"name": "March invoices"} for the job's display name, {"project_id": "..."} (a UUID you mint to group jobs), or the tool's own options, such as {"output": {"table_format": "markdown"}}. A key that one of the tool's arguments sets, source included, is refused before any request: options can't set parse_mode: pass it as the tool's own argument instead. On parse_document, formats goes into output.formats, so options may set the other output fields; setting output.formats in options as well as formats is refused. A key named options inside options is a request field: Ground's own options object, as in {"options": {"max_matches": 5}}.
An argument the tool doesn't have is refused before any request, rather than dropped: parse_document has no argument page_range (did you mean page_ranges?), so nothing was sent. Its arguments: ..., followed by a pointer to options for any other request field.
Each tool's options are on its page: Parse, Split, Classify, Extract and Ground. The document service refuses a field it doesn't know, a misspelled one included, with 422.
Waiting and payloads
Each tool creates the job and holds the request open up to wait_seconds. A job that finishes by then comes back with its result in the same call. One still queued or running comes back with "instruction": "the job is still running; call wait_for_job with this job_id", and the agent then calls wait_for_job.
Every tool that returns a job returns the same payload:
{
"job_id": "6f1a2b3c-...",
"kind": "parse",
"status": "succeeded",
"credits": 3,
"usage_final": true,
"result": {
"result_type": "parse",
"file_name": "report.pdf",
"lane": "pdf",
"page_count": 3,
"ocr_applied": false,
"units": 3,
"markdown": { "text": "# Quarterly report\n...", "total_chars": 5120 }
}
}| Field | What it holds |
|---|---|
job_id | The id the job tools take. |
kind | The operation: parse, split, classify, extract or ground, or a workspace job: workspace_delete, file_delete, ingestion, intelligent_search (an automatic search) or automatic_agent (a QA turn). |
status | queued, running, succeeded, failed or cancelled. The last three are final. |
credits, usage_final | The credits the job used, final once usage_final is true. |
error | Why a failed or cancelled job ended: {code, message}. |
result | A summary of the result, made for the agent to read: see each tool below. get_job with full_result=True returns it as the API sent it. |
result_state | expired or not_retained when a finished job's result is gone. |
progress | While the job is queued or running, how far it got, when the document service says. |
instruction | While the job is queued or running, what to call next. |
uploaded | After a path source: {workspace_id, file_id, path}. |
Spill files
A rendering, such as a parse's Markdown or text, comes back as {text, total_chars}, or past max_inline_chars as {path, total_chars, head}: the text is written to a file, and head holds its first max_inline_chars characters. Any other text or JSON longer than that, such as a search's answer or a long list of blocks or segments, comes back as {"spilled": {path, total_chars, head}}, with the text in the file.
The files go in your system's temp directory, named nace-..., and only you can read them. The agent reads or searches them with its own tools. The server never deletes them.
Parse
parse_document turns a document into Markdown, plain text or layout blocks. Billed per page in USD; see Pricing.
| Argument | What it does |
|---|---|
formats | A list of markdown, text and blocks, sent as output.formats. Default: the API's markdown and blocks. |
password | An encrypted PDF's password. It is never stored or returned. Not with parse_job_id. |
The result holds file_name, lane, page_count, ocr_applied and units, then:
| Field | What it holds |
|---|---|
markdown, text | Each rendering you asked for, inline or in a spill file. |
content | For a very large document, the full Markdown in a file. See Large documents and workbooks. |
blocks, chunks, ocr | As the API sent them, or spilled when long. |
sheets | A workbook's sheets, each with sheet_name, row_count, column_count, content_url, markdown_deferred, rows_url and cell_map_url when it has them. |
links | Every other job file the result links, such as a transcript_url, for download_job_file. |
disclosures | Notes such as content_truncated. |
Pass the job_id as parse_job_id to split, extract or ground the same document without parsing, and paying for, it again. See Parse.
Large documents and workbooks
For a very large document, the result's markdown and text are only a preview, and document.content_url links the full content. See Very large results. The server then downloads the full Markdown into a new directory only you can read, and returns result.content instead of the preview: {path, bytes, format, head}, where format is markdown. It waits up to 60 seconds for content the document service is still preparing. If the download fails, you get the preview, with preview: true, the content_url, a content_error, and an instruction to save the full content later with download_job_file.
A workbook's sheets can link their own Markdown (content_url) and a cell map (cell_map_url): save them with download_job_file. Sheets carry content_url only when the workbook's rendering is over 1 MiB. Then markdown and text only preview them, and the server doesn't download them: the result says preview: true, with an instruction to save each sheet with download_job_file(job_id, content_url). A sheet with markdown_deferred: true is only its heading in the preview. A Parquet sheet's rows_url reads with get_job_rows.
Split
split_document cuts a packet of several documents into page-range segments, each with one of your classes. A workbook becomes one segment per sheet. Billed per page in USD; see Pricing.
| Argument | What it does |
|---|---|
classes | Required. [{"id", "label", "description"?}], with ids unique and without /. blank_page, unreadable_content and __ndi_unclassified are reserved ids, refused with 422. Other class fields, such as instance_key, go in as the API takes them. |
Its options, such as unknown_policy (include, the default, keeps pages no class matches as unclassified segments; force gives them the best class; error fails the job), overlap_policy, split_rules or {"output": {"include_content": true}}, go in options. The result's segments each carry id, sequence, start_page, end_page (and sheet_name for a sheet), status, class_id, class_label, classification_confidence and boundary_confidence, plus instance, content and artifacts when they have them. An unclassified segment has no class_id or class_label. Segment files, from {"output": {"materialize_files": true}}, save with download_job_file. See Split.
Classify
classify_document labels a document, or each page, with your classes. Billed per page in USD; see Pricing.
| Argument | What it does |
|---|---|
classes | Required. [{"id", "label", "description"?, "criteria"?: [str], "subclasses"?}], with ids unique and without /. criteria lists extra rules the model applies when scoring the class. other, blank_page, unreadable_content and __ndi_unclassified are reserved ids, refused with 422. |
granularity | document (the default) labels the whole file once; page labels each page. |
Classify reads the original file, so it takes no parse_job_id. Options such as unknown_policy (allow by default, or force_best) or {"output": {"max_alternatives": 3, "include_reason": true}} go in options. The result's units, one per document or page, each carry granularity, page_range, unknown and labels, best first: each with rank (1 is the class assigned), class_id, subclass_id when a subclass was given, label, confidence and reason. A label's confidence is the model's probability for that class, or the share of read pages given it when a label in the unit has no probability. Labels are ranked by the pages they cover, so a runner-up can have a higher confidence. The result also carries reason_status, unknown_units and pages_read_fraction. A running classify job can already hold a partial result, so wait for a final status. See Classify.
Extract
extract_data fills a JSON Schema from a document, with a status and a citation for each field. Billed per page in USD; see Pricing.
| Argument | What it does |
|---|---|
schema | The fields to extract, as an inline JSON Schema. |
schema_id | A saved schema (sch_...) instead, at its latest version. |
schema_version | With schema_id, use this version. |
instructions | Extra guidance for filling the declared fields. It can't add fields. |
Pass exactly one of schema and schema_id; neither, both, or schema_version without schema_id is refused before any request. A finished parse as parse_job_id is the cheapest source. Options such as {"citations": {"enabled": false}} go in options.
The result holds data, shaped by the schema, with null for a value not found. fields lists each field's path and status (found, not_found or ambiguous), plus confidence, citation_count and first_citation when it has them (a not_found field has none): first_citation is its location, without polygons, and up to 200 characters of source_text. field_counts counts the fields of each status, and schema_id, schema_version, units and warnings follow when the result has them. not_found is an answer, not an error. For every citation and polygon, read the job with get_job and full_result=True. See Extract.
Ground
ground_items finds where each quote appears in a document, with its exact location. Billed per target in USD; see Pricing.
| Argument | What it does |
|---|---|
targets | Required. 1 to 30 of {"id", "text", "hint"?, "sheet"?, "page_hints"?, "semantic"?}. hint is nearby text that picks the right occurrence, sheet a workbook tab, and page_hints the 1-based pages to search first. |
semantic | true sets "semantic": true on every target that doesn't set semantic itself; the request has no switch of its own. A semantic target's text describes what to find. On deployments that ground PDF, Word and PowerPoint sources with the exact-match runtime, the answering sentences, table cells, lines, blocks, sections or table rows come back, and if the picking model is unavailable, that target comes back failed with native_2_semantic_unavailable while the job still succeeds. Every hit has match_method semantic. Default false. |
Ground locates text; it doesn't check whether a claim is true. It reads the whole document, so it takes neither page_ranges nor parse_mode. Options such as {"options": {"max_matches": 5, "include_previews": true}} go in options.
The result's targets come in request order, each with id, status (found, not_found or failed, with an error), match_count and its first 3 matches. Each match has rank (1 is the place to use), matched_text, match_method (exact, normalized or semantic), confidence, location, and cropped_image_url with include_previews. The result also carries units, and transcript_url when the job made a transcript. Check matched_text and match_method before you trust a match. See Ground.
Uploads
Uploads are free and work at any balance. See Uploads.
Upload a file
upload_document(path, workspace_path=None, on_conflict=None, workspace_id=None) uploads a local file and returns {workspace_id, file_id, path}. Pass the workspace_id and file_id to any tool to work on the file without uploading it again. A tool given a path source uploads the file itself, so the agent needs upload_document only to upload once and then run several tools.
| Argument | What it does |
|---|---|
path | The local file, as in Local files. |
workspace_id | The workspace to upload into, to ingest and search the file there. Left out, the file is uploaded to the server as a source for the document tools; NACE_WORKSPACE_ID and the saved workspace don't apply. |
workspace_path | The file's path in the workspace. Default: its file name. Keep its extension. |
on_conflict | What happens when a different file already has that path. Left out, or reject, the upload fails with 409 path_conflict. new_version adds a version of that file, which jobs on it that haven't started yet then read. |
Uploading the same bytes to the same path again returns the file already there. A file of 32 MiB or more goes up in parts.
Upload an attachment
upload_attachment(download_url=None, file_name=None, workspace_path=None, on_conflict=None, workspace_id=None, file=None) transfers a remote attachment and returns {workspace_id, file_id, path}. Pass exactly one of download_url or file; empty download_url and file_name strings count as unset. Use the returned ids as a document tool's source, or ingest the file before searching its workspace. Uploading is free at any balance.
| Argument | What it does |
|---|---|
download_url | A direct public or signed HTTPS download link from any service whose hostname the operator trusts in NACE_ATTACHMENT_HOSTS. Omit when passing file. Never invent a URL or pass a sandbox path. |
file | A native attachment reference. ChatGPT supplies this through openai/fileParams: required download_url and file_id, optional file_name and mime_type, all strings. Select the attachment rather than constructing a reference. Omit when passing a direct download_url. mime_type is descriptive; the upload uses the file name's extension. |
file_name | Original file name with its extension; overrides file.file_name when nonempty. If absent or unusable, pass workspace_path with the extension. |
workspace_id | Destination workspace. Left out, the file is uploaded to the server; NACE_WORKSPACE_ID and the saved workspace don't apply. |
workspace_path | Path in the workspace, defaulting to the supplied file name. Required with the original extension when no usable file name is available. |
on_conflict | As on upload_document: omitted or reject refuses a different file at the same path; new_version adds a version. The same bytes at the same path return the existing file. |
For the direct download_url input, the operator must configure NACE_ATTACHMENT_HOSTS=files.example.com,downloads.example.org in the MCP server's environment. Entries are exact ASCII DNS hostnames with at least two labels; case and surrounding whitespace are normalized, and subdomains need their own entry. No URL syntax, ports, credentials, wildcards, IP addresses or localhost names. Empty or malformed configuration refuses direct URL downloads; callers cannot override the policy. A service that requires extra authentication headers cannot use this URL contract.
The native file input uses an internal adapter for HTTPS oaiusercontent.com and its true subdomains, independently of NACE_ATTACHMENT_HOSTS. Hosts and tunnels must forward both the file reference and the tool's openai/fileParams metadata. This preserves ChatGPT attachment support under the generic tool name; it does not add attachment transfer to hosts that lack that contract. An expired or refused link needs a fresh attachment reference from its service.
Both input forms share a 256 MiB/120-second download budget, HTTPS on port 443, no redirects, no Nace authentication or environment proxy/netrc credentials, and private temporary copies removed after success, failure or cancellation. The tool needs a nace-mcp release whose changelog lists it, so refresh a cached uvx install and reconnect to refresh the tool catalog.
upload_document(path=...) reads a file on the machine running nace-mcp. When that server runs on your Mac through a tunnel, a sandbox path such as /mnt/data/report.pdf is on another machine and cannot be read there. Use upload_attachment with a native file reference or a trusted direct download_url. The hosted Nace MCP server still provides only decide and list_models.
Delete a file
delete_uploaded_file(workspace_id, file_id) deletes a file you uploaded: every version of it and everything derived from it. It can't be undone. It returns {workspace_id, file_id, status}, and waits while the deletion runs: status is deleted once every trace is confirmed gone, or deleting if it is still running, and calling it again then reports on the same deletion.
| Argument | What it does |
|---|---|
workspace_id | The file's workspace_id, from upload_document or a payload's uploaded. Only your account's workspace; another is 404 workspace_not_found. |
file_id | The file's file_id. A file that isn't there is 404 file_not_found. |
Jobs
Reading, listing and cancelling jobs is free and works at any balance. See Jobs and results.
Read a job
get_job(job_id, max_inline_chars=20000, full_result=False) reads a job as it is now, without waiting, and returns the payload with a summary of its result. max_inline_chars is as on the tools. full_result=True returns the result exactly as the API sent it, every citation and match included, in a spill file when it is longer than max_inline_chars. A job of another account answers 404, like one that doesn't exist.
Wait for a job
wait_for_job(job_id, timeout=60, max_inline_chars=20000) reads the job every second at first, then less often, up to every 10 seconds, until it finishes or timeout seconds pass. timeout is 0 to 600; anything else is refused. It returns the payload either way:
- A job that finished,
failedandcancelledincluded, comes back with itsresultor itserror. A failed job is not a tool error. - When the time runs out, the job keeps running: the payload has its status,
queuedorrunning, and says"instruction": "the job is still running; call wait_for_job again".
List jobs
list_jobs(operation=None, status=None, cursor=None, limit=None) returns {items, next_cursor}: your jobs, newest first by when Drex received each request, without their results. created_at is when NDI created the job (the stored Drex admission time on a fallback item), so two jobs sent within a moment of each other can list a little out of created_at order. Each item has job_id, kind, status, credits, usage_final and created_at.
| Argument | What it does |
|---|---|
operation | Only jobs of one operation: parse, split, classify, extract or ground. |
status | Only jobs in one status: queued, running, succeeded, failed or cancelled. A page can then hold fewer than limit jobs, even none, while next_cursor says more follow. |
limit | Jobs per page, 1 to 50 (default 20). |
cursor | The next_cursor of the page before. Keep going until it is null. |
Cancel a job
Workspace/file deletion jobs and automatically chained child jobs cannot be cancelled through this tool (404).
cancel_job(job_id) cancels a queued or running document job or a recorded workspace processing job started directly by your account (read access to a shared workspace doesn't let you cancel another account's job; your account can still cancel its own after sharing ends or the workspace is deleted), and drops document jobs, finished or not, from list_jobs. It returns {job_id, deleted, message}. The job stays readable with get_job. A job cancelled before completion is not charged, including delivered parts; a finished one keeps its charge.
Job files
A result links the job's own files: a large document's full Markdown, sheets' Markdown and cell maps, figure crops, ground crops, transcripts, converted PDFs, split segment files and spreadsheet rows. See Files a job produced. Ordinary results and their files stay until you delete the job, and under Zero retention until their short window ends; after that, a call on a file fails with 404 or 410 result_expired, while get_job still reads the job, without its result.
Download a file
download_job_file(job_id, path, destination=None, wait_timeout=600) saves a file a job produced and returns {path, bytes}: where it went and its size.
| Argument | What it does |
|---|---|
job_id | The job. |
path | The link from the job's result, as it is, or the part after the job id, such as request. A query or fragment in it is ignored. A path such as .., or a link from another job's result, is refused before any request. |
destination | Where to save the file: a file, which is replaced if it exists, or an existing directory. Default: a new directory in your system's temp directory, named after the job, that only you can read. |
wait_timeout | How long to wait for content the document service is still preparing, in seconds (default 600). |
Without destination, or with a directory, the file is named <job id>.md for a document's full Markdown, else <job id>-<kind>[-<digest>] plus the kind's extension, as nace file names it. Stored files come through their signed link, without your API key; everything else streams from Drex.
Job request
get_job_request(job_id) returns the whole request the job ran under: request_type (the operation), source as the document service stored it, and the options, schema, classes or targets. A PDF password is never returned. Use it to see exactly what a job was asked, for example to run it again with a change. A request longer than 20,000 characters comes back as request_type and a spill file. It expires with the job's result.
Spreadsheet rows
get_job_rows(job_id, path, start_row=0, limit=50) reads one page of a parsed sheet's rows, in file order.
| Argument | What it does |
|---|---|
job_id | The parse job. |
path | The sheet's rows_url from the parse result (only Parquet sources have one), or the part after the job id. |
start_row | The first row, numbered from 0 (default 0). |
limit | Rows per page, 1 to 100 (default 50). |
It returns path, start_row, total_rows, columns (each with name and data_type), rows (each keyed by column name, spilled past 20,000 characters) and next_start_row. Pass next_start_row as start_row for the next page, until it is null.
Saved schemas
Save a JSON Schema once, then name it with schema_id on extract_data instead of sending it every time. Saved schemas are free and work at any balance. See Saved schemas.
List and read schemas
| Tool | What it returns |
|---|---|
list_extraction_schemas(cursor=None, limit=None) | {items, next_cursor}: each schema at its latest version, by schema_id, without its JSON Schema. Items carry schema_id, name, version, owner, created_at and updated_at, plus description when the schema has one. owner is account for yours and platform for those Nace provides. |
get_extraction_schema(schema_id, version=None) | One schema with its JSON Schema in schema: the latest version, or version. |
list_extraction_schema_versions(schema_id, cursor=None, limit=None) | {items, next_cursor}: the schema's versions, oldest first, each with its schema. |
Lists take limit, 1 to 200 (default 50), and cursor, the next_cursor of the page before; keep going until it is null. A JSON Schema longer than 20,000 characters comes back as a spill file. Another account's schema_id answers 404 schema_not_found.
Save a schema or a version
| Tool | What it does |
|---|---|
create_extraction_schema(name, schema, description=None) | Saves schema, a JSON Schema, as version 1 of a new schema called name, and returns it with its schema_id. |
add_extraction_schema_version(schema_id, schema, name=None, description=None) | Adds the next version to one of your schemas and returns it. Leave out name or description to keep the current one. |
A saved version can't be edited: add a version instead. An extract with schema_id uses the latest version unless it pins schema_version. Nace's own schemas can't take versions. Neither tool is retried, because a retry could save a second schema or version: after a failure, list the schemas or versions before calling it again.
Delete a schema
| Tool | Returns |
|---|---|
delete_extraction_schema(schema_id) | {schema_id, deleted, message} once one of your schemas is deleted with every version. |
A saved schema, with every version, is kept until you delete it or close your account. Extracts already accepted keep the copy of the schema they took and finish. Afterwards, reading it or naming its schema_id on extract_data answers 404 schema_not_found. Deleting it again succeeds. Nace's own schemas can't be deleted.
Workspaces
A workspace holds the files you upload into it, and what ingestion builds from them, so you can search them. Creating, reading, updating and deleting workspaces costs nothing and works at any balance. See Workspaces for what a workspace is, and the API reference for every field. The tools return each object as the API sent it, without its top-level null fields; null values inside access stay.
workspace_id is optional on every tool that works on one workspace, except three that need it: delete_workspace, use_workspace (it names the workspace to save) and delete_uploaded_file. Left out, the server uses NACE_WORKSPACE_ID from its environment, then the workspace_id that use_workspace or nace workspace-use saved in the config file, reading both again on each call. With neither, the tool is refused before any request with no workspace: pass workspace_id, set NACE_WORKSPACE_ID in the server's environment, or save one with use_workspace (or `nace workspace-use <id>`). .... Creating a workspace doesn't make it the default.
A typical run: create_workspace, use_workspace with its workspace_id, upload_document for each file, ingest_workspace once for the batch, then automatic_search or qa, or the agent's own search with the nace CLI (see Two ways to search).
Create a workspace
create_workspace(name, domain_slug=None, access=None, storage_connection_id=None) calls POST /v1/workspaces and returns the workspace, with its workspace_id, and next: a reminder to call use_workspace with that id so the nace CLI searches it by default. It doesn't save it as the default itself.
| Argument | What it does |
|---|---|
name | Required. 1 to 120 characters, unique in the account ignoring case. Rename it in the dashboard or through the API. |
domain_slug | The published domain the workspace sorts its files into: generic, one your account published, or one Nace provides. It can't be changed later. Default generic. |
access | Access labels: {"labels": [{"name", "description"}], "default_label"}, up to 16 labels, each name lowercase letters and digits joined by _; with labels, default_label is required and must be one of them. They can't be changed later. Default: none. See Create a workspace. |
storage_connection_id | Keep the workspace's files in your own bucket or container: a storage connection, set up in the dashboard or with nace storage, whose is_ready is true, else 422 storage_connection_not_ready. It can't be changed later, and never falls back to Nace's managed storage. Default: Nace's managed storage. |
What each setting does is in Create a workspace. A create isn't idempotent, so create_workspace is sent once and never retried: after a failure without an answer, check list_workspaces before calling it again, or you may create the workspace twice.
Use a workspace by default
use_workspace(workspace_id) reads the workspace, then saves its id as workspace_id in the config file, ~/.nace/config.toml, as nace workspace-use does. It returns the workspace with saved_to, the file's path. An id the key can't read is refused with 404 workspace_not_found, and nothing is saved.
The tools here then use it when the agent passes no workspace_id, and so do the nace commands the agent runs in its shell, such as nace locate, in this session and later ones. A NACE_WORKSPACE_ID in the server's environment still wins for the tools here; the reply's note says so. delete_workspace forgets the saved workspace when it deletes it.
List and read workspaces
| Tool | What it returns |
|---|---|
list_workspaces(name_contains=None, cursor=None, limit=None) | {items, next_cursor, total_count}: your workspaces, oldest first. items longer than 20,000 characters comes back as a spill file. |
get_workspace(workspace_id=None) | One workspace: its name, status and settings. |
workspace_stats(workspace_id=None) | What the workspace holds, with the workspace_id it read: files_total, bytes_total, files_by_ingestion_status, pages_ingested, index_documents, last_ingestion_at, next_expiry_at and files_expiring_within_7d. |
| Argument | What it does |
|---|---|
name_contains | Only workspaces whose name contains this, ignoring case. |
limit | Workspaces per page, 1 to 200 (default 50). |
cursor | The next_cursor of the page before. Keep going until it is null. |
workspace_id | The workspace, as in Workspaces. |
stale files in files_by_ingestion_status changed since they were ingested, and expired ones lost what ingestion built: ingest them again to make them searchable. A workspace that isn't yours, or was deleted, answers 404 workspace_not_found. See Workspace stats.
Delete a workspace
delete_workspace(workspace_id, confirm_name) deletes the workspace and everything in it: every file and version, and what ingestion built. It can't be undone. It returns the workspace_delete job's payload, usually still running, with the instruction to call wait_for_job.
| Argument | What it does |
|---|---|
workspace_id | Required. The workspace to delete. NACE_WORKSPACE_ID and the saved workspace never apply here. |
confirm_name | Required. The workspace's name, exactly. Anything else is refused with 422. |
The job's result has the counts of what was deleted (files_deleted, index_documents_deleted) and verified, true once the deletion is confirmed complete. The workspace answers 404 workspace_not_found as soon as the delete is accepted, so calling delete_workspace again gets that 404 and changes nothing. The tool takes no idempotency_key and doesn't wait: naming either is refused with that reason. If the document service had already let the workspace go, it returns {workspace_id, deleted: true, message}, with no job to read. Only the account's owners and admins, or whoever created the workspace, can delete it; anyone else gets 403. See Delete a workspace.
List and read workspace files
| Tool | What it returns |
|---|---|
list_workspace_files(workspace_id=None, path_prefix=None, ingestion_status=None, categories=None, file_types=None, modified_after=None, modified_before=None, cursor=None, limit=None) | {items, next_cursor, total_count, coverage}: the files that match, each with file_id, path, file_type, size_bytes, version, ingestion_status and more, with total_count across every page. coverage counts files the workspace's access labels kept from you. |
get_workspace_file(file_id, workspace_id=None, metadata=False, versions=False) | One file, with download_url and preview_url: links to its bytes that work without your API key until download_url_expires_at, 15 minutes on. |
| Argument | What it does |
|---|---|
workspace_id | The workspace, as in Workspaces. |
path_prefix | Only files whose path starts with this. |
ingestion_status | Only files in these statuses, a list of discovered (never ingested), queued, ingesting, ingested (searchable), stale (changed since), expired (retention dropped what ingestion built), failed and not_required (nothing to index). |
categories | Only files ingestion put in these categories. |
file_types | Only these extensions, such as pdf or xlsx. |
modified_after, modified_before | Only files changed after or before this ISO 8601 time. |
limit | Files per page, 1 to 200 (default 50). |
cursor | The next_cursor of the page before. Keep going until it is null. |
file_id | The file: the file_id that upload_document returned. A file that doesn't exist, or was deleted, answers 404 file_not_found. |
metadata | true adds metadata: what ingestion learned about the file, such as its summary, keywords, outputs, representations and last_error. See File metadata. |
versions | true adds versions: every version, newest first, following every page. |
A long list of items, metadata or versions comes back as a spill file past 20,000 characters.
Delete a workspace file
delete_workspace_file(file_id, workspace_id=None) removes the file and what ingestion built from it, and cancels ingestions still running on it. It can't be undone. It waits up to 30 seconds and returns the file_delete job's payload; the job's result says verified: true once every trace is gone.
| Argument | What it does |
|---|---|
file_id | The file to delete. |
workspace_id | The file's workspace, as in Workspaces. |
Ingest files
ingest_workspace(workspace_id=None, file_ids=None, path_prefix=None, stale_only=False, reingest_unchanged=False, idempotency_key=None, wait_seconds=30) reads files and builds what search needs: the search index, metadata and outputs. It's the only tool that makes files searchable: upload a batch, then ingest it in one call. It returns the ingestion job's payload. Pricing is to be announced. See Ingest files.
| Argument | What it does |
|---|---|
workspace_id | The workspace, as in Workspaces. |
file_ids | Ingest these files, 1 to 1000. Not with path_prefix. |
path_prefix | Ingest the files under this path. Leave out both for the whole workspace. |
stale_only | Only files that changed since they were last ingested. Not with file_ids. Default false. |
reingest_unchanged | Also rebuild files that are ingested and unchanged, which are otherwise skipped. Default false. |
idempotency_key | Retry the same selection with the same key to get the original job; use a new key for a new selection. Default: one minted per call. |
wait_seconds | Hold the request open up to this many seconds, 0 to 60 (default 30). |
file_ids with path_prefix, stale_only with file_ids, or more than 1000 file_ids is refused before any request. One bad file doesn't fail the run: the job succeeds, and its result counts files_ingested, files_skipped, files_empty and files_failed, with units_total and outcomes: each file's path, file_id, status (ingested, skipped_unchanged, skipped_empty or failed) and, when it failed, the error message.
Domains
A domain is the two-level taxonomy a workspace classifies its files against: a list of classes, each with a group, a subgroup, a description and aliases. The platform provides some and you can author your own. A draft is edited in place and published once; a published or platform domain can't change, so you duplicate it and edit the copy. A workspace uses a published domain by its slug: create_workspace(name, domain_slug=slug), fixed at creation. Domains cost nothing and work at any balance. See Workspaces for what a domain is, and the API reference for every field.
| Tool | What it returns |
|---|---|
list_domains(owner=None, status=None, slug=None, limit=None, cursor=None) | {items, next_cursor, total_count}: the domains you see, each with domain_id, slug, display_name, owner, version, status, class_count and selectable. |
get_domain(domain_id) | One domain with its taxonomy. A taxonomy too long to return inline is written to a file whose path is returned. |
create_domain(slug, display_name, taxonomy=None) | {domain, validation} for a new draft of yours, empty or with the taxonomy given. The slug can't change. |
update_domain(domain_id, display_name=None, taxonomy=None) | {domain, validation} for the draft as changed. |
publish_domain(domain_id) | {domain, validation} once the draft is frozen. One way. |
duplicate_domain(domain_id) | {domain, validation} for a draft copy of any domain you see, the platform's included, under a new slug. |
| Argument | What it does |
|---|---|
owner | Only platform or tenant (your own) domains. |
status | Only draft or published domains. Only a published one can be used to create a workspace. |
slug | Only the versions of this slug. |
limit | Domains per page, 1 to 200. |
cursor | The next_cursor of the page before. Keep going until it is null. |
domain_id | The domain, from list_domains or a write's domain.domain_id. |
slug (create) | Required. The name a workspace is created against: lowercase letters, digits and single underscores, up to 64. |
display_name | Required on create: the name people see. On update_domain, the new one. |
taxonomy | {"classes": [{"group", "subgroup", "description", "aliases"}]}. group and subgroup are lowercase letters, digits and single underscores, up to 64 characters each; aliases is optional. If a class doesn't follow these rules, the whole call is refused with 422 invalid_request, and nothing is saved. On update_domain it replaces the whole taxonomy: read it with get_domain, change it, and send all of it back. |
A field update_domain leaves out stays as it is, and a call with neither field is refused before any request. A published domain answers 409 domain_not_editable and a platform one 403: duplicate_domain it and edit the copy. A write's validation is {valid, errors}, each error a {code, path, message} naming the taxonomy entry that failed. publish_domain on a taxonomy that breaks a rule, such as one with no classes or with a class in the reserved group other, is refused with 422 domain_validation_failed, and the error lists each rule as code: message under its path (the first five, then a count): fix them with update_domain and publish again. A domain you can't see answers 404 domain_not_found.
create_domain, publish_domain and duplicate_domain are sent once and never retried, since a retry after the first landed would answer 409 or make a second copy: after a failure without an answer, check list_domains or get_domain before calling again.
Search
automatic_search and qa answer questions from the files in a workspace that are ingested; a file that isn't ingested isn't searched. Each creates a job and returns its payload; a job still running comes back with an instruction to call wait_for_job. Pricing is to be announced. Automatic searches are limited to 60 per account every 10 minutes: past that, a call is refused with 429 rate_limited, and the next one works once retry-after has passed. In the dashboard, see Search.
The payload's result is a summary in one shape, whether a quick fact search or the agent answered:
| Field | What it holds |
|---|---|
result_type | automatic_search, or automatic_agent for a qa turn. |
routing | automatic_search only: selected, the route chosen before the search ran or the one mode named, final, the route that answered, and reason. |
output_job_id | automatic_search only: the job that produced the answer: this job for a fact answer, the agent's own job otherwise. |
output_type | automatic_search only: intelligent_search for a fact answer, automatic_agent for the agent's. |
answer | The answer, as text. null when the agent asked back. |
structured_answer | With output_schema, the answer parsed as JSON; otherwise null. Read it rather than answer. If the answer still missed the schema, it's null and the text is in answer. |
confidence | certain, very_high, high, medium, low or speculative. |
reasoning | How the evidences add up to the answer, including any assumption it made. |
evidences | What it cites, most important first: each with path and file_id (the file), page and component (a sheet or section) when it has them, and the quote. |
grounding_job_ids | A fact answer's Ground jobs, already started, that locate each quote. Read them with wait_for_job; each result is a ground result. Don't call ground_items for those quotes. An agent answer has null: call ground_items on its cited files when you need locations. |
session_id | An agent answer's conversation, for a qa follow-up. |
clarification_questions | A qa turn that asked back: up to 2 of {question, options}, with an instruction saying how to reply. |
coverage | On a fact answer, restricted_candidates: matching files the search wasn't allowed to read. On an agent answer, one entry per thing the question asked for, with its status and missing_inputs. |
exhausted, sql_receipts | An agent answer: true when it ran out of steps first, so the answer is partial; and each SQL statement it ran, as {result_id, sql}. |
catalog | An agent answer that listed documents: columns, its first rows (each with path, file_id, entry_id, summary and cells of {field, status, value}, without citations), total_matches, total_files, and exhaustive, true only when every file in scope was fully read. |
answer, structured_answer, reasoning and coverage are as they are when they fit; past max_inline_chars, each comes back as {"spilled": {path, total_chars, head}}, a spill file. get_job with full_result=True returns the result as the API sent it, citations and all.
Two ways to search
automatic_search and qa hand the question to the document service's search agent, which answers with cited evidence in one call. Your agent can also search a workspace itself, with the nace CLI's nace locate, nace read-file and nace run-sql, to steer the search, check each page and use what it finds in its own work, and with nace folder-metadata and nace catalog for questions about the workspace as a whole. See Search or navigate tools for when to use which.
The server's instructions tell the agent both ways. For the second they point it to the drex-search agent skill, and to nace skill drex-search when it doesn't have the skill installed. The commands search the workspace use_workspace saved, else the account's only workspace; they don't see a NACE_WORKSPACE_ID set in the server's config, so the agent otherwise passes --workspace. When nace isn't on the agent's PATH, or says it has no API key, the agent is told to ask you to run uv tool install nace-cli (or pipx install nace-cli) and nace login, and to search with automatic_search meanwhile.
From a fresh machine:
uv tool install nace-cli && nace login.- Set the workspace up with the tools here, or in the dashboard, and have the agent call
use_workspacewith it. nace skill install --agent claude-code, or your agent's name (see Agent skills).- Start a new session and ask.
Automatic search
automatic_search(query, workspace_id=None, context=None, path_prefix=None, paths=None, top_k=None, mode=None, output_schema=None, idempotency_key=None, wait_seconds=30, max_inline_chars=20000) answers one question, with no follow-ups, from a quick fact search or from the agent, which reads documents, queries tables and lists matching documents. See Automatic search.
| Argument | What it does |
|---|---|
query | Required. The question, up to 4,096 characters. |
workspace_id | The workspace to search, as in Workspaces. |
context | Background the answer should take as given. Left out, none. |
path_prefix | Search only the files under this folder. |
paths | Search exactly these uploaded files too, by path. With path_prefix, the search covers both. |
top_k | How many evidences a fact answer cites, 1 to 50 (default 6). The agent ignores it. |
mode | auto (the default) lets the document service choose; fact runs fact search and agent the agent, with no routing step. |
output_schema | A JSON Schema the answer must follow, with an object at its root. The parsed answer is structured_answer. |
idempotency_key | Retry identical inputs with the same key to get the original job; use a new key for a changed question or mode. Default: one minted per call. |
wait_seconds | Hold the request open up to this many seconds, 0 to 60 (default 30). |
max_inline_chars | Text or JSON in the result longer than this many characters goes to a spill file instead (default 20,000). |
QA
qa(query, workspace_id=None, session_id=None, context=None, path_prefix=None, paths=None, allow_clarification=True, clarification_answers=None, output_schema=None, idempotency_key=None, wait_seconds=30, max_inline_chars=20000) asks one turn of a conversation. Every turn runs the agent, and the result carries the session_id to pass for a follow-up. See QA.
| Argument | What it does |
|---|---|
query | Required. The question, up to 4,096 characters. |
workspace_id | The workspace to search, as in Workspaces. |
session_id | Continue a conversation: an earlier turn's session_id. Leave it out to start one. While a turn is still running, the next is refused with 409 session_busy; only the API key that started a conversation can continue it. |
context, path_prefix, paths | As on automatic_search. Changing path_prefix or paths starts a fresh conversation, with a new session_id. |
allow_clarification | Let the agent ask back when a question is ambiguous. Default true. |
clarification_answers | Answers to the last turn's clarification_questions, one per question, in order: [{"question", "answer"}]. An answer of null skips a question. Needs session_id. |
output_schema | As on automatic_search, for this turn only: send it again on each turn, a clarification reply included, that should keep the shape. |
idempotency_key, wait_seconds, max_inline_chars | As on automatic_search. |
When the agent asks back, the turn succeeds with answer set to null, its clarification_questions, each with suggested options, and an instruction. Reply with the same query, the same session_id and clarification_answers, or ask a rephrased question. A turn that sends clarification_answers never asks again.
Usage notes for the agent
get_documentation(topic) returns {topic, text}: usage notes shipped inside the package, so the agent reads what the installed version does rather than what it remembers. Each topic ends with a link to its page in these docs.
topic | What it covers |
|---|---|
overview | What each tool does and costs. The tool's description tells the agent to read it first. |
decide | Question shapes, answers and limits. |
sources | url, path, workspace_id and file_id, parse_job_id, and uploads. |
parse, split, classify, extract, ground | Each tool's arguments, options and summarised result. |
jobs | Waiting, payloads, spill files, job files and rows. |
schemas | Saved extraction schemas. |
workspaces | Workspaces, their domains, files, ingestion, and search and QA. |
errors | What a failed call means and what to do next. |
Any other topic is refused with the list of valid ones.
Retries and timeouts
Each HTTP request may take up to 120 seconds, and a document create wait_seconds more. The server uses the Python SDK's default retry policy, as the CLI does: it retries a 408, a 429, a 5xx (unless the error says "retryable": false), a dropped connection or a timeout up to 2 times. There is no total retry budget by default, so a slow retryable failure can use the remaining retries and a failed tool call can take several minutes, including waits.
A document create carries an Idempotency-Key, so these retries never create a second job. Calling the tool again is a new call with a new key, though, unless the agent passes its own: see idempotency_key. After a failure worth retrying, a document tool's error names the key its create carried. create_extraction_schema and add_extraction_schema_version are never retried, because a retry could save a second schema or version. Nor is create_workspace, whose retry after a create that went through would make a second workspace. The workspace jobs (ingest_workspace, automatic_search and qa) carry a key the same way and name it after such a failure.
Handle errors
A failed call comes back as a tool error: text the agent reads, which the MCP library starts with Error executing tool <name>: . For an API error, the text is the status, the error's type and code as type/code (only the parts Drex sent), its message and the request id. An upload's error comes straight from the document service and has no type, as in 409 path_conflict: .... Up to five fields that failed validation follow, from the error's issues or detail.errors, then (+N more):
Error executing tool parse_document: 422 invalid_request_error/invalid_request: The request does not match the schema for this method. (request req_...)
- output.table_fromat: Extra inputs are not permittedSome errors add a line on what to do next:
| Error | The line added |
|---|---|
401 | Run `nace-mcp login` with a valid key: the server reads the saved key again on each call, so the next call uses it without a restart. Then that a NACE_API_KEY set in the server's environment wins over the saved key, so change it there and restart the server. |
402 insufficient_credit | The account has insufficient available funds: add funds or add a card at <your Drex host>/dashboard/billing, then call the tool again. |
402 payment_required | The account has an unpaid invoice: pay it at <your Drex host>/dashboard/billing, then call the tool again. A top-up doesn't lift it. |
409 path_conflict | To pass on_conflict="new_version", or another workspace_path to keep both files. |
| A create that failed after a local file was uploaded | The workspace_id and file_id to pass to try again without uploading the file again. |
A document create that failed with a 408, a 429 or a 5xx, or after a connection failure or timeout | To try again, call the tool with idempotency_key="..." and the same arguments: if the job was created, that returns it instead of creating, and billing, another. See idempotency_key. |
On the document tools, these read as follows:
| What happened | What the agent reads |
|---|---|
Arguments the server refuses before any request: a decide question it can't send; on the document tools, two sources or none (and on upload_attachment, both or neither of download_url and file), an http:// URL, a URL without an extension and no file_name, parse_job_id with password, parse_mode medium or high, or page_ranges on parse_document or extract_data, wait_seconds outside 0 to 60, both or neither of schema and schema_id, an options key an argument sets; on a workspace tool, no workspace_id with none in NACE_WORKSPACE_ID or the config file or update_domain with nothing to change, or ingest_workspace with both file_ids and path_prefix, or stale_only with file_ids; qa with clarification_answers and no session_id; and on any tool, an argument the tool doesn't have | One line naming the problem, such as question 'topic' (choice): criteria: Field required. An argument the tool doesn't have is named with the argument it was likely meant to be, as in decide has no argument modle (did you mean model?), so nothing was sent. Its arguments: state, questions, model., or with why the tool lacks it: classify_document has no argument parse_job_id (classify reads the original file: ...). Nothing is uploaded or created. |
| A request the document service refused, such as an unsupported file type or an unknown field | 422 invalid_request_error/<code>: ..., then up to five fields from the error's detail.errors. See Errors. |
A remote attachment that can't be downloaded: an untrusted or non-HTTPS URL, missing or invalid NACE_ATTACHMENT_HOSTS (direct download_url only), credentials, a custom port or a fragment; a status other than 200, redirects included; more than 256 MiB; a failure or timeout past 120 seconds; or no file name with an extension | One line naming the problem, never the signed link, with service-neutral guidance for a fresh direct link (or workspace_path with the extension); the ChatGPT adapter adds a fresh-reference hint. Nothing is uploaded. |
| A different file at an upload path | 409 path_conflict: ..., without a type. From 32 MiB, Upload did not finish: job <id> is failed (path_conflict: ...). Either way, then the line naming on_conflict="new_version" and workspace_path. |
The same idempotency_key with a different body (document tools) | 409 conflict_error/idempotency_conflict. |
Content still being prepared after wait_timeout, or content that couldn't be prepared | An error naming the job and path, or 409. |
| A job that failed or was cancelled | Not a tool error: the job, with status and error: {code, message}. |
| A wait that ran out | Not a tool error: the job, still queued or running, with an instruction. |
A failure before any answer from Drex reads differently:
| What happened | What the agent reads |
|---|---|
| No API key | no API key: set $NACE_API_KEY or run `nace-mcp login` . |
| The connection to Drex failed | the connection to Drex failed: ..., then what calling the tool again does. |
| A request timed out | Request timed out (timeout=...). The request may have reached Drex and run., then what calling the tool again does. |
After a connection failure or a timeout, the request may have reached Drex, so the error says what calling the tool again does:
| Tools | The line added |
|---|---|
Reads, cancel_job, delete_uploaded_file, download_job_file, delete_workspace and delete_workspace_file | Calling the tool again is safe. |
upload_document, upload_attachment, and a document tool whose upload failed | Calling the tool again is safe: the same bytes at the same workspace path return the file already there. |
A document tool whose create failed, and ingest_workspace, automatic_search and qa | The idempotency_key line above. |
decide | Calling decide again is safe, but if this request reached Drex it was billed, and the new one is billed too. |
create_extraction_schema, add_extraction_schema_version | To check list_extraction_schemas or list_extraction_schema_versions before calling again, or it may be saved twice. |
create_workspace | It may have been created: check list_workspaces before calling again, or it may be created twice. |
Quote the request id when you contact support. The error types are in Errors.
Request identity
The server sends X-Nace-Transport: nace-mcp/<version> independently of the
connecting host's validated X-Nace-Client: name/version. Drex records MCP as
the transport and retains the host as client_app. An unknown or invalid host
name does not remove the MCP transport identifier.
Remote MCP server
Connect coding agents to Nace's hosted endpoint for Drex decisions. No installation required; try decide and list_models without an API key within the free limits.
Agent skills
Teach a code agent such as Claude Code, Codex, Cursor, Pi or Hermes Agent to search a workspace's documents with the nace CLI's navigate tools (drex-search), and to set the workspace up (drex-workspace).