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 +21 -0
- nace_mcp-0.1.0/PKG-INFO +86 -0
- nace_mcp-0.1.0/README.md +61 -0
- nace_mcp-0.1.0/pyproject.toml +86 -0
- nace_mcp-0.1.0/pyproject.toml.orig +67 -0
- nace_mcp-0.1.0/src/nace_mcp/__init__.py +12 -0
- nace_mcp-0.1.0/src/nace_mcp/_cli.py +97 -0
- nace_mcp-0.1.0/src/nace_mcp/_decide.py +98 -0
- nace_mcp-0.1.0/src/nace_mcp/_docs.py +30 -0
- nace_mcp-0.1.0/src/nace_mcp/_documents.py +441 -0
- nace_mcp-0.1.0/src/nace_mcp/_entrypoint.py +25 -0
- nace_mcp-0.1.0/src/nace_mcp/_jobs.py +215 -0
- nace_mcp-0.1.0/src/nace_mcp/_local_files.py +19 -0
- nace_mcp-0.1.0/src/nace_mcp/_payloads.py +370 -0
- nace_mcp-0.1.0/src/nace_mcp/_run.py +135 -0
- nace_mcp-0.1.0/src/nace_mcp/_schemas.py +101 -0
- nace_mcp-0.1.0/src/nace_mcp/_source.py +121 -0
- nace_mcp-0.1.0/src/nace_mcp/_version.py +1 -0
- nace_mcp-0.1.0/src/nace_mcp/docs/classify.md +13 -0
- nace_mcp-0.1.0/src/nace_mcp/docs/decide.md +34 -0
- nace_mcp-0.1.0/src/nace_mcp/docs/errors.md +23 -0
- nace_mcp-0.1.0/src/nace_mcp/docs/extract.md +19 -0
- nace_mcp-0.1.0/src/nace_mcp/docs/ground.md +13 -0
- nace_mcp-0.1.0/src/nace_mcp/docs/jobs.md +21 -0
- nace_mcp-0.1.0/src/nace_mcp/docs/overview.md +25 -0
- nace_mcp-0.1.0/src/nace_mcp/docs/parse.md +26 -0
- nace_mcp-0.1.0/src/nace_mcp/docs/schemas.md +13 -0
- nace_mcp-0.1.0/src/nace_mcp/docs/sources.md +25 -0
- nace_mcp-0.1.0/src/nace_mcp/docs/split.md +13 -0
- nace_mcp-0.1.0/src/nace_mcp/py.typed +0 -0
- nace_mcp-0.1.0/src/nace_mcp/server.py +177 -0
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.
|
nace_mcp-0.1.0/PKG-INFO
ADDED
|
@@ -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).
|
nace_mcp-0.1.0/README.md
ADDED
|
@@ -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)}
|