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).

A few tool families never need a project and work unbound: project management (project_info, list_projects, create_project, switch_project, delete_project, diff_projects), the pure-compute helpers (decode, jwt_*, cookie_*, sequence_analyze), the query-language reference (ql_reference, ql_explain), and the ad-hoc OAST listener (oast_presets, oast_payload, oast_start, oast_stop, oast_poll). Everything else, including the persisted oast_resume / oast_release, answers NO_PROJECT until one is bound.

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.

The project named in instructions is the one bound at the handshake. That text is sent once and nothing pushes an update to it, so after a switch_project it still describes the binding as it was then while every later call reads and writes the new project. project_info is the live answer. switch_project β€” and create_project when it auto-binds β€” return the same correction in their result, alongside previous_project, the binding they moved off.

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, because 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.

Choosing Which Tools Are Exposed

gori exposes about 160 MCP tools. A client loads that whole catalogue into the model's context before you ask the first question and keeps it there for the session β€” roughly 43,000 tokens. --read-only cuts it to 53 tools (~12,000 tokens), but only along one axis. --tools lets you pick directly:

gori mcp --tools='list_*,get_*,ql_*,project_info,send_request'   # recon + replay, ~11k tokens
gori mcp --tools='-fuzz_*,-mine_*,-discover_*,-sequence_*'       # everything but the async workbench
gori mcp --tools='*,-intercept_*'                                # same idea, spelled out

The spec is a comma-separated list of tool names and * globs, applied left to right; a term prefixed with - subtracts. Because the tools are already named in prefix families (list_*, intercept_*, fuzz_*, oast_*), globbing gives you groups without a separate catalogue to keep in step. A spec that starts with a subtraction begins from every tool, so it keeps working when a later gori adds one.

A pattern that matches nothing aborts at startup with a suggestion (--tools: "list_hisotry" matches no tool β€” did you mean list_history?) rather than quietly serving a smaller set β€” a server missing a tool looks exactly like a gori that never had the feature. Tools left out are absent from tools/list and refused if called anyway, naming the flag that hid them. --tools composes with --read-only, and like every other flag it is written into the command when you pass it alongside --install-*.

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
--install-pi Pi ~/.pi/agent/mcp.json (mcpServers.gori), or $PI_CODING_AGENT_DIR

Pi requires an MCP adapter such as pi-mcp-adapter. Install it with pi install npm:pi-mcp-adapter, then restart Pi. --install-pi writes the MCP server configuration.

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-pi
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, --tools, --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, which filters only by the view you pass it
get_flow Full request + response for one flow. Bodies come back sanitized, with a body_redaction object, where a redaction profile is on by default; include_sensitive:true turns that off along with the header redaction
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
list_evidence / get_evidence Frozen evidence β€” immutable copies of an exchange with provenance, linked issues and SHA-256s (one issue's with issue_id, the whole project archive including orphans without it) β€” and one copy's bytes (heads redacted unless include_sensitive, bodies capped like get_flow)
list_retest_steps / list_retest_runs / get_retest_run An Issue's retest: the ordered Repeater sends that reproduce the finding, each resolved against the project so the reply already says what it will SEND and which steps change state; the bounded run history (newest first); and one run's result table, each row keeping the History flow id of its own send
get_issue (retest field) An issue that HAS a retest carries a retest object beside links and evidence β€” the step count and the last run's verdict β€” so reading a finding already tells you whether a reproducible check exists and what it last said. Omitted entirely on an issue without one
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 $BIND.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. Takes scope, because a global candidate resolves before every project rule
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 and built-in generators available to substitution, as {syntax, prefix, example, vars, generators} (env values redacted). The three scalars are how to WRITE a reference: syntax is this install's grammar (namespaced β†’ $ENV.KEY, $BIND.NAME, and $GEN.UUID; bare β†’ the legacy $KEY with no generators), prefix is the sigil, and example is the first two applied, so a non-default sigil needs no assembling. Each vars row is keyed by the bare name and also carries length and, when the value already begins with one, scheme; each generators row carries the complete token and output format
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_verify / jwt_encode / jwt_attacks Decode (a JWS or the protected header of an encrypted JWE), verify a signature against a key you hold, re-sign with HMAC or a PEM key, 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
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 $ENV.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). Every one that opens a socket (send_request, send_websocket, fuzz_*, mine_*, authorize_*, sequence_*, discover_*, grpc_reflect, minimize_repeater, and probe_scan with active:true) is scope-gated: a target outside, or without, a configured scope is refused with SCOPE_BLOCKED unless the call passes allow_unscoped:true, the explicit waiver, and the sandbox and explicit excludes apply even then.

Tool Purpose
send_request Send / resend an HTTP request (active; records History by default, expands $ENV.KEY env tokens and $BIND.NAME bindings, 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
freeze_evidence / link_evidence / unlink_evidence / delete_evidence Copy a flow's or a Repeater tab's current exchange into immutable evidence on an issue (the next send and retention cannot touch it; link:true by default files the live link in the same transaction), change its Issue memberships without changing the snapshot, or delete one copy. Freeze when a response proves a finding, and again after the retest. A tab whose request was edited after its stored response arrived is refused β€” those two are not one exchange, so send it again or pass allow_drift:true to keep the mismatched pair anyway
add_retest_step / update_retest_step / move_retest_step / remove_retest_step Build the retest: a Repeater session, a role (setup / baseline / variant / control / cleanup) and at most one assertion (status:2xx, json:data.role=admin, json-absent:…, body:same / body:diff). The session is not copied β€” a step sends whatever the tab holds when the run happens
run_retest Run it, through the project's scope and Sandbox gates, and answer pass / fail / inconclusive / blocked plus a row per step (isError on anything but pass). A batch containing a state-changing method is REFUSED with the exact request count until confirm:true; after gori refuses a send the rest is skipped, cleanup included, unless allow_cleanup:true. Each send is recorded in History as src:retest
clear_retest_steps / delete_retest_run Drop every step of an Issue's retest (the run history is kept β€” re-planning a check does not un-run it), or delete one run summary and its result rows. The steps and the History flows each send recorded both stay: delete_retest_run drops the report, not the evidence, for a run that should not be on the record at all
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 $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 substitution reads. The key is stored bare: reference it as $ENV.KEY, or as $KEY under the bare opt-out β€” list_env's syntax / example says which one this install speaks
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 $BIND.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 (the request was re-sent or retried, the response came back truncated, the send errored, or a Β¦chain step could not run so the payload went out untransformed), 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_status / discover_results / discover_stop Spider and brute-force endpoints, poll progress, and read findings. All four are action tools, so a read-only server has no Discover surface
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, mine, discover, sequence, and authorize 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.

It reads the other way too. A row intercept_list returns with operator_editing: true is one you have unsaved changes typed into right now, so an agent can leave that message to you instead of forwarding, editing or dropping it and discarding your work.

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

  • AI Setup: a step-by-step walkthrough to connect an agent and drive its first request
  • Scripting: the other automation path, gori run for pipelines and CI
  • CLI Reference: full gori mcp flags
  • Query Language: the syntax agents use to filter