jinn exposes 21 tools by default, or 22 when started with
--shell-mode=sandboxed or --shell-mode=unsafe. The protocol accepts exactly
one JSON object of at most 16 MiB and rejects duplicate keys, trailing values,
unknown fields, invalid types, and unknown tool arguments.
echo '{"tool":"read_file","args":{"path":"main.go"}}' | jinnEvery text tool returns {"ok": true, "result": "..."} on success or {"ok": false, "error": "..."} on failure. Tools can also attach structured content and meta fields for images, checksums, truncation, diffs, and other machine-readable details.
echo '{"tool":"web_fetch","args":{"url":"https://example.com","reader":"defuddle"}}' | jinn| Argument | Type | Default | Meaning |
|---|---|---|---|
url |
string | required | Absolute HTTP(S) URL. |
raw |
bool | false |
Return direct response content. |
reader |
string | configured | jina, defuddle, or auto. |
render |
string | never |
never, auto, or always. |
render_wait |
string | load |
load or networkidle when rendering. |
format |
string | markdown |
markdown, headings, or links projection. Raw content supports only markdown. |
start_line |
integer | 0 |
Zero-based line offset within the selected projection. |
max_bytes |
integer | 0 |
Maximum projected UTF-8 bytes; zero is unlimited. |
max_lines |
integer | 0 |
Maximum projected lines; zero is unlimited. |
The text result is projected document content. Metadata uses snake case for document fields plus format, truncated, truncated_by, total_bytes, output_bytes, total_lines, output_lines, max_bytes, max_lines, start_line, and next_start_line. Private/local addresses and redirect hops are denied unless JINN_WEB_ALLOW_PRIVATE_NETWORKS=true is intentionally set at process start.
echo '{"tool":"web_search","args":{"query":"Go release notes","max_results":5}}' | jinn| Argument | Type | Default | Meaning |
|---|---|---|---|
query |
string | required | Search terms. |
max_results |
integer | 10 |
1 through 50 normalized results. |
category |
string | omitted | Provider category. |
include_domains |
string array | omitted | Hostname filters. |
start_published_date |
string | omitted | RFC3339 or YYYY-MM-DD lower bound. |
include_highlights |
bool | true |
Include provider highlights where supported. |
highlight_sentences |
integer | 3 |
Requested highlight sentence count, 1 through 10. |
Search uses Brave by default (BRAVE_API_KEY) or Exa when JINN_WEB_SEARCH_PROVIDER=exa (EXA_API_KEY). max_results sizes one provider request; it is not multi-page provider pagination. Its text is compact JSON with query and results; metadata contains provider and count.
The full response type includes optional fields that carry structured metadata:
| Field | Type | Description |
|---|---|---|
ok |
bool | true on success, false on error |
result |
string | Text tool output (present when ok: true for text responses) |
content |
array | Structured content blocks, currently used for detected images from read_file |
meta |
object | Structured metadata such as truncation info, checksums, diffs, stdout/stderr, and compression details |
error |
string | Error message (present when ok: false) |
error_code |
string | Stable error category on structured errors |
suggestion |
string | One-sentence next-step hint on structured errors |
classification |
string | Exit-code class set by run_shell: success, expected_nonzero, error, timeout, signal |
risk |
string | Pre-execution risk set by run_shell: safe, caution, dangerous |
request_id |
string | Echoes the caller-supplied top-level request_id |
suggestion is present on errors from any tool when jinn can offer a specific recovery action. Always read it before retrying.
These tools read, write, and edit files. All file paths are confined to the working directory. See Security for rooted access, mutation preconditions, and locking.
Read a file with line-numbered output.
echo '{"tool":"read_file","args":{"path":"main.go"}}' | jinnParameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
path |
string | Yes | -- | File path relative to working directory |
start_line |
int | No | 1 |
First line to return |
end_line |
int | No | start_line + 1999 |
Last line to return |
tail |
int | No | 0 (disabled) |
Return last N lines. Overrides start_line/end_line |
line_numbers |
bool | No | true |
Prefix each output line with a right-justified line number. Set false for raw content without numbering. |
truncate |
string | No | "head" |
Strategy when windowed output exceeds the line limit: head (keep first N lines, paginate with start_line), tail (keep last N lines, useful for logs), middle (keep both ends, elide center), none (no line-level truncation, byte cap still applies), smart (brace-depth heuristic — cuts at block boundaries for C-syntax files .go/.rs/.ts/.js/.java/.c/.cpp/.h/.hpp/.tsx/.jsx, falls back to head for others). |
include_checksum |
bool | No | false |
Include a SHA-256 checksum in meta.sha256 |
if_checksum |
string | No | -- | Skip the body when the current checksum matches this value (returns {"unchanged":true,...}). On write_file/edit_file the same arg is a stale-write guard instead. |
Notes:
- Files larger than 50 MB are rejected.
- Empty text files are valid and return an empty result.
- PDF files return
ok: falsewithsuggestion: "convert the PDF to text first (pdftotext, pdftk, or a cloud OCR service) and read the text file". Content is never returned. - Image files are detected by content rather than extension — a
.pngrenamed without an extension is still identified as an image. Detected images return a base64-encoded content block with the correct MIME type (image/png,image/jpeg, etc.). SVG files returnimage/svg+xml. Pass the result directly to a vision model. - Binary files (null byte in first 512 bytes) return
[binary file: N bytes — use stat_file for metadata or skip content reads]as a success result (not an error). - Sequentially truncated output appends:
[Showing lines X-Y of Z. Use start_line=N to continue. Remainder saved to <path>.]. It also returns an exactnext_call. Tail and middle truncation omit unsafe continuations and recommend a narrower window. include_checksum:truereturns the SHA-256 required by secure mutation calls. See Security: Mutation Preconditions.
Read lines 10 through 20:
echo '{"tool":"read_file","args":{"path":"main.go","start_line":10,"end_line":20}}' | jinnRead the last 5 lines:
echo '{"tool":"read_file","args":{"path":"main.go","tail":5}}' | jinnRead with a checksum, then skip the body on the next call if the file has not changed:
echo '{"tool":"read_file","args":{"path":"go.mod","include_checksum":true}}' | jinn
echo '{"tool":"read_file","args":{"path":"go.mod","if_checksum":"<sha256-from-meta>"}}' | jinnRead multiple files in a single call.
echo '{"tool":"multi_read","args":{"files":[{"path":"main.go"},{"path":"go.mod"}]}}' | jinnParameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
files |
array | Yes | -- | List of file read requests, 1–20 entries |
Each file entry:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
path |
string | Yes | -- | File path relative to working directory |
start_line |
int | No | 1 |
First line to return |
end_line |
int | No | start_line + 1999 |
Last line to return |
tail |
int | No | 0 (disabled) |
Return last N lines. Overrides start_line/end_line |
line_numbers |
bool | No | true |
Prefix each output line with a right-justified line number |
truncate |
string | No | "head" |
Truncation strategy: head, tail, middle, none, or smart (brace-depth heuristic for C-syntax files) |
Notes:
- Returns JSON with
files(path→content),errors(path→error detail),truncation(path→metadata), andnext_callsfor any truncated file or unprocessed tail of the input batch. - Partial success: if some files fail, they appear in
errorswhile successful reads still return infiles. - Only returns
ok: falseif ALL files fail. - Binary/image files are reported in
errorswitherror_code: "binary_file"— useread_filefor single-image viewing. - Empty text files are returned in
fileswith an empty string value. - Per-file windowing: each file entry supports independent
start_line/end_line/tail/truncate. - Duplicate paths: last entry wins.
Read with per-file windowing:
echo '{"tool":"multi_read","args":{"files":[{"path":"main.go","start_line":1,"end_line":10},{"path":"go.mod","tail":5}]}}' | jinnMixed success (some files missing):
echo '{"tool":"multi_read","args":{"files":[{"path":"main.go"},{"path":"nonexistent.go"}]}}' | jinnWrite content to a file atomically.
echo '{"tool":"write_file","args":{"path":"hello.txt","content":"Hello, world.\n","if_absent":true}}' | jinnParameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
path |
string | Yes | -- | File path relative to working directory |
content |
string | Yes | -- | Content to write |
dry_run |
bool | No | false |
Preview the write without modifying the file |
if_checksum |
string | Conditional | -- | Required to replace an existing file. SHA-256 from a previous read (meta.sha256); mismatch rejects the write with error_code: "stale_file" |
if_absent |
bool | Conditional | -- | Must be true to create a new file |
Notes:
- Writes are atomic: jinn writes to a hidden temp file, syncs to disk, then renames it into place. See Security: Atomic Writes.
- Parent directories are created automatically.
- If the file already exists, jinn preserves its permissions.
- Existing targets require the checksum from a prior read. See Security: Mutation Preconditions.
- Pass
if_checksum(fromread_filewithinclude_checksum: true) to reject the write when the file changed since your read — the in-process check above cannot cross jinn invocations;if_checksumcan. dry_runon an existing file returns a unified diff. On a new file, it returns[dry-run] would create path (N bytes).
Preview a write without applying it:
echo '{"tool":"write_file","args":{"path":"hello.txt","content":"new content\n","dry_run":true}}' | jinnReplace an exact text match in a file.
echo '{"tool":"edit_file","args":{"path":"main.go","old_text":"fmt.Println","new_text":"log.Println","if_checksum":"<sha256-from-read_file>"}}' | jinnParameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
path |
string | Yes | -- | File path relative to working directory |
old_text |
string | Yes | -- | Text to find (must be unique in the file) |
new_text |
string | Yes | -- | Replacement text |
dry_run |
bool | No | false |
Preview the edit as a unified diff |
fuzzy_indent |
bool | No | false |
Detect indentation at match site, re-indent new_text to match |
show_context |
int | No | 0 |
Return N surrounding lines around the edit with * markers on changed lines |
if_checksum |
string | No | -- | SHA-256 from a previous read (meta.sha256). Mismatch rejects the edit with error_code: "stale_file" |
Notes:
old_textcannot be empty. An empty string produces an error with a suggestion to include the first line of the file when you need to prepend content.old_textmust match exactly once. Zero matches or multiple matches both produce an error. On multi-match, the error includes line numbers for up to 10 locations.- If exact match fails, jinn tries fuzzy matching (normalizes whitespace, smart quotes, Unicode dashes). Fuzzy match is used only when it produces exactly one candidate.
- If
old_textandnew_textare equivalent (including after fuzzy normalization), jinn returns an error rather than silently writing an unchanged file. - jinn preserves BOM markers and CRLF line endings through the edit.
- When both exact and fuzzy fail, the error message includes the nearest line by character overlap to help you locate the right text.
dry_runreturns a unified diff with 3 lines of context.
Edit with context lines:
echo '{"tool":"edit_file","args":{"path":"main.go","old_text":"old","new_text":"new","show_context":2,"if_checksum":"<sha256-from-read_file>"}}' | jinnPreview an edit:
echo '{"tool":"edit_file","args":{"path":"config.yaml","old_text":"port: 8080","new_text":"port: 9090","dry_run":true}}' | jinnApply multiple edits across files. jinn validates all edits before writing, then writes each changed file atomically.
echo '{"tool":"multi_edit","args":{"edits":[{"path":"a.go","old_text":"foo","new_text":"bar","if_checksum":"<a-sha256>"},{"path":"b.go","old_text":"baz","new_text":"qux","if_checksum":"<b-sha256>"}]}}' | jinnParameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
edits |
array | Yes | -- | Array of edit objects (see below) |
Each edit object:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
path |
string | Yes | -- | File path relative to working directory |
old_text |
string | Yes | -- | Text to find (must be unique in the file) |
new_text |
string | Yes | -- | Replacement text |
if_checksum |
string | No | -- | SHA-256 for this target from read_file; a mismatch rejects the whole batch as stale |
fuzzy_indent |
bool | No | false |
Detect indentation, re-indent new_text |
show_context |
int | No | 0 |
Return N surrounding lines with markers |
Notes:
- Validate first. jinn validates every edit (rooted path, checksum, and match uniqueness) before applying any. If any edit fails validation, zero edits are applied.
- Cross-call guard. Put
if_checksumon each edit derived from an earlier read. jinn also rechecks preflight bytes immediately before every write. - Per-file atomic writes. Each changed file is written via temp+rename. If a write fails after validation, earlier successful file writes are not rolled back; the error enumerates them with undo ids (
partial apply — N of M files already written: ... (undo id=...)) so you can restore or retry the remainder. old_textcannot be empty in any edit entry. An empty value returns an error immediately, before any edits are applied.- Overlap detection. Edits targeting overlapping byte ranges in the same file are rejected in the validation phase. The error names which two edit indices conflict. Split them into separate
multi_editcalls or combine them into a single edit. - If any edit's
old_textandnew_textare equivalent (including after fuzzy normalization), jinn returns an error and applies nothing. - Each edit uses the same matching and normalization rules as
edit_file. - Multiple edits to the same file are applied sequentially in array order. Later edits in the array see the file as modified by earlier ones (chained edits).
Apply a Codex-style patch payload.
echo '{"tool":"apply_patch","args":{"patch":"*** Begin Patch\n*** Update File: main.go\n@@\n-old\n+new\n*** End Patch","if_checksums":{"main.go":"<sha256-from-read_file>"}}}' | jinnParameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
patch |
string | Yes | -- | Patch text starting with *** Begin Patch and ending with *** End Patch |
if_checksums |
object | No | -- | Map of target path to SHA-256 from read_file; any mismatch rejects the patch before writes |
dry_run |
bool | No | false |
Validate and preview changes without writing |
Supported operations:
| Operation | Behavior |
|---|---|
*** Add File: path |
Creates a new file. Fails if the target already exists. |
*** Delete File: path |
Deletes an existing file. Fails if the target does not exist. |
*** Update File: path |
Applies hunk-based edits using context, removed, and added lines. |
Notes:
- All operations are validated before writing starts.
- Supply
if_checksumsfor update/delete targets derived from earlier reads. jinn also rejects targets whose bytes or existence change between validation and write. - Writes are per-file atomic. If a later write fails after validation, earlier successful writes are not rolled back; the error enumerates them with undo ids so you can
undoeach or retry the remainder. - Update hunks support progressive fuzzy matching when exact context fails.
Browse and restore file snapshots. jinn captures a snapshot automatically before every write_file, edit_file, multi_edit, and apply_patch mutation. Mutations of existing files whose pre-mutation content exceeds 5 MiB are rejected before writing so no successful mutation silently lacks an undo snapshot.
echo '{"tool":"undo","args":{"action":"list"}}' | jinnParameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
action |
string | Yes | -- | list, preview, restore, or clear |
id |
string | For preview, restore |
-- | Snapshot ID. A unique prefix of the ID also works. |
if_checksum |
string | No | -- | For restore, SHA-256 of the current target; mismatch rejects the restore as stale |
limit |
int | No | all | Maximum number of snapshots to return for list |
Actions:
| Action | Required args | Effect |
|---|---|---|
list |
-- | List snapshots newest-first with ID, file path, timestamp, and operation |
preview |
id |
Unified diff between the snapshot and the current file contents |
restore |
id |
Revert the file to the snapshot contents via an atomic write |
clear |
-- | Delete all snapshot history |
Notes:
- Snapshots are recorded automatically -- there is no "save snapshot" action.
- History is bounded; the oldest snapshots are evicted once the limit is reached.
- Existing files larger than 5 MiB are rejected for mutating operations with
file_too_large; the original file remains unchanged. previewandrestoreaccept any unique prefix of a snapshot ID, so the short form shown bylistworks directly.
List snapshots, then restore one:
echo '{"tool":"undo","args":{"action":"list"}}' | jinn
echo '{"tool":"undo","args":{"action":"restore","id":"a1b2c3","if_checksum":"<current-target-sha256>"}}' | jinnCompare two files and return a unified diff.
echo '{"tool":"diff_files","args":{"path_a":"old.go","path_b":"new.go"}}' | jinnParameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
path_a |
string | Yes | -- | First file to compare |
path_b |
string | Yes | -- | Second file to compare |
context_lines |
int | No | 3 |
Lines of context around each change |
Notes:
- Uses the same diff engine as the
edit_filedry-run preview. metacarriesis_identical(bool) andfirst_changed_line(int).- Both paths are confined to the working directory.
Search file contents with regex.
echo '{"tool":"search_files","args":{"pattern":"func main"}}' | jinnParameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
pattern |
string | Yes | -- | Regex pattern (or fixed string when literal: true) |
path |
string | No | "." |
Directory to search in |
format |
string | No | "text" |
Output format: text, json, or filenames |
include |
string | No | -- | Glob filter on filenames (e.g., "*.go") |
literal |
bool | No | false |
Treat pattern as a fixed string rather than a regex. Passes -F to grep / --fixed-strings to rg. |
max_matches |
int | No | 500 |
Maximum number of matches to return. When exceeded, response includes truncated: true and total_count. |
offset |
int | No | 0 |
Zero-based result offset. Requires format: "json". |
context_lines |
int | No | 0 |
Surrounding lines to include per match |
case_insensitive |
bool | No | false |
Case-insensitive matching |
Notes:
- jinn uses
rg(ripgrep) if available, otherwise falls back togrep -r -n. - These directories are always excluded:
.git,node_modules,vendor,__pycache__,.cache,dist,build. - Without
literal: true, the pattern is validated as a regex before any search runs; invalid patterns return an error immediately. - Output limits: 200 characters per line truncation per match. Default cap: 500 matches.
Structured results for programmatic use:
echo '{"tool":"search_files","args":{"pattern":"func \\w+Handler","format":"json","include":"*.go"}}' | jinnformat: "json" returns an object with results, offset, count and
truncation fields, and an exact next_call when another page exists. Each match
contains file, line, column, text, and optional
context_before/context_after fields.
List files with match counts:
echo '{"tool":"search_files","args":{"pattern":"TODO","format":"filenames"}}' | jinnFind files by glob pattern.
echo '{"tool":"find_files","args":{"pattern":"*.go","path":"internal"}}' | jinnParameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
pattern |
string | Yes | -- | Glob pattern such as *.go, **/*.json, or src/**/*_test.go |
path |
string | No | "." |
Directory to search in |
limit |
int | No | 1000 |
Maximum number of results before truncation |
offset |
int | No | 0 |
Zero-based result offset for continuation |
Notes:
- Uses a native bounded walker;
.gitignoreis not interpreted. - Patterns without
/match basenames. Slash patterns match normalized relative paths;*stays within one segment and**spans segments. - Returns one JSON document with
files,offset,truncated,total_count,total_count_exact,limit_used,backend, and optionalhintand exactnext_callfields. - Excludes hidden paths,
.git,.ssh,.aws,.gnupg,.envvariants,node_modules,vendor,__pycache__,.cache,dist, andbuildat every depth.
Replace regex matches across explicit files or glob patterns.
echo '{"tool":"search_replace","args":{"pattern":"oldName","replacement":"newName","files":"*.go","dry_run":true}}' | jinnParameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
pattern |
string | Yes | -- | Regex pattern to search for |
replacement |
string | Yes | -- | Replacement text. Supports $1, $2, etc. for capture groups. |
files |
string or array | Yes | -- | Target path, glob pattern, directory, or array of paths/globs. Max 50 resolved files. |
include |
string | No | -- | Optional glob filter applied after target expansion, e.g. "*.go" |
if_checksums |
object | No | -- | Map of target path to SHA-256 from read_file; mismatched files are rejected as stale |
case_insensitive |
bool | No | false |
Match case-insensitively |
multiline |
bool | No | true |
Enable ^/$ line-boundary matching |
dry_run |
bool | No | false |
Preview diffs and match counts without writing |
Notes:
- Each file is validated before any writes are applied.
- jinn rechecks every pending file immediately before writing; use
if_checksumsto extend that guard across separate jinn calls. - Writes are per-file atomic. If a later write fails after validation, earlier successful writes are not rolled back; the error enumerates them with undo ids.
- Binary files are skipped with structured per-file errors.
- Empty
replacementis valid and deletes the matched text.
Get file metadata without reading content.
echo '{"tool":"stat_file","args":{"path":"main.go"}}' | jinnParameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
path |
string | Yes | -- | File path relative to working directory |
Returns:
| Field | Description |
|---|---|
path |
Resolved file path |
type |
file, directory, or special |
size |
Size in bytes |
lines |
Line count (regular files under 50 MB only) |
modified |
Modification time as RFC 3339 |
List files in a directory tree.
echo '{"tool":"list_dir","args":{"path":".","depth":2}}' | jinnParameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
path |
string | No | "." |
Directory to list |
depth |
int | No | 3 |
Maximum recursion depth (clamped to 1--10) |
max_entries |
int | No | 500 |
Maximum number of entries to return (cap: 10000). When exceeded, response includes truncated: true and total_count. |
offset |
int | No | 0 |
Zero-based result offset for continuation |
changed_since |
number | No | 0 |
Unix epoch seconds, including fractions. Only list entries modified strictly after it. |
changed_after |
string | No | -- | RFC3339Nano timestamp. Only list entries modified strictly after it. |
Notes:
- Hidden files and directories (names starting with
.) are excluded. - Output is sorted alphabetically.
- Directory entries are suffixed with
/to distinguish them from files. - Returns one JSON object with
entries,offset,truncated,total_count, andtotal_count_exact; an early traversal stop makes the total inexact. A truncated result includes an exactnext_call.
Run a bash command with a timeout. This tool exists only when an explicit shell mode is selected on the jinn command line.
echo '{"tool":"run_shell","args":{"command":"go test ./..."}}' | jinnParameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
command |
string | Yes | -- | Bash command to execute |
timeout |
int | No | 30 |
Timeout in seconds (max 300) |
dry_run |
bool | No | false |
Print the command without executing it |
force |
bool | No | false |
Execute even when risk classification is dangerous. See Security: Risk Classifier. |
Notes:
--shell-mode=sandboxeduses the host OS sandbox with network disabled, syntheticHOME/TMPDIR, and fails at startup if confinement is unavailable.--shell-mode=unsafeis unconfined and restores only the allowlisted host environment. Both use a fixed workspace cwd, a sanitized snapshot of absolute existing directories from the launch-time hostPATH, and process-group cleanup.- Exit code 124 means the command was killed by the timeout.
- Output format:
[exit: N]\n<output>\n[classification: <class> — <reason>]. - Every response includes
riskandclassificationfields in the envelope. - Dangerous commands (e.g.,
rm -rf,dd,sudo) are blocked and returnok: falsewith asuggestionunlessforce: trueis passed. - The shell environment is scrubbed to a fixed allowlist. See Security: Shell Environment.
- Output over 1 MiB spills to a temp file and is capped at 16 MiB total; excess output kills the process group with
resource_limit. See Security: Output Bounds.
Run with a longer timeout:
echo '{"tool":"run_shell","args":{"command":"go build ./...","timeout":120}}' | jinnPreview without executing:
echo '{"tool":"run_shell","args":{"command":"rm -rf /tmp/test","dry_run":true}}' | jinnExecute a condition-gated plan tree of tool and shell operations in one deterministic engine walk — no model call between nodes. The walk starts at root and follows first-match-wins conditional edges until it reaches a leaf, a dead end, the depth limit, or a blocked mutation.
echo '{"tool":"run_plan","args":{"plan":{"root":"check","nodes":[{"id":"check","commands":[{"shell":"test -f go.mod"}],"edges":[{"when":{"kind":"exitCode","op":"eq","value":0},"to":"build"}]},{"id":"build","commands":[{"shell":"go build ./..."}],"mutates":true}]}}}' | jinnParameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
plan |
object | Yes | -- | The plan tree to execute (fields below) |
The plan object:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
root |
string | Canonical form | -- | ID of the starting node |
nodes |
array | Canonical form | -- | Non-empty plan nodes, each with non-empty commands and optional edges |
steps |
array | Compact form | -- | One to 16 tool operations, expanded into a single plan node; shell steps are not accepted |
parallel |
bool | No | false |
Run compact-form steps concurrently |
mutates |
bool | No | false |
Enable normal Phase 2 mutation gating for compact-form steps |
cwd |
string | No | working dir | Existing directory inside the engine workdir; all shell, tool, and cwd-relative condition paths use it |
max_depth |
int | No | 8 |
Maximum execution depth before the walk stops, including nested run_plan calls |
force |
bool | No | false |
Plan-level gate; with a node's own force, permits dangerous mutations |
Notes:
- Use either
rootplusnodes, orsteps; combining both forms is rejected. Compact steps preserve the same read-only default, operation budget, and mutation classifier as canonical plans. They cannot set node-levelforce, so dangerous mutations remain blocked. - Each command sets exactly one of a non-empty
shellor a knowntool. Unknown tools, empty nodes, and malformed conditions are rejected before the walk starts. - One shared budget permits at most 256 operations across the outer plan and all nested
run_plancalls. Exhausting it stops the walk withstopped_reason: "resource_limit". - Read-only by default: a node without
mutates: trueallows onlysafeshell commands and read-only tools. This includesmemoryactionsrecall/listandundoactionslist/preview; their mutating actions remain blocked. Amutates: truenode runscautionoperations automatically;dangerousones require bothplan.forceand the node'sforce, including nestedtool: "run_shell"andtool: "run_plan"commands. Nested plans inherit the parent's remaining depth and dangerous-mutation authority. A nestedforceargument cannot bypass those gates. - A node cannot combine
parallel: truewithmutates: true; split mutating operations into serial nodes. - Edges are evaluated against the last op's result; a failed or blocked tool reports a nonzero
exit_code. Condition kinds areexitCode,fileExists,jsonPath,numeric,match, andalways;negateapplies uniformly, and JSON equality preserves value types. - A
numeric.extractregex must contain a capture group for the numeric value. An emptymatch.regexis valid and matches every result. - A condition-evaluation error (for example, a path outside the plan sandbox) stops the walk with
stopped_reason: "error"; it never falls through to a later edge. - The result carries the run in
meta.plan_run(transcript,path_taken,stopped_reason, and edge counts). - Full
PlanNode/PlanEdge/ condition reference lives in therun_plansection of AGENTS.md.
Get tool capability metadata. By default this avoids returning the full schema again.
echo '{"tool":"list_tools","args":{}}' | jinnParameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
include_schema |
boolean | No | false |
Also include the compact OpenAI tool schema for runtime discovery |
Use include_schema: true only when the calling agent needs to discover schemas at runtime.
The default jinn --mcp profile intentionally exposes only one MCP tool,
jinn_route, to avoid prompt bloat from listing every jinn tool directly. It
uses the official Go SDK for MCP 2026-07-28. Requests are stateless and carry
the protocol version and client capabilities in _meta. jinn_route recommends
existing jinn tools for a coding-agent task and never executes them.
{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}},"name":"jinn_route","arguments":{"need":"regex replace across many files","include_call":true}}}An MCP client sends those lines over the long-lived jinn --mcp stdin pipe and
reads one response per request. Do not close stdin until the responses arrive.
<!-- equivalent shell payload, without closing the client pipe -->
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}},"name":"jinn_route","arguments":{"need":"regex replace across many files","include_call":true}}}'
The shell payload above is illustrative only; piping it directly to `jinn --mcp`
closes stdin too early for an in-flight response. Use the checked
long-lived driver in [mcp-smoke-test.md](mcp-smoke-test.md).
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
need |
string | Yes | -- | Natural-language task or capability request |
max_tools |
integer | No | adaptive | Explicit maximum recommendations, capped at 8; omit for adaptive cardinality |
include_schema |
boolean | No | false |
Include lean schemas only for returned tools |
include_call |
boolean | No | false |
Include the smallest executable jinn_call argument template and replacement paths relative to its arguments object |
include_mutating |
boolean | No | true |
Allow recommendations for mutating tools |
With max_tools omitted, a high-confidence route returns one match and a close
score returns the top two. Set it explicitly to override that cardinality.
mutating is a maximum-capability classification: it is true when at least one
valid invocation of the tool may mutate files or persistent state. Therefore,
include_mutating:false excludes run_plan even when the particular plan a
caller intends to submit would be read-only.
Returns: the tool result contains structured structuredContent route JSON
and a mirrored text content block for clients that only consume text.
The request above returns:
{
"route_id": "<32 lowercase hex characters>",
"query": "regex replace across many files",
"confidence": "high",
"score_margin": 23,
"adaptive": true,
"matches": [
{
"name": "search_replace",
"description": "Regex-based search and replace across files. Supports capture groups ($1, $2) in replacement, multi-file scope (glob patterns), replace-all (every occurrence), validate-first apply, and per-file atomic writes. Use when edit_file/multi_edit cannot: regex patterns, multi-file bulk changes, or replacing all occurrences. Binary files are skipped automatically.",
"reason": "Matched name tokens, description, parameters, features, task intent.",
"mutating": true,
"risk": "mutating",
"features": ["regex", "capture_groups", "multi_file", "glob_patterns", "replace_all", "dry_run", "case_insensitive", "multiline"],
"call": {
"tool": "search_replace",
"arguments": {"files":"<required>","pattern":"<required>","replacement":"<required>"},
"replace": ["files","pattern","replacement"]
}
}
],
"notes": [
"Recommendation only: jinn_route does not execute tools.",
"Mutating recommendations can change files or persistent state; use dry_run where supported."
]
}Notes:
-
Routing is deterministic and fully local -- no LLM, no network, no persistent state. The same inputs return the same recommendations, confidence, and score margin. The MCP broker adds a fresh opaque
route_idfor call linkage. -
Tools are scored by lexical overlap between the
needand each tool's name, description, parameter names, enum values, and feature tags, plus curated task-intent rules -- "revert my last change" routes toundowithout naming the tool. -
Matches below a relevance floor are dropped rather than padded. A vague
needreturns emptymatchesand a corrective note:{"query": "do the thing", "matches": [], "notes": ["No confident route found. Try a more concrete task, object, or operation name."]} -
Phrase
needas operation + object: "replace one exact string in a single file" routes toedit_file; "get the size and encoding of a file without reading it" routes tostat_file. -
include_mutating: falserestricts recommendations to read-only tools, useful while an agent is in a plan or review phase. -
include_schema: trueattaches a lean schema (parameter descriptions stripped) to each returned match only.
Start the opt-in read-only profile with:
jinn --mcp-profile=read-only --mcpThe profile keeps jinn_route and adds jinn_call, a generic executor whose
tool enum is generated from the canonical read-only tool registry. It forces
shell execution off and rejects mutation-capable tools, memory, and undo
before dispatch. The default jinn --mcp profile remains route-only, so adding
this profile does not change existing MCP clients. Its jinn_route schema
defaults include_mutating to false and runtime always excludes mutating
recommendations.
jinn_call parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
tool |
string | Yes | -- | One of the advertised read-only jinn tools |
arguments |
object | No | {} |
Arguments for the selected tool |
compress |
boolean | No | true |
Apply deterministic context compression to text output |
route_id |
string | No | -- | Opaque 32-character lowercase hex identifier returned by jinn_route; echo it unchanged to link route and call effectiveness records |
Example call:
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}},"name":"jinn_call","arguments":{"tool":"read_file","arguments":{"path":"README.md"},"compress":false,"route_id":"<32 lowercase hex characters>"}}}The result has structured structuredContent with the selected tool, optional
route_id, text result, optional jinn content blocks, and merged meta.
Errors use isError: true plus structured error, error_code, suggestion,
retryable, and an exact next_call when a safe deterministic recovery exists.
A mutation attempt never reaches the engine dispatcher.
Set JINN_MCP_LOG_LEVEL=error|info|debug to enable the private, capped JSONL
effectiveness ledger at $JINN_CONFIG_DIR/jinn/logs/mcp.jsonl or the platform
user-config equivalent. Version 2 records may include route/call linkage,
recommendations, confidence, score margin, result size, truncation, and retry
classification. They never include prompts, tool arguments, paths, or content.
Logging is best effort and does not affect request results.
The read-only profile requires current stateless request metadata. Its legacy
initialize-based traffic is handled by the current SDK and rejected before route
or tool dispatch; only the default profile retains the route-only compatibility
path.
Start the opt-in HTTP transport with the route-only profile:
jinn --mcp-httpIt listens on 127.0.0.1:8788 and serves only POST /mcp. The read-only
profile uses the same profile and allowlist as stdio:
jinn --mcp-profile=read-only --mcp-http 127.0.0.1:8788Each request is stateless and must include these headers. The body is one MCP 2026-07-28 JSON-RPC request or notification:
| Header | Required | Value |
|---|---|---|
Accept |
Yes | application/json, text/event-stream |
Content-Type |
Yes | application/json |
MCP-Protocol-Version |
Yes | 2026-07-28, matching the body _meta |
Mcp-Method |
Yes | The JSON-RPC method, such as server/discover |
Mcp-Name |
tools/call only |
The body params.name value |
Authorization |
When a token is configured | Bearer $TOKEN |
Example discovery request:
curl -sS http://127.0.0.1:8788/mcp \
-H 'Accept: application/json, text/event-stream' \
-H 'Content-Type: application/json' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: server/discover' \
--data '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'Loopback binds do not need a token unless JINN_MCP_HTTP_TOKEN is set. Any
non-loopback address requires both JINN_MCP_HTTP_TOKEN and
JINN_MCP_HTTP_ORIGINS at startup. The origin variable is a comma-separated
list of exact HTTP(S) origins. When origins are configured, every supplied
Origin header, including localhost, must exactly match that list; a request
without Origin remains available to non-browser MCP clients. Missing or
invalid bearer auth returns HTTP 401 with WWW-Authenticate: Bearer; an
unlisted browser origin returns HTTP 403. The SDK's insecure allow-any-origin
option is never enabled, and tokens are not accepted through CLI arguments.
Use a non-loopback bind only on a trusted network or behind a TLS-terminating proxy or tunnel: bearer tokens authenticate requests but do not encrypt them.
HTTP has an explicit 64 KiB header limit and 8 MiB body limit, a five-second
header timeout, no read/write stream deadline, and a two-minute idle timeout.
SIGINT and SIGTERM perform bounded
graceful shutdown. It intentionally does not implement the stdio legacy
initialize compatibility path. See
mcp-smoke-test.md for a
real endpoint check.
jinn --mcp-profile=network --mcp keeps the compact jinn_route and
jinn_call surface. jinn_call is read-only, non-destructive, and open-world:
its web_fetch and web_search requests leave the machine and may consume
provider quota. Web output defaults to uncompressed; local read-only output
keeps compression by default.
jinn mcp list ENDPOINT [--timeout 30s]
jinn mcp inspect ENDPOINT TOOL [connection flags]
jinn mcp call ENDPOINT TOOL [--args JSON] [-a NAME VALUE]... [connection flags]
jinn mcp list --command PATH [--arg ARG]...The explorer accepts only HTTP(S) endpoints or explicit subprocess argv, follows
all tools/list pages, disables HTTP retries, and reaps subprocesses. HTTP
bearer authentication comes only from JINN_MCP_HTTP_TOKEN; it is never printed
or accepted through a command-line option. --args and repeated assignments
merge in command-line order; valid JSON assignment values remain typed. Its
successful call JSON always contains resultType, content,
structuredContent, and isError; a tool-level error still exits zero.
Detect language, framework, and build commands from project config files.
echo '{"tool":"detect_project","args":{"path":"."}}' | jinnParameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
path |
string | No | "." |
Project root to probe |
Returns:
| Field | Description |
|---|---|
languages |
Detected languages (e.g., ["Go", "TypeScript"]) |
build_tool |
Build command (e.g., "go build ./...") |
test_command |
Test command (e.g., "go test ./...") |
linter |
Lint command (e.g., "golangci-lint run") |
config_files |
Config files found (e.g., ["go.mod", ".golangci.yml"]) |
frameworks |
Detected frameworks (e.g., ["Next.js"]) |
Notes:
- Probes for:
go.mod,package.json,bun.lockb,Cargo.toml,pyproject.toml,setup.py,requirements.txt,composer.json,Makefile,Taskfile.yml. - Secondary detection:
tsconfig.jsonupgrades JS to TypeScript.package.jsonscripts override build/test/lint commands.next.config.jsornext.config.mjstriggers Next.js detection.
Persist key/value pairs across jinn invocations, scoped per project by default.
echo '{"tool":"memory","args":{"action":"save","key":"project.notes","value":"auth service uses JWT RS256"}}' | jinnParameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
action |
string | Yes | -- | save, recall, list, forget, or gc |
key |
string | Depends | -- | Key name. Required for save, recall, and forget. Charset: [a-zA-Z0-9_.-], max 128 chars. |
value |
string | For save |
-- | Value to store. Max 16 KiB. |
scope |
string | No | "project" |
Omit for the auto-detected current project. Use "global", "project", "task", or "agent". |
scope_id |
string | Depends | auto project root | Explicit project path, task ID, or agent name. Required for task and agent; invalid with global. |
kind |
string | No | "fact" |
Memory type: "fact", "directive", or "lesson" |
pin |
bool | No | false |
Keep this entry during memory garbage collection |
expires_in |
string | No | -- | Relative expiry duration such as "12h", "7d", or "2w" |
include_values |
bool | For list |
false |
Include values and metadata in list output instead of keys only |
request_id |
string | No | -- | Idempotency key for mutating actions. The top-level request ID is copied here automatically when present. |
Returns by action:
| Action | Success result |
|---|---|
save |
"saved: <key>" |
recall |
The stored value string |
list |
{"keys": [...], "count": N} or {"entries": [...], "count": N} with include_values: true |
forget |
"forgotten: <key>" (idempotent; not found is success) |
gc |
{"deleted": N, "idempotency_deleted": N, "scope": "..."} |
Notes:
- Stored in a SQLite database at
~/Library/Application Support/jinn/memory.dbon macOS (~/.config/jinn/memory.dbon Linux). Override the base dir withJINN_CONFIG_DIR(the DB lives at$JINN_CONFIG_DIR/jinn/memory.db). - Keys are scoped by
(scope, scope_id). With noscope, jinn usesprojectand auto-detects the nearest.gitancestor of its working directory, falling back to the working directory itself. scope: "project"accepts an optionalscope_idpath.scope: "task"andscope: "agent"require a caller-suppliedscope_id.scope: "global"cannot have ascope_id.- The DB directory is created with mode
0700. Writes use WAL journaling with a 5s busy timeout for cross-process safety. - Legacy
memory.jsonfiles are not imported automatically. - Read-only
recallandlistdo not create or migrate a database. Expired non-pinned rows are hidden immediately, and new saves rejectpin:truecombined with expiry. recallon a missing key returnsok: falsewithsuggestion: "use action=\"list\" to see available keys".gcremoves expired, unpinned memories and old idempotency rows. Passscopeto restrict memory cleanup to one scope bucket.
Save a value:
echo '{"tool":"memory","args":{"action":"save","key":"db.host","value":"localhost"}}' | jinnList all keys:
echo '{"tool":"memory","args":{"action":"list"}}' | jinnList values and metadata:
echo '{"tool":"memory","args":{"action":"list","include_values":true}}' | jinnSave an expiring task-scoped lesson:
echo '{"tool":"memory","args":{"action":"save","key":"migration.lesson","value":"retry failed rows after backfill","kind":"lesson","scope":"task","scope_id":"backfill-2026-06","expires_in":"14d"}}' | jinnForget a key:
echo '{"tool":"memory","args":{"action":"forget","key":"db.host"}}' | jinnGarbage-collect expired memories and old idempotency rows:
echo '{"tool":"memory","args":{"action":"gc"}}' | jinnQuery a language server for semantic information at a source location.
echo '{"tool":"lsp_query","args":{"action":"hover","path":"main.go","line":12,"character":5}}' | jinnParameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
action |
string | Yes | -- | definition, references, hover, symbols, diagnostics, or rename |
path |
string | Yes | -- | File path relative to working directory |
line |
int | Unless symbol |
-- | 1-based line number of the symbol |
character |
int | No | -- | 1-based character offset within the line |
symbol |
string | No | -- | Identifier name. Supply instead of line/character and jinn resolves the declaration position from the file's document symbols (errors if the name is missing or not unique); with an explicit line it resolves just the column. |
new_name |
string | For rename |
-- | New identifier name. Required when action is rename. |
Supported extensions and servers:
| Extension | Server binary | Install hint |
|---|---|---|
.go |
gopls |
go install golang.org/x/tools/gopls@latest |
.rs |
rust-analyzer |
rustup component add rust-analyzer |
.py |
pylsp |
pip install python-lsp-server |
.ts, .tsx, .js, .jsx |
typescript-language-server |
npm install -g typescript-language-server typescript |
.c, .h, .cpp, .cc, .cxx, .hpp |
clangd |
bundled with LLVM or via system package manager |
.java |
jdtls |
install the Eclipse JDT Language Server |
.lua |
lua-language-server |
https://github.com/LuaLS/lua-language-server |
.zig |
zls |
https://github.com/zigtools/zls |
Returns by action:
| Action | Result format |
|---|---|
definition |
file:line:col of the definition site |
references |
One file:line:col per reference, up to 100. Truncation noted with [truncated: showing N of M]. |
hover |
Documentation / type signature string from the server |
symbols |
Kind Name (line:col) table for every symbol in the file |
diagnostics |
One file:line:col severity source/code: message line per diagnostic |
Notes:
- The language server is started, queried, and torn down within a single call. There is no persistent daemon.
diagnosticsuses the LSP pull diagnostics request and may depend on server support.renamereturns a preview of changes; it does not modify files.- Hard timeout: 10 seconds per query. Slow server startups may cause timeouts on cold runs.
- If the server binary is not on
PATH,ok: falseis returned with asuggestioncontaining the install command. - Path must be inside the working directory (normal path security applies).
Run up to 20 semantic queries while starting each required language server only once. Results preserve input order and report per-item success or structured failure, so one bad query does not discard the others.
echo '{"tool":"lsp_batch","args":{"queries":[{"action":"symbols","path":"internal/jinn/engine.go"},{"action":"diagnostics","path":"main.go"}]}}' | jinnEach query accepts the same fields as lsp_query. The JSON result contains
results, succeeded, failed, and server_starts; every result item contains
its zero-based index, ok, and either result or error with
error_code/suggestion. Queries sharing a server binary reuse one initialized
session. The total batch budget is the per-query timeout multiplied by its group
size, capped at two minutes.
Get definition:
echo '{"tool":"lsp_query","args":{"action":"definition","path":"cmd/jinn/main.go","line":15,"character":12}}' | jinnList symbols:
echo '{"tool":"lsp_query","args":{"action":"symbols","path":"internal/jinn/engine.go"}}' | jinn