Guide

MCP Server

gori ships a built-in MCP (Model Context Protocol) server. Instead of embedding a chat window in the TUI, gori exposes its project over a clean tool interface so any MCP-capable agent (Claude, Codex, Grok, and others) can read your traffic and drive the tools.

agent Β· gori over MCP

youFind an IDOR on the users API and log it.

β†’ list_history path~/v1/users status:200

← 14 flows, customer and admin tokens

β†’ send_request GET /v1/users/2 Β· customer token

← 200 {"id":2,"email":"other-tenant@example.com"}, not the caller's row

β†’ create_issue "IDOR on /v1/users/{id}" severity:high

βœ“ Issue logged; the request is saved as a Repeater session for repro.

gori mcp

The server speaks JSON-RPC 2.0 over stdio: STDOUT carries the protocol, STDERR carries logs. Tool results include both backward-compatible text and MCP structuredContent when the payload is JSON.

Choosing a Project

cd /path/to/my-repository && gori mcp # path-binds this Git workspace to its own gori project
gori mcp --project my-engagement   # serve a named project's database
gori mcp --db /path/to/project.db  # serve a specific database file
gori mcp --use-active-project      # explicitly serve the active TUI/MRU project
gori mcp --no-project              # force unbound even inside a Git workspace

With no explicit selector, gori discovers the nearest Git root and binds its canonical path to an isolated project. The binding prevents two repositories with the same directory name from sharing a database.

Outside a Git workspace (the common case when an AI client spawns MCP from a home or app directory), the server starts unbound: the MCP handshake and tool list succeed immediately, but traffic tools (list_history, send_request, …) return NO_PROJECT until the agent calls list_projects, create_project (auto-binds when unbound), or switch_project. Unbound mode never silently opens the active TUI or MRU project β€” that requires the explicit --use-active-project opt-in (or --project / --db / GORI_MCP_PROJECT / GORI_MCP_DB).

If the selected project cannot be opened β€” a database that is missing, corrupt, or unreadable, a project name that no longer exists β€” the server still completes the handshake and starts unbound rather than exiting. The reason is written to stderr, repeated in the handshake instructions, returned with every NO_PROJECT tool error, and reported as bind_error by project_info, so the agent can call list_projects and switch_project to recover without a restart.

Call project_info before using data. It reports bound, the selected project, database path, workspace root, and selection source.

Read-Only Mode

By default the server also exposes action tools that send live requests and write issues. To expose only the read tools (safe for handing a project to an untrusted agent), start it read-only:

gori mcp --read-only

A read-only server also keeps no writer. It never writes the project it serves and runs no background indexing β€” SQLite allows a single writer, and a second gori holding that slot for nothing but its own bookkeeping is what makes it contend with the TUI capturing into the same project. The one exception is a database written by an older gori, which is migrated on open because this build cannot read it otherwise.

A default (actions-on) server still has a writer β€” send_request and create_issue need one β€” but it does not idle-index. Free-text search drains the backlog on demand; the capturing TUI is the process that keeps the index current in the background.

One consequence is worth knowing: free-text search (body:) reads an index that is built off the capture commit, and a read-only server cannot build it. If flows are still waiting to be indexed, such a query is refused with FTS_BACKLOG rather than answered from a partial index β€” open the project in gori, or drop --read-only, to drain it.

Seeing an Agent From the TUI

While an MCP server is bound to a project, gori shows it. In the project picker the project's row carries an mcp mark (mcpΓ—2 for more than one), and once the project is open a clickable mcp:<client> chip appears on the top bar. Click it β€” or run app.agents from the command palette β€” to open a card listing every attached agent: its name, version, pid, when it connected, and whether it is read-only. The name comes from the client's own clientInfo handshake; a server started with --read-only shows as read-only there. A row disappears on its own the moment its process exits, so the chip and card always reflect what is attached right now.

Installing Into an Agent

gori can write the MCP configuration for common clients for you:

Flag Client Config written
--install-claude Claude Desktop claude_desktop_config.json in the platform's app-config directory (see below)
--install-claude-code Claude Code ~/.claude.json (mcpServers.gori)
--install-codex OpenAI Codex ~/.codex/config.toml ([mcp_servers.gori])
--install-agy Antigravity CLI ~/.gemini/antigravity-cli/mcp_config.json
--install-grok Grok ~/.grok/config.toml ([mcp_servers.gori])
--install-hermes Hermes ~/.hermes/config.yaml (mcp_servers.gori), or $HERMES_HOME

