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

ServerToolsSetup
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 serverdecide 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 skills

The 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-mcp

Then 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
CommandWhat it does
nace-mcpServe 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 loginCheck an API key and save it. See Sign in.
nace-mcp --versionPrint the server and SDK versions.
nace-mcp -h, --helpPrint the usage and exit.
nace-mcp 0.5.1
nace-sdk 0.7.0

If 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.

WayHowUse 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 skills

That 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:

claude_desktop_config.json
{
  "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 skills

Leave 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:

~/.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:

~/.config/opencode/opencode.json
{
  "$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/skills

command 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:

~/.cursor/mcp.json
{
  "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 skills

Write "${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 list

pi 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:

~/.pi/agent/mcp.json
{
  "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 nace

hermes 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:

~/.hermes/config.yaml
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, agents

Until 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
FlagWhat it does
--api-key KEYThe 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:

  1. Checks that the key looks like a Nace API key: nace_sk_ followed by 43 characters. Anything else exits 2 before any request.
  2. 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, else https://console.nace.ai. A refused key exits 1 with the API's error. So does a host it can't reach, with error: could not reach <host>: ... Check $NACE_BASE_URL (else base_url in <config path>) and your network; nothing was saved.
  3. Saves it to ~/.nace/config.toml, readable only by you, and prints saved 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

VariableWhat it does
NACE_API_KEYThe API key. Wins over api_key in the config file.
NACE_BASE_URLThe 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_HOSTSComma-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_PATHWhere the config file is, instead of ~/.nace/config.toml. nace-mcp login uses it too.
NACE_DEFAULT_DECISION_MODELThe model decide asks when the agent names none. Default drex-latest.
NACE_LOG_LEVELLog each request to stderr, as the CLI does.
NACE_WORKSPACE_IDThe 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.txt

The 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

ToolWhat it doesBilling
decideAsk typed questions about a state and get calibrated probabilities.Per input token
list_modelsList the models decide can use.Free
parse_documentTurn a document into Markdown, text or layout blocks.Per page, in USD
split_documentCut a packet into page-range segments, each with one of your classes.Per page, in USD
classify_documentLabel a document, or each page, with your classes.Per page, in USD
extract_dataFill a JSON Schema, inline or saved, with a status and a citation for each field.Per page, in USD
ground_itemsFind where each quote appears, with its exact location.Per target, in USD
upload_documentUpload a local file, into a workspace, or to the server without one.Free
upload_attachmentTransfer a remote attachment from a trusted HTTPS link or a native file reference.Free
delete_uploaded_fileDelete a file you uploaded, and everything derived from it.Free
get_jobRead a job and a summary of its result.Free
wait_for_jobWait for a job to finish.Free
list_jobsList your jobs, newest first.Free
cancel_jobCancel a job, or drop it from the list.Free
get_job_requestRead the request a job ran under.Free
download_job_fileSave a file a job produced.Free
get_job_rowsRead a page of a parsed sheet's rows.Free
list_extraction_schemasList saved extraction schemas.Free
get_extraction_schemaRead a saved schema, at its latest version or another.Free
list_extraction_schema_versionsList a saved schema's versions.Free
create_extraction_schemaSave a new extraction schema.Free
add_extraction_schema_versionAdd a version to a saved schema.Free
delete_extraction_schemaDelete a saved schema with every version.Free
create_workspaceCreate a workspace.Free
list_workspacesList your workspaces.Free
get_workspaceRead a workspace and its settings.Free
workspace_statsSummarize what a workspace holds.Free
use_workspaceSave a workspace as the default, for these tools and the nace CLI.Free
delete_workspaceDelete a workspace and everything in it.Free
list_domainsList the domains a workspace can classify its files against.Free
get_domainRead a domain with its taxonomy.Free
create_domainCreate a draft domain of your own.Free
update_domainRename a draft or replace its taxonomy.Free
publish_domainFreeze a draft so workspaces can use it.Free
duplicate_domainCopy a domain as a draft you can edit.Free
list_workspace_filesList a workspace's files.Free
get_workspace_fileRead a file, with links to its bytes, its metadata and its versions.Free
delete_workspace_fileDelete a file from a workspace.Free
ingest_workspaceMake a workspace's files searchable.TBD
automatic_searchAnswer one question from a workspace's files.TBD
qaAsk one turn of a conversation about a workspace's files.TBD
get_documentationRead 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.

ArgumentWhat it is
stateWhat 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.
questionsAn object of name to question, up to 512. The name keys the answer, so name it for what it decides (wants_refund, not q1).
modelThe 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.
imagesUp 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:

typecriteriaAnswer
noulOptional: {"true": "...", "false": "..."}, what counts as yes and as no. Needs instructions or a criterion.noul: the probability of yes, from 0 to 1.
choiceRequired: 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.
scoreRequired: 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:

decide arguments
{
  "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:

decide arguments
{
  "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:

ArgumentsWhat it is
url, with file_nameA public https:// link the document service downloads. See URLs.
path, with workspace_path and on_conflictA file on your machine, uploaded to the server first. See Local files.
workspace_id and file_idA file already in one of your workspaces: what upload_document returns, or a payload's uploaded.
parse_job_idOne 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.

ArgumentWhat it does
workspace_pathThe 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_conflictWhat 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:

ArgumentWhat it does
parse_modeHow 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_ranges1-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.
optionsAny other request field, as an object. See Request fields.
idempotency_keyUp 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_secondsHold 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_charsText 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 }
  }
}
FieldWhat it holds
job_idThe id the job tools take.
kindThe 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).
statusqueued, running, succeeded, failed or cancelled. The last three are final.
credits, usage_finalThe credits the job used, final once usage_final is true.
errorWhy a failed or cancelled job ended: {code, message}.
resultA 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_stateexpired or not_retained when a finished job's result is gone.
progressWhile the job is queued or running, how far it got, when the document service says.
instructionWhile the job is queued or running, what to call next.
uploadedAfter 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.

ArgumentWhat it does
formatsA list of markdown, text and blocks, sent as output.formats. Default: the API's markdown and blocks.
passwordAn 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:

FieldWhat it holds
markdown, textEach rendering you asked for, inline or in a spill file.
contentFor a very large document, the full Markdown in a file. See Large documents and workbooks.
blocks, chunks, ocrAs the API sent them, or spilled when long.
sheetsA 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.
linksEvery other job file the result links, such as a transcript_url, for download_job_file.
disclosuresNotes 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.

ArgumentWhat it does
classesRequired. [{"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.

ArgumentWhat it does
classesRequired. [{"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.
granularitydocument (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.

ArgumentWhat it does
schemaThe fields to extract, as an inline JSON Schema.
schema_idA saved schema (sch_...) instead, at its latest version.
schema_versionWith schema_id, use this version.
instructionsExtra 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.

ArgumentWhat it does
targetsRequired. 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.
semantictrue 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.

ArgumentWhat it does
pathThe local file, as in Local files.
workspace_idThe 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_pathThe file's path in the workspace. Default: its file name. Keep its extension.
on_conflictWhat 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.

ArgumentWhat it does
download_urlA 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.
fileA 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_nameOriginal file name with its extension; overrides file.file_name when nonempty. If absent or unusable, pass workspace_path with the extension.
workspace_idDestination workspace. Left out, the file is uploaded to the server; NACE_WORKSPACE_ID and the saved workspace don't apply.
workspace_pathPath in the workspace, defaulting to the supplied file name. Required with the original extension when no usable file name is available.
on_conflictAs 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.

ArgumentWhat it does
workspace_idThe file's workspace_id, from upload_document or a payload's uploaded. Only your account's workspace; another is 404 workspace_not_found.
file_idThe 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, failed and cancelled included, comes back with its result or its error. A failed job is not a tool error.
  • When the time runs out, the job keeps running: the payload has its status, queued or running, 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.

ArgumentWhat it does
operationOnly jobs of one operation: parse, split, classify, extract or ground.
statusOnly 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.
limitJobs per page, 1 to 50 (default 20).
cursorThe 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.

ArgumentWhat it does
job_idThe job.
pathThe 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.
destinationWhere 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_timeoutHow 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.

ArgumentWhat it does
job_idThe parse job.
pathThe sheet's rows_url from the parse result (only Parquet sources have one), or the part after the job id.
start_rowThe first row, numbered from 0 (default 0).
limitRows 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

ToolWhat 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

ToolWhat 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

ToolReturns
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.

ArgumentWhat it does
nameRequired. 1 to 120 characters, unique in the account ignoring case. Rename it in the dashboard or through the API.
domain_slugThe 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.
accessAccess 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_idKeep 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

ToolWhat 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.
ArgumentWhat it does
name_containsOnly workspaces whose name contains this, ignoring case.
limitWorkspaces per page, 1 to 200 (default 50).
cursorThe next_cursor of the page before. Keep going until it is null.
workspace_idThe 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.

ArgumentWhat it does
workspace_idRequired. The workspace to delete. NACE_WORKSPACE_ID and the saved workspace never apply here.
confirm_nameRequired. 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

ToolWhat 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.
ArgumentWhat it does
workspace_idThe workspace, as in Workspaces.
path_prefixOnly files whose path starts with this.
ingestion_statusOnly 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).
categoriesOnly files ingestion put in these categories.
file_typesOnly these extensions, such as pdf or xlsx.
modified_after, modified_beforeOnly files changed after or before this ISO 8601 time.
limitFiles per page, 1 to 200 (default 50).
cursorThe next_cursor of the page before. Keep going until it is null.
file_idThe file: the file_id that upload_document returned. A file that doesn't exist, or was deleted, answers 404 file_not_found.
metadatatrue adds metadata: what ingestion learned about the file, such as its summary, keywords, outputs, representations and last_error. See File metadata.
versionstrue 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.

ArgumentWhat it does
file_idThe file to delete.
workspace_idThe 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.

ArgumentWhat it does
workspace_idThe workspace, as in Workspaces.
file_idsIngest these files, 1 to 1000. Not with path_prefix.
path_prefixIngest the files under this path. Leave out both for the whole workspace.
stale_onlyOnly files that changed since they were last ingested. Not with file_ids. Default false.
reingest_unchangedAlso rebuild files that are ingested and unchanged, which are otherwise skipped. Default false.
idempotency_keyRetry 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_secondsHold 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.

ToolWhat 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.
ArgumentWhat it does
ownerOnly platform or tenant (your own) domains.
statusOnly draft or published domains. Only a published one can be used to create a workspace.
slugOnly the versions of this slug.
limitDomains per page, 1 to 200.
cursorThe next_cursor of the page before. Keep going until it is null.
domain_idThe 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_nameRequired 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.

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:

FieldWhat it holds
result_typeautomatic_search, or automatic_agent for a qa turn.
routingautomatic_search only: selected, the route chosen before the search ran or the one mode named, final, the route that answered, and reason.
output_job_idautomatic_search only: the job that produced the answer: this job for a fact answer, the agent's own job otherwise.
output_typeautomatic_search only: intelligent_search for a fact answer, automatic_agent for the agent's.
answerThe answer, as text. null when the agent asked back.
structured_answerWith 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.
confidencecertain, very_high, high, medium, low or speculative.
reasoningHow the evidences add up to the answer, including any assumption it made.
evidencesWhat 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_idsA 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_idAn agent answer's conversation, for a qa follow-up.
clarification_questionsA qa turn that asked back: up to 2 of {question, options}, with an instruction saying how to reply.
coverageOn 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_receiptsAn 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}.
catalogAn 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.

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:

  1. uv tool install nace-cli && nace login.
  2. Set the workspace up with the tools here, or in the dashboard, and have the agent call use_workspace with it.
  3. nace skill install --agent claude-code, or your agent's name (see Agent skills).
  4. Start a new session and ask.

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.

ArgumentWhat it does
queryRequired. The question, up to 4,096 characters.
workspace_idThe workspace to search, as in Workspaces.
contextBackground the answer should take as given. Left out, none.
path_prefixSearch only the files under this folder.
pathsSearch exactly these uploaded files too, by path. With path_prefix, the search covers both.
top_kHow many evidences a fact answer cites, 1 to 50 (default 6). The agent ignores it.
modeauto (the default) lets the document service choose; fact runs fact search and agent the agent, with no routing step.
output_schemaA JSON Schema the answer must follow, with an object at its root. The parsed answer is structured_answer.
idempotency_keyRetry 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_secondsHold the request open up to this many seconds, 0 to 60 (default 30).
max_inline_charsText 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.

ArgumentWhat it does
queryRequired. The question, up to 4,096 characters.
workspace_idThe workspace to search, as in Workspaces.
session_idContinue 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, pathsAs on automatic_search. Changing path_prefix or paths starts a fresh conversation, with a new session_id.
allow_clarificationLet the agent ask back when a question is ambiguous. Default true.
clarification_answersAnswers 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_schemaAs 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_charsAs 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.

topicWhat it covers
overviewWhat each tool does and costs. The tool's description tells the agent to read it first.
decideQuestion shapes, answers and limits.
sourcesurl, path, workspace_id and file_id, parse_job_id, and uploads.
parse, split, classify, extract, groundEach tool's arguments, options and summarised result.
jobsWaiting, payloads, spill files, job files and rows.
schemasSaved extraction schemas.
workspacesWorkspaces, their domains, files, ingestion, and search and QA.
errorsWhat 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 permitted

Some errors add a line on what to do next:

ErrorThe line added
401Run `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_creditThe account has insufficient available funds: add funds or add a card at <your Drex host>/dashboard/billing, then call the tool again.
402 payment_requiredThe 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_conflictTo pass on_conflict="new_version", or another workspace_path to keep both files.
A create that failed after a local file was uploadedThe 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 timeoutTo 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 happenedWhat 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 haveOne 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 field422 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 extensionOne 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 path409 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 preparedAn error naming the job and path, or 409.
A job that failed or was cancelledNot a tool error: the job, with status and error: {code, message}.
A wait that ran outNot a tool error: the job, still queued or running, with an instruction.

A failure before any answer from Drex reads differently:

What happenedWhat the agent reads
No API keyno API key: set $NACE_API_KEY or run `nace-mcp login` .
The connection to Drex failedthe connection to Drex failed: ..., then what calling the tool again does.
A request timed outRequest 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:

ToolsThe line added
Reads, cancel_job, delete_uploaded_file, download_job_file, delete_workspace and delete_workspace_fileCalling the tool again is safe.
upload_document, upload_attachment, and a document tool whose upload failedCalling 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 qaThe idempotency_key line above.
decideCalling 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_versionTo check list_extraction_schemas or list_extraction_schema_versions before calling again, or it may be saved twice.
create_workspaceIt 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.

On this page