DecisionNode MCP server
DecisionNode is a decision engine: send it text, JSON or images with a question, and it answers with calibrated probabilities.
DecisionNode's judgments as tools for AI agents. An agent asks a narrow question about a piece of content (is this claim supported, which team should take this ticket, how urgent is it, how many line items does the invoice list, where in the photo is the damaged package) and gets a calibrated answer back in one tool call.
Every tool call is a DecisionNode API request made with your own API key. The server stores no key, state, question or answer.
Install
You need a DecisionNode API key and uv. The server runs on your machine and your MCP client starts it over stdio:
uvx bynn-decisionnode-mcp
Before the package's first release on PyPI, install it from this repository instead:
uvx --from git+https://github.com/Bynn-Intelligence/decision-node-mcp-server bynn-decisionnode-mcp
In the client setups below, the git form replaces bynn-decisionnode-mcp with
--from git+https://github.com/Bynn-Intelligence/decision-node-mcp-server bynn-decisionnode-mcp.
Claude Code
claude mcp add decisionnode -e DECISIONNODE_API_KEY=<key> -- uvx bynn-decisionnode-mcp
Claude Desktop
Add the server to claude_desktop_config.json (Settings, Developer, Edit Config) and
restart Claude Desktop:
{
"mcpServers": {
"decisionnode": {
"command": "uvx",
"args": ["bynn-decisionnode-mcp"],
"env": { "DECISIONNODE_API_KEY": "<key>" }
}
}
}
Other MCP clients
Cursor, Windsurf and other clients configured by a JSON file take the same entry under
mcpServers; VS Code takes it under servers. Any client that launches a server over
stdio works: the command is uvx, the argument is bynn-decisionnode-mcp, and the key
goes in the server's environment as DECISIONNODE_API_KEY. The server name
(decisionnode above) is yours to choose.
Over HTTP
The same package serves Streamable HTTP, for a team that runs one server for several clients:
uvx bynn-decisionnode-mcp --transport http --host 0.0.0.0 --port 8000
The endpoint is the root path, stateless, with GET /health. Each client sends its own
key on every request, Authorization: Bearer <key>, and the server forwards it to the
API; it never uses a key of its own. Over HTTP a tool never reads a file path, and the
base64 image data of one call is limited to 3 MiB (see Images below).
claude mcp add --transport http decisionnode http://127.0.0.1:8000/ \
--header "Authorization: Bearer $DECISIONNODE_API_KEY"
Tools
All eleven are read-only and idempotent (readOnlyHint and idempotentHint true,
destructiveHint false). Each declares an output schema and returns its result twice: as
structured content, and as the same JSON in a text block.
| Tool | Use it to | Returns | Costs |
|---|---|---|---|
guide |
Learn how to use the other tools well | The guide, as text | Nothing |
truth |
Ask one yes-or-no question about a state | {"truth": 0.94, "cost": {...}} |
A request |
choice |
Pick one option from a set you define (1 to 255) | {"choice", "confidence", "probabilities", "cost"} |
A request |
score |
Place a state on an ordered scale (1 to 10 levels) | {"score", "confidence", "legend", "probabilities", "cost"} |
A request |
number |
Ask for a count or a quantity within a range | {"number", "expected", "confidence", "probabilities", "cost"} |
A request |
point |
Find one spot in an image | {"x", "y", "present", "confidence", "image", "width", "height", "cost"} |
A request |
box |
Draw a box around one thing in an image | {"x1", "y1", "x2", "y2", "present", "confidence", "image", "width", "height", "cost"} |
A request |
decide |
Ask several questions of any type about one state | {"answers": {id: answer}, "model", "input_tokens", "cost"} |
One request |
decide_many |
Ask the same questions about many states (1 to 50 items) | {"results": {item id: {"answers", "cost"} or {"error"}}, "input_tokens"} |
One request per item |
decide_video |
Ask questions of any type about videos sent as frames | {"answers", "model", "input_tokens", "cost", "video_frames", "video_seconds"}, and warnings |
One request |
models |
List the models the key may use, with their limits | {"models": [...]} |
Nothing |
Inputs:
| Tool | Arguments |
|---|---|
truth |
instructions, state, if_true, if_false, images, model |
choice |
instructions, options (a list of names, or an object of name to description), state, images, model |
score |
instructions, levels (lowest first), state, images, model |
number |
instructions, min, max, step, state, images, model |
point, box |
instructions, images (at least one), image, units, state, model |
decide |
questions (id to {type, instructions, criteria}, {type, instructions, min, max, step} for a number, or {type, instructions, image, units} for a point or a box, as the API takes them), state, images, model |
decide_many |
items (a list of {id, state, images}, images optional and each item's own), questions, model |
decide_video |
questions, videos, state, images, model |
cost, on every tool that asks a question, is what the call cost as the API booked it, in
USD: usd, split into model_usd and boosters_usd, and free_tier. It is passed
through as sent and appears when the API sends it.
state is a string or any JSON value and may be left out. model defaults to
decisionnode-moe; decisionnode-flash is the faster, lower-cost tier. Both mean the
current release; a result's model is the pinned id of the version that answered. The
question types are truth, choice, score, number, point and box.
Values are passed through exactly as the API sent them. A result has a safety field only
when the API's safety check flagged a category for the key.
Images
Up to 16 images a call, within the model's token limit, 10 MB each, JPEG, PNG or WebP. An
image is {id, url}, {id, data, media_type} or, on your own machine, {id, path} for a
file. A url must start with https://; any other scheme is refused before a request is
made.
There are two ways to say which image a question is about:
- By id (recommended). Give each image a short, meaningful id and use it in the
instructions, in backticks: "Does the total on
receiptmatch the amount in the claim?" - By number. Images are numbered from 1 in the order of
images: "image 2", "the second image", "Picture 2" and "the last image" all work.
The model sees each image as Picture 1 (receipt):, Picture 2 (claim_photo):, so both
ways point to the same image. Prefer ids: they stay right when images are added or
reordered. Use names, not digits, as ids (front, not 2). Videos work the same way, by
id or by number ("video 1"), numbered separately in the order of videos. The image of
a point or box question takes the id.
Over HTTP one call's body is limited to 4 MiB by the MCP transport, so the base64 data
of all images in a call (every item's, in decide_many) is limited to 3 MiB in total
(3,145,728 characters, about 2.25 MiB of image bytes). A call with more is refused with an
invalid_request_error tool result before any request is made, and a body over 4 MiB is
answered 413. Send larger images by https url; the API fetches them itself, up to the
model's own limit (10 MB an image).
Number questions
A number question asks for a count or a quantity within a range: how many people are in
the photo, how many line items the invoice lists, the year a contract was signed, a
rating in halves. min and max bound the answer (0 and 255 when left out) and step
sets the grid of possible answers (1 when left out; 0.5 for halves). The grid holds at
most 4,096 values; keep the range as narrow as the question allows.
{"type": "number", "instructions": "How many cars are visible?", "min": 0, "max": 50}
The answer:
{"number": 7, "expected": 7.3, "confidence": 0.62, "probabilities": {"6": 0.21, "7": 0.62, "8": 0.14}}
number is the most probable value on the grid and confidence its probability.
expected is the probability-weighted mean, a fractional estimate even on a grid of
whole numbers. probabilities lists the values above 0.001, keyed by the value as a
string, so they can sum to less than 1. Number questions work on text, images and video
frames, alone with the number tool or beside other questions in decide,
decide_many and decide_video.
Points and boxes
A point or box question asks where something is in an image: point gives one spot,
box a rectangle around the thing. The instructions say what to find, as a phrase ("the
yellow package"), a description with conditions ("the person standing nearest the
door") or a question ("Where is the crack?"), and can refer to the state and to the
other images ("the part that differs from the reference image").
{"type": "box", "instructions": "Draw a box around the damaged package.", "image": "belt"}
| Field | |
|---|---|
instructions |
What to find. |
image |
The id of the image to look in (a string). Needed with two or more images; with one, that one is used. A reference by number ("Picture 2") goes in the instructions. |
units |
pixels (the default): pixels of the image as sent. normalized: fractions of the width and the height, from 0 to 1. |
A point or box question needs at least one image in the call. The point and box tools
require images and check image before any request is made.
The answers:
{"x": 812, "y": 455, "present": 0.97, "confidence": 0.91, "image": "belt", "width": 1920, "height": 1080}
{"x1": 300, "y1": 120, "x2": 520, "y2": 410, "present": 0.95, "confidence": 0.88, "image": "belt", "width": 1920, "height": 1080}
| Field | |
|---|---|
x, y |
The point, from the image's top-left corner, x to the right and y down: whole pixels, or fractions with normalized units. |
x1, y1, x2, y2 |
The box: its top-left corner (x1, y1) and bottom-right corner (x2, y2), in the same units. |
present |
The probability that what the instructions describe is in the image at all. Under a low present the place is only a best guess for something probably not there. |
confidence |
The probability that the place is right if the thing is there: for a point, that it lies on the thing; for a box, that it overlaps the true box by at least half (an intersection over union of 0.5 or more). |
image, width, height |
The image looked in and its size in pixels, to draw or convert the answer without another lookup. |
Every field is always there, so the shape never changes. Act on an answer when both
present and confidence are high, and hand the image to a person when either is low.
The thresholds are yours. When several things match ("the yellow package" with three on
the belt), the answer is the best match; the instructions can say which one ("the
leftmost yellow package"). Point and box questions mix with the other types in
decide, and in decide_many each item can carry its own images, such as one photo per
item.
Video (frames)
Only frames are sent: no video file, video URL or sound ever reaches the API. A video is the frames a camera or a script took, each with its time, and the model reads them as one video, in order and with their times:
{
"id": "dock",
"fps": 2,
"frames": [
{"t": 30.0, "media_type": "image/jpeg", "data": "<base64>"},
{"t": 30.5, "media_type": "image/jpeg", "data": "<base64>"}
]
}
| Field | |
|---|---|
id |
Unique within the call, across images and videos. The model sees the video as "Video " (its place in videos, from 1) with this id beside it, so questions can refer to it by either. |
frames |
The frames in time order, all of one size: t in seconds, larger than the frame before; media_type image/jpeg, image/png or image/webp; data the image as base64. |
fps |
A cap on the frames read a second: 2 when left out, at most 4. |
start, end |
A window in seconds on the frames' own times; every frame when left out. |
On your own machine a video can also be a file,
{"id": "dock", "path": "~/clips/dock.mp4", "fps": 2, "start": 30, "end": 45}. The
server samples it into JPEG frames on that machine with ffmpeg, at fps within start
and end (seconds from the video's first frame), at most max_frames of them (32 when
left out, up to 128). A window that holds more frames at that rate is cut there, and the
result's warnings say so. The frames are then sent as above; the file itself and its
sound stay on the machine. ffmpeg and ffprobe must be on the PATH the client starts
the server with (set PATH in the server's env if they are not); without them a
path video is refused with an error that says so, and frames still work. Over HTTP the
server never reads a path.
Send as few frames as the question needs: one or two for one item on a sorting line,
about one a second for an incident such as a fall or smoke, about two a second for
counting what passes. The API publishes its video limits with the models (models).
Over HTTP the base64 data of the images and the frames of one call share the 3 MiB above.
A decide_video result carries answers, model and input_tokens as decide does,
plus video_frames and video_seconds (what the API read), and warnings when the API
read a video at a lower rate or a smaller size than sent, or a file's window was cut.
Errors
A failed call is a tool result with isError true and the error as structured content:
{
"error_type": "invalid_request_error",
"message": "Too many score levels. Must have at most 10 levels.",
"reason": "question 'score': a score takes up to 10 levels, got 11",
"status": 400,
"request_id": "req_..."
}
reason is the API's explanation in words, so the agent can correct the call. A
request refused under the usage policy has error_type refusal_error and its
category. A 422 adds entries; a 429 or 529 adds retry_after in seconds.
Temporary failures (408, 429, 5xx, timeouts, dropped connections) are retried twice
before an error is reported; 400, 401, 402, 403, 404 and 422 are never retried.
Configuration
| Variable | Default | |
|---|---|---|
DECISIONNODE_API_KEY |
none | The API key, on stdio. Over HTTP each caller's own key is used and never this one. |
DECISIONNODE_BASE_URL |
https://api.decisionnode.com |
The API the tools call. Must be https://; plain http:// is accepted only for a loopback address (localhost, 127.0.0.1, ::1), and any other is refused at startup. |
DECISIONNODE_MODEL |
decisionnode-moe |
The default model. |
DECISIONNODE_MCP_RATE_LIMIT |
120 over HTTP, 0 (off) on stdio |
Requests a minute per key on each server process; 0 turns it off. A negative or non-numeric value is refused at startup. Several processes each count on their own. At most 10,000 keys are tracked per process; when the table is full, a new key replaces one whose minute has passed, and if every tracked key has called within the minute the new key's call is refused with the rate-limit error. |
MCP_TRANSPORT |
stdio |
Or http; the --transport flag wins. |
MCP_HOST, MCP_PORT, MCP_PATH |
127.0.0.1, 8000, / |
HTTP only; the --host, --port and --path flags win. |
Over HTTP the server accepts any well-formed bearer value and refuses a request without one (401). The API decides whether the key is valid. A key the API has just answered 401 for is refused for the next 60 seconds with the same error result, and no request is made; the server holds only the key's SHA-256 digest, in memory. The key is never written to a log.
Development
Requires Python 3.10 or later and uv.
git clone https://github.com/Bynn-Intelligence/decision-node-mcp-server
cd decision-node-mcp-server
uv sync
uv run bynn-decisionnode-mcp # stdio
uv run bynn-decisionnode-mcp --transport http # http://127.0.0.1:8000/
The tests run against the mock API server of the
DecisionNode SDK conformance suite
and never call a real endpoint. Clone the suite beside this repository, or point
DECISIONNODE_CONFORMANCE_DIR at it:
git clone https://github.com/Bynn-Intelligence/decision-node-conformance ../decision-node-conformance
uv run pytest
The video file tests need ffmpeg and ffprobe and are skipped without them. After a
deliberate change to a tool's schema or result, regenerate the golden files with
UPDATE_GOLDEN=1 uv run pytest tests/test_golden.py.
License
MIT. See LICENSE.
Metadata
Release files for bynn-decisionnode-mcp 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| bynn_decisionnode_mcp-0.1.2.tar.gz | 35.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| bynn_decisionnode_mcp-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 79.2 kB
Release files / bynn_decisionnode_mcp-0.1.2.tar.gz
| Download URL | bynn_decisionnode_mcp-0.1.2.tar.gz |
|---|---|
| Size | 35.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b8d09c0521284778ca4715f5e30d57ebd7b0eafc593e45c4ce052f705d0cf959
|
|
BLAKE2b-256 checksum How to use checksums |
c63160ab66b8da3ce721d2784934f25488e2bae40c2774e52ba3ff3bc28dd2dc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 13, 2026.
Transparency logRelease files / bynn_decisionnode_mcp-0.1.2-py3-none-any.whl
| Download URL | bynn_decisionnode_mcp-0.1.2-py3-none-any.whl |
|---|---|
| Size | 44.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
293330db07da42c305edc048bb2ed672422c3e2740be379e25a1ca6037b7ad8a
|
|
BLAKE2b-256 checksum How to use checksums |
343df6000ac60551e3777ef32f6a3270d63fbe9b46d0efcc2de9631fd25ca531
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 13, 2026.
Transparency log