Every client except Claude Desktop and Hermes keeps its config in the same place on macOS, Linux and Windows. Hermes reads $HERMES_HOME when it is set, and otherwise ~/.hermes (%LOCALAPPDATA%\hermes on Windows). Claude Desktop follows Electron's app-data directory instead: ~/Library/Application Support/Claude/ on macOS, %APPDATA%\Claude\ on Windows, and $XDG_CONFIG_HOME/Claude/ β€” defaulting to ~/.config/Claude/ β€” on Linux. gori reads that variable, so a Nix or home-manager session that moves it is followed too.

The exception is a Flatpak Claude Desktop: it reads XDG_CONFIG_HOME from inside its own sandbox (~/.var/app/<app-id>/config/Claude/), which the host shell running gori cannot see. There gori writes ~/.config/Claude/claude_desktop_config.json and prints that path β€” copy it into the app's sandbox directory yourself. Every install command prints the file it wrote, so check that line against where your build actually reads.

gori mcp --install-claude-code
gori mcp --install-codex
gori mcp --install-grok
gori mcp --install-hermes
gori mcp --install-claude-code --install-codex  # several clients in one run

Codex and Grok use TOML with an [mcp_servers.gori] table, and Hermes YAML with an mcp_servers: entry, rather than JSON. Restart the client (or re-open the session) after installing so it reloads MCP servers. Existing config files are updated in place: other servers, tables and comments are preserved, the file's permissions are kept, and the replacement is atomic so an interrupted install can never truncate it. gori edits these files as text rather than re-emitting them from a parse tree, so the documentation you keep around your own settings survives the install; a config it cannot splice safely is reported and left alone rather than rewritten.

If a client starts MCP outside your repository directory, the server starts unbound and the agent can pick or create a project over tools. To pin a fixed engagement at install time instead, pass a selector, for example gori mcp --project my-engagement --install-codex.

Every flag you pass alongside --install-* is written into the installed command, so what the client spawns matches what you typed β€” selectors (--project, --db, --no-project, --use-active-project), --read-only, --insecure-upstream, and --config. Paths are made absolute, because the client spawns the server from a working directory you did not choose.

Tools

Read tools (always available):

