nace-mcp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
nace_mcp-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nace AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,86 @@
1
+ Metadata-Version: 2.4
2
+ Name: nace-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for the Drex API: calibrated decisions and document jobs for coding agents
5
+ Keywords: drex,nace,mcp,model-context-protocol,document-intelligence
6
+ Author: Nace AI
7
+ Author-email: Nace AI <engineering@nace.ai>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Typing :: Typed
18
+ Requires-Dist: nace-sdk>=0.1,<1
19
+ Requires-Dist: mcp>=1.28,<2
20
+ Requires-Dist: truststore>=0.10.4,<1
21
+ Requires-Python: >=3.11
22
+ Project-URL: Homepage, https://console.nace.ai
23
+ Project-URL: Documentation, https://console.nace.ai/docs/developer-tools/mcp
24
+ Description-Content-Type: text/markdown
25
+
26
+ # nace-mcp
27
+
28
+ An [MCP](https://modelcontextprotocol.io) server for the [Drex API](https://console.nace.ai/docs): coding agents get calibrated decisions and document jobs as tools. MIT; built on [`nace-sdk`](https://pypi.org/project/nace-sdk/).
29
+
30
+ ```bash
31
+ nace-mcp login # or: uvx nace-mcp login; paste a key from https://console.nace.ai/dashboard/api-keys
32
+ claude mcp add nace -- uvx nace-mcp # Claude Code
33
+ ```
34
+
35
+ `nace-mcp login` checks the key against Drex and saves it to `~/.nace/config.toml` (mode 0600), the file `nace login` from [`nace-cli`](https://pypi.org/project/nace-cli/) writes too, so either signs in both. `--api-key KEY` passes the key, `--api-key -` reads it from stdin; without a terminal one of them is required. `$NACE_API_KEY`, `$NACE_BASE_URL` and `$NACE_CONFIG_PATH` in the server's environment override the file. Without a key the server still starts, and every tool answers with how to sign in until one is saved. The server reads the credentials on each call, so `nace-mcp login` while it runs, to sign in or to replace a refused key, needs no restart. `nace-mcp --version` prints the server and SDK versions.
36
+
37
+ ## Connect an agent
38
+
39
+ Codex: `codex mcp add nace -- uvx nace-mcp` (or a `[mcp_servers.nace]` table with `command = "uvx"` and `args = ["nace-mcp"]` in `~/.codex/config.toml`).
40
+
41
+ OpenCode (`opencode.json`): `"mcp": {"nace": {"type": "local", "command": ["uvx", "nace-mcp"], "enabled": true}}`.
42
+
43
+ Claude Desktop (`claude_desktop_config.json`) and Cursor (`.cursor/mcp.json`):
44
+
45
+ ```json
46
+ {
47
+ "mcpServers": {
48
+ "nace": {
49
+ "command": "uvx",
50
+ "args": ["nace-mcp"]
51
+ }
52
+ }
53
+ }
54
+ ```
55
+
56
+ Add `"env": {"NACE_API_KEY": "nace_sk_..."}` to pass a key without `nace-mcp login`. To install it once instead of through `uvx`: `pipx install nace-mcp` (Python 3.11 or later), then use `nace-mcp` as the command.
57
+
58
+ ## Tools
59
+
60
+ | Tool | What it does |
61
+ | --- | --- |
62
+ | `decide` | Answer typed questions (`noul`, `choice`, `score`) about text or JSON with calibrated probabilities. |
63
+ | `list_models` | List the models `decide` can use. |
64
+ | `upload_document` | Upload a local file into the account's workspace. |
65
+ | `parse_document` | Convert a document to Markdown, text or layout blocks. |
66
+ | `split_document` | Cut a packet into segments, each with a class. |
67
+ | `classify_document` | Label a document, or each page, with your classes. |
68
+ | `extract_data` | Fill a JSON Schema, with a status and citation per field. |
69
+ | `ground_items` | Find where each quote appears in a document. |
70
+ | `get_job` | Read a job and a summary of its result. |
71
+ | `wait_for_job` | Wait for a job to finish. |
72
+ | `list_jobs` | List jobs, newest first. |
73
+ | `cancel_job` | Cancel a running job, or drop a finished one from the list. |
74
+ | `get_job_request` | The request a job ran under. |
75
+ | `download_job_file` | Save a file a job produced. |
76
+ | `get_job_rows` | Page through a parsed sheet's rows. |
77
+ | `list_extraction_schemas` | List saved extraction schemas. |
78
+ | `get_extraction_schema` | Read a saved schema, at its latest or a given version. |
79
+ | `list_extraction_schema_versions` | List a saved schema's versions. |
80
+ | `create_extraction_schema` | Save a new extraction schema. |
81
+ | `add_extraction_schema_version` | Add a version to a saved schema. |
82
+ | `get_documentation` | Usage notes for a topic, shipped with the server. |
83
+
84
+ The document tools take one source: an `https://` `url`, a local `path` (uploaded first), an uploaded file's `workspace_id` and `file_id`, or a finished parse's `parse_job_id`. They wait up to `wait_seconds` (60 at most) and return the job; one still running comes back with an instruction to call `wait_for_job`. Any other request field goes in a tool's `options`; an argument a tool doesn't have is refused rather than ignored. Results are summarised, and text or JSON longer than `max_inline_chars` is written to a file readable only by you, whose path is returned. For a document too large to return inline, `parse_document` saves the full Markdown to a file.
85
+
86
+ Full documentation: [MCP server](https://console.nace.ai/docs/developer-tools/mcp).
@@ -0,0 +1,61 @@
1
+ # nace-mcp
2
+
3
+ An [MCP](https://modelcontextprotocol.io) server for the [Drex API](https://console.nace.ai/docs): coding agents get calibrated decisions and document jobs as tools. MIT; built on [`nace-sdk`](https://pypi.org/project/nace-sdk/).
4
+
5
+ ```bash
6
+ nace-mcp login # or: uvx nace-mcp login; paste a key from https://console.nace.ai/dashboard/api-keys
7
+ claude mcp add nace -- uvx nace-mcp # Claude Code
8
+ ```
9
+
10
+ `nace-mcp login` checks the key against Drex and saves it to `~/.nace/config.toml` (mode 0600), the file `nace login` from [`nace-cli`](https://pypi.org/project/nace-cli/) writes too, so either signs in both. `--api-key KEY` passes the key, `--api-key -` reads it from stdin; without a terminal one of them is required. `$NACE_API_KEY`, `$NACE_BASE_URL` and `$NACE_CONFIG_PATH` in the server's environment override the file. Without a key the server still starts, and every tool answers with how to sign in until one is saved. The server reads the credentials on each call, so `nace-mcp login` while it runs, to sign in or to replace a refused key, needs no restart. `nace-mcp --version` prints the server and SDK versions.
11
+
12
+ ## Connect an agent
13
+
14
+ Codex: `codex mcp add nace -- uvx nace-mcp` (or a `[mcp_servers.nace]` table with `command = "uvx"` and `args = ["nace-mcp"]` in `~/.codex/config.toml`).
15
+
16
+ OpenCode (`opencode.json`): `"mcp": {"nace": {"type": "local", "command": ["uvx", "nace-mcp"], "enabled": true}}`.
17
+
18
+ Claude Desktop (`claude_desktop_config.json`) and Cursor (`.cursor/mcp.json`):
19
+
20
+ ```json
21
+ {
22
+ "mcpServers": {
23
+ "nace": {
24
+ "command": "uvx",
25
+ "args": ["nace-mcp"]
26
+ }
27
+ }
28
+ }
29
+ ```
30
+
31
+ Add `"env": {"NACE_API_KEY": "nace_sk_..."}` to pass a key without `nace-mcp login`. To install it once instead of through `uvx`: `pipx install nace-mcp` (Python 3.11 or later), then use `nace-mcp` as the command.
32
+
33
+ ## Tools
34
+
35
+ | Tool | What it does |
36
+ | --- | --- |
37
+ | `decide` | Answer typed questions (`noul`, `choice`, `score`) about text or JSON with calibrated probabilities. |
38
+ | `list_models` | List the models `decide` can use. |
39
+ | `upload_document` | Upload a local file into the account's workspace. |
40
+ | `parse_document` | Convert a document to Markdown, text or layout blocks. |
41
+ | `split_document` | Cut a packet into segments, each with a class. |
42
+ | `classify_document` | Label a document, or each page, with your classes. |
43
+ | `extract_data` | Fill a JSON Schema, with a status and citation per field. |
44
+ | `ground_items` | Find where each quote appears in a document. |
45
+ | `get_job` | Read a job and a summary of its result. |
46
+ | `wait_for_job` | Wait for a job to finish. |
47
+ | `list_jobs` | List jobs, newest first. |
48
+ | `cancel_job` | Cancel a running job, or drop a finished one from the list. |
49
+ | `get_job_request` | The request a job ran under. |
50
+ | `download_job_file` | Save a file a job produced. |
51
+ | `get_job_rows` | Page through a parsed sheet's rows. |
52
+ | `list_extraction_schemas` | List saved extraction schemas. |
53
+ | `get_extraction_schema` | Read a saved schema, at its latest or a given version. |
54
+ | `list_extraction_schema_versions` | List a saved schema's versions. |
55
+ | `create_extraction_schema` | Save a new extraction schema. |
56
+ | `add_extraction_schema_version` | Add a version to a saved schema. |
57
+ | `get_documentation` | Usage notes for a topic, shipped with the server. |
58
+
59
+ The document tools take one source: an `https://` `url`, a local `path` (uploaded first), an uploaded file's `workspace_id` and `file_id`, or a finished parse's `parse_job_id`. They wait up to `wait_seconds` (60 at most) and return the job; one still running comes back with an instruction to call `wait_for_job`. Any other request field goes in a tool's `options`; an argument a tool doesn't have is refused rather than ignored. Results are summarised, and text or JSON longer than `max_inline_chars` is written to a file readable only by you, whose path is returned. For a document too large to return inline, `parse_document` saves the full Markdown to a file.
60
+
61
+ Full documentation: [MCP server](https://console.nace.ai/docs/developer-tools/mcp).
@@ -0,0 +1,86 @@
1
+ [project]
2
+ name = "nace-mcp"
3
+ version = "0.1.0"
4
+ description = "MCP server for the Drex API: calibrated decisions and document jobs for coding agents"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.11"
9
+ keywords = [
10
+ "drex",
11
+ "nace",
12
+ "mcp",
13
+ "model-context-protocol",
14
+ "document-intelligence",
15
+ ]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
24
+ "Typing :: Typed",
25
+ ]
26
+ dependencies = [
27
+ "nace-sdk>=0.1,<1",
28
+ "mcp>=1.28,<2",
29
+ "truststore>=0.10.4,<1",
30
+ ]
31
+
32
+ [[project.authors]]
33
+ name = "Nace AI"
34
+ email = "engineering@nace.ai"
35
+
36
+ [project.urls]
37
+ Homepage = "https://console.nace.ai"
38
+ Documentation = "https://console.nace.ai/docs/developer-tools/mcp"
39
+
40
+ [project.scripts]
41
+ nace-mcp = "nace_mcp._entrypoint:main"
42
+
43
+ [tool.uv.sources.nace-sdk]
44
+ workspace = true
45
+
46
+ [tool.ruff]
47
+ target-version = "py311"
48
+ line-length = 150
49
+ fix = true
50
+
51
+ [tool.ruff.lint]
52
+ select = [
53
+ "E",
54
+ "F",
55
+ "W",
56
+ "I",
57
+ "B",
58
+ "UP",
59
+ "RUF",
60
+ "ASYNC",
61
+ "SIM",
62
+ "BLE",
63
+ "PLC0415",
64
+ ]
65
+ ignore = [
66
+ "D",
67
+ "ANN",
68
+ "PLR0913",
69
+ "ASYNC109",
70
+ ]
71
+
72
+ [tool.pytest.ini_options]
73
+ testpaths = ["tests"]
74
+ pythonpath = [
75
+ "src",
76
+ "tests",
77
+ ]
78
+ asyncio_mode = "auto"
79
+ addopts = [
80
+ "--strict-markers",
81
+ "--tb=short",
82
+ ]
83
+
84
+ [build-system]
85
+ requires = ["uv_build>=0.9.0,<0.12.0"]
86
+ build-backend = "uv_build"
@@ -0,0 +1,67 @@
1
+ [project]
2
+ name = "nace-mcp"
3
+ version = "0.1.0"
4
+ description = "MCP server for the Drex API: calibrated decisions and document jobs for coding agents"
5
+ authors = [
6
+ { name = "Nace AI", email = "engineering@nace.ai" }
7
+ ]
8
+ readme = "README.md"
9
+ license = "MIT"
10
+ license-files = ["LICENSE"]
11
+ requires-python = ">=3.11"
12
+ keywords = [
13
+ "drex",
14
+ "nace",
15
+ "mcp",
16
+ "model-context-protocol",
17
+ "document-intelligence",
18
+ ]
19
+ classifiers = [
20
+ "Development Status :: 3 - Alpha",
21
+ "Intended Audience :: Developers",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Programming Language :: Python :: 3.14",
27
+ "Typing :: Typed",
28
+ ]
29
+ dependencies = [
30
+ "nace-sdk>=0.1,<1",
31
+ "mcp>=1.28,<2",
32
+ "truststore>=0.10.4,<1",
33
+ ]
34
+
35
+ [project.urls]
36
+ Homepage = "https://console.nace.ai"
37
+ Documentation = "https://console.nace.ai/docs/developer-tools/mcp"
38
+
39
+ [project.scripts]
40
+ nace-mcp = "nace_mcp._entrypoint:main"
41
+
42
+ [tool.uv.sources]
43
+ nace-sdk = { workspace = true }
44
+
45
+ [build-system]
46
+ requires = ["uv_build>=0.9.0,<0.12.0"]
47
+ build-backend = "uv_build"
48
+
49
+ [tool.ruff]
50
+ target-version = "py311"
51
+ line-length = 150
52
+ fix = true
53
+
54
+ [tool.ruff.lint]
55
+ select = ["E", "F", "W", "I", "B", "UP", "RUF", "ASYNC", "SIM", "BLE", "PLC0415"]
56
+ ignore = [
57
+ "D",
58
+ "ANN",
59
+ "PLR0913",
60
+ "ASYNC109", # `timeout` on wait_for_job is a polling budget, not a cancel scope
61
+ ]
62
+
63
+ [tool.pytest.ini_options]
64
+ testpaths = ["tests"]
65
+ pythonpath = ["src", "tests"]
66
+ asyncio_mode = "auto"
67
+ addopts = ["--strict-markers", "--tb=short"]
@@ -0,0 +1,12 @@
1
+ """``nace-mcp`` — an MCP server for the Drex API, for coding agents.
2
+
3
+ Decisions (``POST /v1/systemone``), document jobs and uploads (``/v1/documents/*``)
4
+ and saved extraction schemas (``/v1/extraction-schemas``) over ``nace-sdk``, served
5
+ over stdio. Credentials come from ``$NACE_API_KEY`` / ``$NACE_BASE_URL`` or
6
+ ``~/.nace/config.toml`` (``nace-mcp login`` or ``nace login``).
7
+ """
8
+
9
+ from nace_mcp._version import __version__
10
+ from nace_mcp.server import build_server
11
+
12
+ __all__ = ["__version__", "build_server"]
@@ -0,0 +1,97 @@
1
+ """``nace-mcp`` entry point: serve over stdio, ``login`` to save a key, ``--version``.
2
+
3
+ While serving, stdout is the MCP protocol channel: nothing else may print there.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ import sys
10
+ from getpass import getpass
11
+
12
+ from nace_sdk import NaceAPIConnectionError, NaceAPIError
13
+ from nace_sdk import __version__ as sdk_version
14
+ from nace_sdk.constants import BASE_URL_ENV, DEFAULT_BASE_URL, read_env
15
+ from nace_sdk.credentials import CredentialsError, config_path, login, read_config
16
+
17
+ from nace_mcp import __version__, build_server
18
+ from nace_mcp._run import describe_api_error
19
+
20
+
21
+ def build_parser() -> argparse.ArgumentParser:
22
+ parser = argparse.ArgumentParser(
23
+ prog="nace-mcp",
24
+ description=(
25
+ "Nace MCP server for coding agents, over stdio. Credentials come from $NACE_API_KEY / $NACE_BASE_URL "
26
+ "or ~/.nace/config.toml (`nace-mcp login` or `nace login`)."
27
+ ),
28
+ )
29
+ parser.add_argument("--version", dest="show_version", action="store_true", help="Print nace-mcp and nace-sdk versions.")
30
+ sub = parser.add_subparsers(dest="command")
31
+ login_p = sub.add_parser("login", help="Check an API key against Drex and save it to ~/.nace/config.toml (the file `nace login` uses).")
32
+ login_p.add_argument(
33
+ "--api-key",
34
+ nargs="?",
35
+ const="",
36
+ default=None,
37
+ metavar="KEY",
38
+ help="The nace_sk_ key; --api-key - reads it from stdin. Omit KEY to type it hidden at a terminal.",
39
+ )
40
+ return parser
41
+
42
+
43
+ def read_key(value: str | None) -> str:
44
+ """The key to save: ``KEY`` itself, ``-`` from stdin, else typed hidden at a terminal."""
45
+ if value == "-":
46
+ return getpass("Drex API key: ") if sys.stdin.isatty() else sys.stdin.readline()
47
+ if value:
48
+ return value
49
+ if not sys.stdin.isatty():
50
+ raise CredentialsError("pass --api-key KEY (or --api-key - to read stdin) when not in a terminal")
51
+ return getpass("Drex API key: ")
52
+
53
+
54
+ def login_base_url() -> str:
55
+ """The host ``login`` checks the key against, as :func:`nace_sdk.credentials.login` picks it."""
56
+ return read_env(BASE_URL_ENV) or read_config(config_path(), recover=True).get("base_url") or DEFAULT_BASE_URL
57
+
58
+
59
+ def run_login(api_key: str | None) -> int:
60
+ try:
61
+ path = login(read_key(api_key))
62
+ except CredentialsError as exc:
63
+ print(f"error: {exc}", file=sys.stderr)
64
+ return 2
65
+ except NaceAPIError as exc:
66
+ print(f"error: {describe_api_error(exc)}", file=sys.stderr)
67
+ return 1
68
+ except NaceAPIConnectionError as exc:
69
+ # A person reads this, not an agent: name the host, which a typo in $NACE_BASE_URL makes unreachable.
70
+ print(
71
+ f"error: could not reach {login_base_url()}: {str(exc).rstrip('.')}. Check ${BASE_URL_ENV} "
72
+ f"(else base_url in {config_path()}) and your network; nothing was saved.",
73
+ file=sys.stderr,
74
+ )
75
+ return 1
76
+ print(f"saved API key to {path}", file=sys.stderr)
77
+ return 0
78
+
79
+
80
+ def run_version() -> int:
81
+ print(f"nace-mcp {__version__}")
82
+ print(f"nace-sdk {sdk_version}")
83
+ return 0
84
+
85
+
86
+ def main(argv: list[str] | None = None) -> int:
87
+ args = build_parser().parse_args(argv)
88
+ if args.show_version:
89
+ return run_version()
90
+ if args.command == "login":
91
+ return run_login(args.api_key)
92
+ build_server().run(transport="stdio")
93
+ return 0
94
+
95
+
96
+ if __name__ == "__main__":
97
+ raise SystemExit(main())
@@ -0,0 +1,98 @@
1
+ """Decision tools: ``decide`` (``POST /v1/systemone``) and ``list_models``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from mcp.server.fastmcp import FastMCP
8
+ from nace_sdk import AsyncNaceClient, Choice, NaceError, Noul, Question, Score, SystemOneResponse
9
+ from pydantic import ValidationError
10
+
11
+ from nace_mcp._run import GetClient, run_tool
12
+
13
+ #: decide has no idempotency key: a request that reached Drex was answered and billed.
14
+ DECIDE_RETRY = "Calling decide again is safe, but if this request reached Drex it was billed, and the new one is billed too."
15
+
16
+ QUESTION_TYPES: dict[str, type[Noul] | type[Choice] | type[Score]] = {"noul": Noul, "choice": Choice, "score": Score}
17
+
18
+
19
+ def question_objects(questions: dict[str, Any]) -> dict[str, Question]:
20
+ """Each question as the SDK's ``Noul``, ``Choice`` or ``Score``, refusing an unknown type or field by name."""
21
+ parsed: dict[str, Question] = {}
22
+ for name, raw in questions.items():
23
+ kind = raw.get("type") if isinstance(raw, dict) else None
24
+ model = QUESTION_TYPES.get(kind) if isinstance(kind, str) else None
25
+ if model is None:
26
+ raise ValueError(f'question {name!r} needs "type": "noul", "choice" or "score", got {kind!r}')
27
+ try:
28
+ parsed[name] = model.model_validate(raw)
29
+ except ValidationError as exc:
30
+ problems = "; ".join(f"{'.'.join(str(part) for part in error['loc']) or 'question'}: {error['msg']}" for error in exc.errors())
31
+ raise ValueError(f"question {name!r} ({kind}): {problems}") from None
32
+ return parsed
33
+
34
+
35
+ def _request_id(response: SystemOneResponse) -> str | None:
36
+ try:
37
+ return response.request_id
38
+ except NaceError:
39
+ return None
40
+
41
+
42
+ def register_decide_tools(mcp: FastMCP, get_client: GetClient) -> None:
43
+ @mcp.tool()
44
+ async def decide(state: Any, questions: dict[str, dict[str, Any]], model: str | None = None) -> dict[str, Any]:
45
+ """Answer typed questions about a state with calibrated probabilities, in one call.
46
+
47
+ Use it for a decision about text or data: route a ticket, flag a message, grade a reply,
48
+ check a condition. state is the text or any JSON value the questions are about. questions
49
+ maps a name you choose (it keys the answer) to one question, up to 512:
50
+
51
+ - {"type": "noul", "instructions": "Is the customer asking for a refund?", "criteria"?:
52
+ {"true": "what counts as yes", "false": "what counts as no"}}: yes or no. Needs
53
+ instructions or a criterion. The answer's noul is the probability of yes, 0 to 1.
54
+ - {"type": "choice", "instructions"?: "What is this ticket about?", "criteria": {"billing":
55
+ "Payments or refunds", "technical": "Bugs or outages", "other": null}}: one label from a
56
+ set without order. The answer has choice, probabilities per label and confidence.
57
+ - {"type": "score", "instructions"?: "How upset is the customer?", "criteria": ["Calm",
58
+ "Annoyed", "Angry"]}: a place on an ordered scale, low to high, the index being the
59
+ score. The answer has score (an expected level, possibly fractional), legend,
60
+ probabilities per level and confidence.
61
+
62
+ Batch every question about one state into one call: the state is billed once, per input
63
+ token. model defaults to the server's $NACE_DEFAULT_DECISION_MODEL, else drex-latest; list_models
64
+ lists the others. Returns {model,
65
+ answers, usage, evaluation_time_ms, request_id}. Choose your own threshold on the
66
+ probabilities rather than treating them as a verdict.
67
+ """
68
+
69
+ async def op(client: AsyncNaceClient) -> dict[str, Any]:
70
+ response = await client.system_one(state, question_objects(questions), model=model or None)
71
+ return {
72
+ "model": response.model,
73
+ "answers": {name: answer.model_dump(mode="json") for name, answer in response.answers.items()},
74
+ "usage": response.usage.model_dump(mode="json"),
75
+ "evaluation_time_ms": response.evaluation_time_ms,
76
+ "request_id": _request_id(response),
77
+ }
78
+
79
+ return await run_tool(get_client, op, retry=DECIDE_RETRY)
80
+
81
+ @mcp.tool()
82
+ async def list_models() -> dict[str, Any]:
83
+ """List the decision models the account can call, for decide's model argument.
84
+
85
+ alias_for names the version an alias such as drex-latest (or a retired id) is answered by,
86
+ and is null for a pinned version. Works at any balance.
87
+ """
88
+
89
+ async def op(client: AsyncNaceClient) -> dict[str, Any]:
90
+ response = await client.models.list()
91
+ return {
92
+ "models": [
93
+ {"name": item.name, "alias_for": item.alias_for, "release_date": item.release_date, "description": item.description}
94
+ for item in response.models
95
+ ]
96
+ }
97
+
98
+ return await run_tool(get_client, op)
@@ -0,0 +1,30 @@
1
+ """``get_documentation``: current Drex usage, shipped with the package, not training memory."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib.resources import files
6
+ from typing import Any, Literal, get_args
7
+
8
+ from mcp.server.fastmcp import FastMCP
9
+
10
+ Topic = Literal["overview", "decide", "sources", "parse", "split", "classify", "extract", "ground", "jobs", "schemas", "errors"]
11
+ TOPICS: tuple[str, ...] = get_args(Topic)
12
+
13
+
14
+ def topic_text(topic: str) -> str:
15
+ if topic not in TOPICS:
16
+ raise ValueError(f"unknown topic {topic!r}; choose one of: {', '.join(TOPICS)}")
17
+ return files("nace_mcp").joinpath("docs").joinpath(f"{topic}.md").read_text(encoding="utf-8")
18
+
19
+
20
+ def register_doc_tools(mcp: FastMCP) -> None:
21
+ @mcp.tool()
22
+ async def get_documentation(topic: Topic) -> dict[str, Any]:
23
+ """Return Drex's current usage notes for one topic, with the link to the full page.
24
+
25
+ Read overview first. Topics: overview (what each tool does, costs), decide (questions and
26
+ answers), sources (url, path, workspace files, parse_job_id), parse, split, classify,
27
+ extract, ground (each tool's options and result), jobs (waiting, files, rows, payloads and
28
+ spill files), schemas (saved extraction schemas), errors (what a failed call means).
29
+ """
30
+ return {"topic": topic, "text": topic_text(topic)}