Tool Purpose
list_history List flows newest-first, with optional QL and pagination. Every row carries source β€” proxy for traffic a client sent, repeater for a send_request (including your own, which records by default), discover, import, … β€” so a flow gori made is never read back as evidence about the target. Filter with src: . Pass columns β€” the same [LABEL=][req|res:]kind:selector specs gori run ls --column takes β€” to carry an extracted value per row (a header, a JSON field, a regex capture) under a columns object: what QL can filter on, shown. Opt-in, since it costs a read per row
list_events Tail an append-only feed of job lifecycle and agent activity, by forward cursor. Flows stay the firehose; this never duplicates flow rows. Every event carries actor β€” the surface that acted (tui / cli / mcp) β€” so an agent can tell its own writes from the operator's, and config changes are recorded whoever makes them. The human reads the same feed on the Project β†’ Activity pane
list_views The project's History views β€” named QL queries list_history{view} applies as a lens, ANDed over query rather than replacing it. Seven built-ins (All, History, History + Repeater β€” the default β€” WebSocket, gRPC, SSE, Errors), then the global library, then the project's own; active marks the one the TUI is showing, which does not apply to list_history β€” that filters only by the view you pass it
get_flow Full request + response for one flow
get_response_body_chunk Page through decoded (or raw) flow/Repeater responses beyond the inline 64 KiB cap
list_sitemap / list_sitemap_tags Distinct endpoints (host, method, path), and the tags placed on them
list_issues / get_issue Read triaged issues
probe_scan Rescan captured flows and Repeater tabs. Passive (zero requests) unless active:true, which needs write access and is scope-gated
probe_issues The Probe tab's persisted findings, as triage state (open by default)
list_probe_rules Every scan rule (passive, active, custom), which are enabled, and the project's scan mode
list_scope Current scope include/exclude rules
list_links Evidence pointers from an issue or note to a flow, Repeater session, or job
compare_flows Line diff of two flows' request or response — with each side's status/size/time and the A→B delta; context:N folds the unchanged runs into {kind:fold,hidden} markers
diff_projects Retest diff: two PROJECTS at endpoint scale β€” what is new, gone, or answering differently since the last engagement. Endpoints are keyed by the Sitemap's folded template, and removed (never requested in the newer capture) is a separate verdict from gone (asked, got 404/410)
intercept_list / intercept_get Inspect the live intercept queue and one held item in full
list_projects Every gori project on this host
list_notes / get_note Read project notes
list_rule_presets The response-modification presets β€” named starting points that install ordinary Match & Replace rules (unhide hidden fields, enable disabled controls, remove maxlength, strip client-side validation, drop CSP / security headers, disable SRI). Each row names the rules it would install
list_extract_rules The project's extract rules β€” the read half of a session binding: each observes a response and binds one $NAME in memory for a Match & Replace rule to inject
list_color_rules / list_custom_colors The Colormarker rules in precedence order, and the global custom colours a rule's color can name. Display only β€” a colour rule never modifies traffic
preview_color_rule How many recent flows a colour condition would MATCH, and how many it would actually PAINT once the rules resolving ahead of it are counted
grpc_schema What .proto schema this project renders captured gRPC through, and where each piece came from β€” a descriptor-set file or a reflection fetch. Sends nothing
list_rules List the Match & Replace rules applied to the project in apply order β€” global rules first, then the project's own (scope filters to one)
list_env Project env tokens available to $KEY substitution (values redacted). Each row also carries length and, when the value already begins with one, scheme β€” enough to tell whether a header should read Bearer $KEY or just $KEY, without the value
list_host_overrides The host to IP dial map in force for this project
list_session_slots The project's session slots β€” named identities, each a header overlay plus the extract rules whose bound values belong to it β€” and which one is ACTIVE (header values redacted)
list_oast_providers Configured OAST providers and which one is active
list_oast_sessions The project's persisted OAST listening sessions β€” payload host, hits, last poll β€” the rows oast_resume re-arms
decode Run an encode/decode/hash/compress chain over input (pure transform; no network or state)
jwt_decode / jwt_encode / jwt_attacks Decode, re-sign, or generate attack payloads for a JWT (pure compute; available even under --read-only)
cookie_decode / cookie_verify / cookie_crack / cookie_forge The Cookie workbench as pure offline compute: parse a Flask / Rack / Django signed session cookie, check it against a candidate secret, brute-force the secret over a wordlist, and re-sign an edited payload. No network, so all four survive --read-only
sequence_analyze Grade a pasted token list for randomness / predictability (pure)
oast_presets / oast_payload / oast_poll List OAST providers, read the active payload, and poll a running listener for callbacks
discover_status / discover_results Progress and findings of a Discover run
project_info Flow / issue counts, database, workspace binding, and selection source
get_current_context What the user is viewing in the TUI right now
get_repeater_context Repeater workbench state and saved sessions. Every session reports both ids β€” db_id, which every repeater tool takes, and tui_index, the 1-based number the TUI paints on its sub-tab chip (6:POST /api) β€” so an agent and the operator name the same tab. filter takes the same sub-tab language the TUI's / does (tag: name: host: method: status:, - negates, bare words search), ANDed with query. include_content adds the request head and, per credential header, an env_headers shape (Authorization: Bearer $AUTH) that names the wiring without the secret; include_response_body inlines the stored last response body
list_fuzz_runs / get_fuzz_run List and inspect permanent Fuzzer result sets. Metrics use a scalar-only projection, including result_index, so retained BLOBs are not loaded. include_content:true returns at most 25 rows from SQLite-capped prefixes: max_head_bytes (default 16 KiB, max 64 KiB) bounds heads and max_body_bytes (default 2 KiB, max 64 KiB) bounds decoded bodies/raw samples. Full source sizes plus head/source/decode truncation flags say what was omitted; include_sensitive:true opts into exact capped prefixes, never uncapped bytes. Run metadata labels pre-current snapshots as legacy:true
ql_reference The query-language reference
ql_explain Diagnose a query without running it, to check a filter before spending requests on it

Action tools (disabled by --read-only):

Tool Purpose
send_request Send / resend an HTTP request (active; records History by default, expands $KEY env tokens, and redacts sensitive response-header values unless explicitly requested). reframe_grpc: true recomputes a unary gRPC message's 5-byte length prefix over the body actually sent β€” off by default, so an edited message ships with the prefix it was captured with
send_websocket Execute a saved WebSocket Repeater session and collect the replies
create_repeater / update_repeater / delete_repeater Manage one Repeater session. Every reply carries tui_index beside id; a delete names the tab it destroyed (was_tui_index) and renumbers the rest
create_repeaters Seed a tab from each of several captured flows β€” the second hop of an OpenAPI import (see below). Checks every flow exists before creating the first session
delete_repeaters / update_repeaters Bulk close, and bulk re-label (tags and name affixes only β€” update_repeater is the one that writes request bytes). Both take explicit ids, never a filter: narrow with get_repeater_context{filter} first, so the set you read is the set acted on. Delete needs confirm:true, and an unknown id refuses the whole call
move_repeater Rearrange the sub-tab strip β€” to_index for an absolute tab number, direction for a one-step nudge. An open TUI picks the new order up on its own
minimize_repeater Shrink a Repeater request to the smallest form that still reproduces the response
create_issue / update_issue / delete_issue Record, update, and remove issues
add_link / remove_link Attach or detach an issue's / note's evidence pointer
create_note / update_note / delete_note Manage project notes
create_rule / update_rule / set_rule_enabled / delete_rule Create, edit, toggle, and delete Match & Replace rules (rewrites on in-flight request/response head or body). Each takes scope β€” project (default) or global, which applies in every project
create_rule_from_preset Install a preset (see list_rule_presets) as ordinary Match & Replace rules β€” the same result as calling create_rule once per rule, so they stay visible, editable and disable-able afterwards. Returns the ids created
create_extract_rule / update_extract_rule / set_extract_rule_enabled / delete_extract_rule Manage the extract rules that bind $NAME from a response. Renaming drops the old name's bound value rather than re-labelling it, and disabling un-declares the name, so a rule injecting it goes back to refusing rather than sending a stale value
create_color_rule / update_color_rule / set_color_rule_enabled / move_color_rule / delete_color_rule Manage Colormarker rules. move_color_rule is a semantic edit, not cosmetic: the first enabled match paints the row. Each takes scope β€” project (default) or global
create_custom_color / update_custom_color / delete_custom_color Define the global custom colours the picker offers on top of the six built-ins. Deleting one leaves a rule that still names it inert β€” its rows fall back to a visible default rather than the deletion cascading into rules
grpc_reflect / grpc_forget Ask a target's grpc.reflection.v1 service (falling back to v1alpha) for its descriptors and cache them in the project, or drop a cached target. grpc_reflect is an outbound send and is scope-gated like any other
create_view / update_view / delete_view Create, edit, re-home and delete saved History views. Each takes scope β€” project (default) or global. The query is validated on the way in: one whose every term would be dropped is refused, because it would narrow nothing while every surface showed a chip claiming it does
preview_rule Estimate how many stored flows a rule would change, before creating it
import_flows Bulk-import a HAR / URL list / OpenAPI / Postman / Insomnia / Burp / WSDL file into History
delete_flow / clear_history Remove one flow, or wipe captured History
set_sitemap_tag Pin a free-text memo onto a sitemap path
create_project / switch_project / delete_project Create or reopen a project, point this server at another one, or delete one. Deletion is two-step: a dry_run first, then a confirmation token
add_scope_rule / update_scope_rule / delete_scope_rule / set_scope_enabled Edit the project's include / exclude rules and toggle the scope lens
set_sandbox Hard containment: when on, the proxy forwards only what scope allows and blocks the rest
set_env_var / delete_env_var Manage the project env tokens $KEY substitution reads
create_session_slot / update_session_slot / delete_session_slot Manage the session slots β€” the same list the Authorize tab's identities card edits, and the set authorize_start replays under
set_active_session_slot Choose the identity every outbound request goes out as: its header overlay is applied to the final wire bytes and $NAME resolves against its binding table. Held by this server process only β€” never persisted, so a new connection starts as-captured
add_host_override / update_host_override / delete_host_override Manage the host to IP dial map (changes only the connect IP, never the request)
probe_promote / probe_dismiss / probe_delete Triage a Probe finding into Issues, dismiss it, or remove it
set_probe_mode Set the scan mode: off, passive, active, or aggressive (authorized targets only)
create_probe_rule / update_probe_rule / delete_probe_rule / set_probe_rule_enabled Manage custom match rules and arm or disarm any scan rule
create_oast_provider / update_oast_provider / delete_oast_provider / set_oast_provider_enabled Manage the OAST providers oast_start can listen on
fuzz_start / fuzz_status / fuzz_results / fuzz_stop Drive the fuzzer. save_results:true permanently stores every row through a byte-bounded asynchronous writer and returns a database run_id; storage backpressure marks the save failed without stopping outbound traffic. This is independent of the bounded/selective live-job cache and record_history. fuzz_start{fields: ["role"]} sweeps a schema-known gRPC field of a unary request β€” each payload goes through the field's declaration on its way to bytes, every other byte of the message is copied from the capture, and the length prefix follows. A gRPC sweep of BYTE positions reports grpc_stale_prefix when a payload changed a message's length; fuzz_start{reframe_grpc: true} recomputes the prefix instead of reporting it. fuzz_results keeps rows the matcher rejected when the run observed something about them, so read each row's matched β€” or pass matched_only: true
delete_fuzz_run Delete a permanent fuzz run and its results; refuses a known live writer. force_stale:true removes a running/saving row left by a crashed process, and must never be used while another gori is saving
mine_start / mine_status / mine_results / mine_stop Drive the param miner
sequence_start / sequence_status / sequence_results / sequence_stop Collect tokens by live replay and grade them (results return the report, never the tokens)
authorize_start / authorize_status / authorize_results / authorize_stop Replay captured flows under several identities and compare each response against a baseline β€” broken access control. Results lead with access_control (BYPASS/enforced/review/error/nothing_sent) and a flat, never-paged bypasses list
discover_start / discover_stop Spider and brute-force endpoints (poll with discover_status / discover_results)
oast_start / oast_stop Register an ad-hoc OAST payload and poll for callbacks (read the hits with oast_poll); oast_stop on a RESUMED session stops polling but keeps it resumable
oast_resume / oast_release Re-arm a persisted session so payloads planted earlier keep resolving (its polls are saved into the project), or deregister one for a finished engagement β€” its callbacks stay
list_jobs / get_job / stop_job Work across job kinds: list every fuzz and mine job this session started, or fetch and stop one by id
intercept_forward / intercept_forward_edit / intercept_drop Release a held message byte-exact, release it with edited wire bytes, or drop it
intercept_toggle / intercept_set_filter / intercept_set_direction Arm or disarm the catch, set its condition query, and choose which leg it holds

Action tools are capped for safety: fuzz, mine, sequence, discover, and authorize jobs are limited in total requests, concurrency, and stored results. An authorize run's cap counts flows Γ— identities, and a selection over it is refused up front rather than truncated into a run that would report "enforced" for flows it never sent. A rule created via create_rule is picked up by gori run and newly opened TUIs; an already-running TUI applies it only after its rules reload.

From a Spec to Repeater Tabs

import_flows reads OpenAPI/Swagger (JSON or YAML) and joins servers[0].url with each operation path, so the base-path assembly is already done. Getting from a spec to a strip of tabs is three calls:

import_flows{kind: "oas", path: "openapi.yaml"}
list_history{query: "src:import"}          β†’ the flow ids
create_repeaters{flow_ids: [...], name_prefix: "oas: ", tags: "spec"}

create_repeaters checks every flow exists before it creates the first session, seeds each one through the same path a single create_repeater{flow_id} uses, and appends them in the order given. Rearrange afterwards with move_repeater, and prune with delete_repeaters.

Live Intercept

An agent can sit in the intercept loop next to you rather than reading History after the fact. The TUI session holding the capture lock mirrors held messages out to the agent and drains the commands it sends back, so intercept_list β†’ intercept_get β†’ intercept_forward_edit is the same loop you drive by hand.

The mutating half (intercept_forward, intercept_forward_edit, intercept_drop, intercept_toggle, intercept_set_filter, intercept_set_direction) is disabled by --read-only, and every one of them refuses when no live capture session is holding the lock. There is nothing to forward without a proxy actually holding traffic.

Agent actions are visible, not silent. Each one lands in the notification center tagged as coming from an agent, rendered differently from your own actions, so you can see what a co-pilot did to traffic while you were reading another tab.

One safety rule is worth knowing before you leave an agent running. A held message normally waits forever for a human decision, which is what you want when you are the only one at the keyboard. Once an agent attaches to the intercept queue in that session, gori arms a 30 second auto-forward for items nobody is watching, so a client that dies mid-hold cannot wedge the connection indefinitely. A session with no agent attached never auto-forwards.

One Call at a Time

Tools run one at a time, in the order they arrive β€” a fuzz or a slow send_request does not overlap with the next call, and responses come back in order. Two messages are answered immediately regardless: ping, so a client's liveness probe never stalls behind a long call and declares the server dead, and notifications/cancelled, which suppresses the response to a request you stopped waiting for. Cancelling does not abort work already in flight β€” an in-progress request finishes, its answer is simply not sent.

Why an MCP Seam

gori deliberately has no in-tool AI chat. The intelligence lives outside the tool, reachable through MCP. That means you choose the model, your traffic isn't shipped anywhere you didn't intend, and the same interface serves scripts and agents alike. gori run covers the non-interactive path; MCP covers the interactive-agent path.

Next Steps