Use the KIFF MCP gateway
Already have an AI agent that can connect to remote MCP servers? You can put KIFF between that agent and the tools it uses:
your agent ──MCP──▶ mcp.kiff.dev ──MCP──▶ your connected tool
│
└── checks the agent's Card first
KIFF does not host the agent. The agent still plans the work; the connected tool still performs it. KIFF checks each call against the Card before it forwards the call. For this to be a boundary, connect the tool to KIFF and remove the agent’s direct access to that same tool.
This is a different integration from kiff-guard, which you add to code you can change. Using Claude Code or Codex? The KIFF skill walks through these steps with you. The gateway is for remote MCP tools. New to KIFF? The quickstart walks through the same steps in the dashboard.
What you need
- A KIFF account, signed in as its owner or an admin.
- A remote MCP server reachable at a public HTTPS URL. Private, loopback, link-local, metadata and reserved addresses are refused.
- An agent that can connect to a remote MCP server over HTTP. The production flow has been tested with Claude Code and Codex.
- A bearer credential for the tool server, if it requires one. KIFF stores connected tool credentials encrypted. To keep the key away from KIFF entirely, the tool can check KIFF’s execution permit itself instead: see Tools that check KIFF. OAuth and arbitrary non-MCP web APIs are not supported yet.
1. Connect a remote MCP tool
Signed in to KIFF Cloud as the account’s owner or an admin, open Tools and choose Connect a tool:
- Enter the server’s URL and, if it needs one, its bearer credential. KIFF lists the server’s tools with that credential; nothing is saved yet.
- Choose the tool to connect, the name your agent will see, the amount argument a Card limits (whole-number arguments only), the idempotency argument KIFF fills (text arguments only), and whether the tool only reads.
From the same page you can test a connected tool, replace its credential (the old one is kept unless the new one works), change who holds its key (verify or relay mode) or remove it. The credential is never shown again after it is saved.
With the gateway API
The same steps work over the API for a caller that already holds an owner or admin API key (see the note on API keys below). Prepare the request body in a local file readable only by you, such as connection.json:
{
"name": "issue_refund",
"url": "https://tools.example.com/mcp",
"tool": "refund",
"amount_argument": "amount_eur",
"amount_unit": "EUR",
"amount_scale": 0,
"idempotency_argument": "idempotency_key",
"credential": "<tool server bearer token>"
}
Then send it with your owner or admin key:
curl -s -X POST https://mcp.kiff.dev/v1/connections \
-H "Authorization: Bearer $OWNER_KEY" \
-H "Content-Type: application/json" \
--data-binary @connection.json
tool is the name exposed by the remote server; name is the name your agent will see. Set amount_argument to the tool’s integer amount parameter if the Card should limit amounts. Set idempotency_argument if the tool accepts an idempotency key. Omit either field if it does not apply. Use the same units your tool expects when you set Card limits. amount_unit and amount_scale are optional and only change how amounts are shown: a currency code or a word for the unit, and the number of decimal places the tool’s integer carries. A tool that takes cents uses "amount_unit": "EUR", "amount_scale": 2, so a limit of 5000 reads as 50.00 EUR. KIFF still checks the whole numbers either way.
KIFF checks that the URL, tool and credential work before saving the connection. The gateway then updates the account’s tool contract. Treat the tool credential as a secret: do not commit the request file or paste a real credential into a command that will be saved in shell history. Remove the local request file after the connection is saved.
Trusted no-effect outcomes
Enable outcome_contract: "terminal_no_effect_v1" only for a trusted
business executor that guarantees a final refusal has no effects and
cannot execute later. It requires an idempotency argument, which KIFF
fills with its invocation identity. An ordinary MCP error never implies
that guarantee.
For a final refusal, the action tool returns isError: true with:
{
"isError": true,
"structuredContent": {
"kiff_outcome": {
"operation_id": "<KIFF invocation identity>",
"effect": "none",
"terminal": true,
"reason_code": "order_not_paid"
}
}
}
KIFF records this evidence durably before appending effect releases. Effect limits get the original quantity back once; attempt limits do not. Failed recording or reconciliation leaves the quantity charged. The original operation remains spent.
For lost responses, optionally configure outcome_lookup_tool with a
separate read-only status tool on that executor. It must declare
readOnlyHint: true and accept the same string idempotency argument.
Its successful status read returns the same structured facts with
isError: false; an absent operation, unfinished operation or failed
read cannot release a draw.
Call POST /v1/operations/{invocation}/reconcile on the gateway with
the original agent credential or a management credential. For a token
bound to an agent’s connect link, use
POST /a/{link}/operations/{invocation}/reconcile. This endpoint reads
status; it never resends the action, issues a permit or reopens the
operation. The invocation retains the original executor binding. If
the connection or credential changes, reconciliation fails closed.
Repeated reconciliation uses the durable evidence and cannot release
twice. For an executor without this contract, resolve the business
outcome with its operator; KIFF keeps the draw charged.
2. Give the agent a Card
In KIFF Cloud, open Tools and choose Give an agent a Card on the connected tool. Name the agent and set, in the tool’s own units, the most per call, a total and a number of calls per day, per 24 hours or per hour, what happens to a call outside the Card (held for you, or refused), and how long a held call waits for your answer. Issuing again for the same agent and tool changes its Card; Change limits on the Card starts from its current terms.
Each agent needs its own key bound to that agent and limited to the gateway_agent role. The key must be created by a caller that holds the admin role, because a key can only grant roles its creator already holds.
Signed in as an admin, open the agent under Agents (My agents in a personal account), go to its Tools tab and choose Connect to the KIFF gateway. KIFF creates the key and shows it once, already inside the configuration for Claude Code and Codex; copy it then. In a company account, only an admin can do this, and anyone else is told so.
To do the same with the API, use your admin API key ($ADMIN_KEY):
curl -s -X POST https://api.kiff.dev/v1/keys \
-H "Authorization: Bearer $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"label":"my-agent","roles":["gateway_agent"],"agent_id":"my-agent"}'
A solo account owner has the admin role, and so does their signed-in session in KIFF Cloud, which is why the dashboard steps above work. An API key created on API keys (/dashboard/keys) holds no roles, so it cannot connect tools or create gateway keys: there is no way yet to create an admin API key from the dashboard. Use the dashboard for these steps; the API calls are for an operator who already holds an admin key.
A gateway key is good only for the gateway. Like a sign-in connection for the same agent, it reaches only the routes the gateway calls: it cannot list the account’s Cards, read another agent’s held work, or change anything on the account.
3. Point the agent at KIFF
Configure the agent’s remote MCP server URL as https://mcp.kiff.dev/mcp and give it the gateway key as a bearer token.
For Claude Code, the MCP server entry has this shape:
{
"mcpServers": {
"kiff": {
"type": "http",
"url": "https://mcp.kiff.dev/mcp",
"headers": {
"Authorization": "Bearer <gateway agent key>"
}
}
}
}
For Codex, set mcp_servers.kiff.url to that URL and mcp_servers.kiff.bearer_token_env_var to the environment variable holding the gateway key. Keep the secret out of the configuration file if that file is checked in.
4. Let KIFF check each call
When the agent calls the connected tool, KIFF checks the agent’s Card first:
- If the call is within the Card, KIFF forwards it to the tool.
- If the Card requires an owner decision, KIFF holds the call and returns a link. KIFF also emails the account owner, once per held call, with what the agent asked for and the same link. Open it in KIFF Cloud, signed in, and choose an answer; nothing can be approved from the email itself. Approval authorizes the call; the agent must retry before KIFF forwards it. In Claude Code (2.1.287 or later), the chat also offers to open the link, on accounts where that is on. See Held calls and approvals.
- A held call waits for the owner’s answer for the Card’s hold expiry, 10 minutes unless the Card sets another (from 1 minute to 7 days,
hold_expirywhen the Card is issued). If nobody answers in time it is refused, nothing is sent, and a late answer is not accepted; the agent has to ask again with a newkiff_operation_id. An answer given in time stands until the agent collects it. - If the call is refused, KIFF does not forward it.
If the client runs tool calls as MCP tasks (the gateway declares the
tasks capability), a held call is answered with a working task instead of
a result that says to retry. The client waits on the task; it completes once
the call is forwarded, refused or expired, and the conversation continues
with the outcome. Only the key that made the call can read its task.
The gateway records calls so retries do not forward the same held action twice. If the tool’s response is lost after a call was sent, the result may be unknown; KIFF will not send that action again automatically. When the tool supports idempotency, configure its idempotency argument as shown above.
The gateway adds an optional kiff_operation_id argument to every tool. Give each new action a new id, and reuse the id when retrying the same action:
- A call with an id KIFF has seen is the same call, at any time: it gets that call’s outcome and is never forwarded again. The same id with different arguments is refused.
- Without an id, an identical call (same tool, same arguments) within 10 minutes is treated as a retry of the first and gets its outcome, with a note saying so. Two genuinely separate identical actions in that window are done once; give each its own id to do both.
- A tool connected as read-only is never answered with an earlier call’s result: a second identical read reaches the tool again once the first has finished.
What the agent knows before it calls
Each tool the gateway lists ends with a line saying what the agent’s Card allows for it: the most one call may carry, how much of each total is left in its window, and whether a call over the Card is held for an approver or refused. A tool no Card of the agent’s covers says that calls to it are refused. For example:
Your KIFF Card: Card mdt-refunds covers this tool: up to 1000 amount_eur per call; 3800 of 5000 amount_eur left today. A call over it is held for a person to approve.
The gateway also lists its own read-only tool, kiff_card. It shows the same Cards with the numbers as they are now, the same ones the Card’s statement shows. Calling it is not a governed call: nothing is recorded, decided or drawn, and nothing reaches a connected tool. No connected tool can be named kiff_card.
These numbers are information, read when the agent asks. They can change between that read and the call, for example when another agent on the same Card draws from it. The decision KIFF makes when the call arrives is what counts. kiff_card shows only the Cards the agent holds, and reading a Card gives the agent no way to issue, change or revoke one.
The gateway also lists kiff_pending, read-only as well. It lists this credential’s own calls that wait for the owner, are being sent, or have an unknown outcome, newest first, from the last 7 days, 50 at a time. When there are more, the result has a next_cursor to pass back as cursor:
{"name": "kiff_pending", "arguments": {}}
These are the parameters for an MCP tools/call request. For the next page, use {"name": "kiff_pending", "arguments": {"cursor": "<next_cursor>"}}.
For a held call, the result includes the original arguments and the owner’s answer: waiting, approved, refused, or ended without approval. unavailable means KIFF could not read the answer; it is not a refusal or proof that nobody approved. A client must preserve recovery and check again. For every call, the list gives the kiff_operation_id that collects it. A call made without one is listed under KIFF’s own id, so it can still be collected after the 10-minute identical-call window. Reuse the listed original arguments, including any idempotency argument the agent supplied; the downstream tool still receives KIFF’s own idempotency value.
Reading the list sends nothing, uses no approval and draws nothing from the Card. Collecting a call is the agent’s own call, made again with the same tool, original arguments and listed id; an approved call is sent at most once. When recovering work in a new session, read the pending list first using the same credential that made the calls.
In Claude Code, the optional kiff plugin checks held calls and brings the owner’s answer into the conversation. Its bounded scans continue across checks and its notices are announced once. See installation, permissions and recovery.
What the agent gets back
A successful authorization and a successful tool response are separate facts. A forwarded call can still return a tool error, and a missing response leaves the outcome unknown. KIFF Cloud records the tool’s reported outcome alongside the decision; see approval and the tool’s outcome.
A forwarded call returns the tool’s own result, unchanged. Every result KIFF writes itself (held, refused, still being decided, sent with no answer yet, failed, or unknown) has text for the model with the next step, and the same facts for the program running the agent in the result’s _meta, under dev.kiff/call:
{
"state": "held",
"invocation": "tc-…",
"operation_id": "order-1042-refund",
"sent": "no",
"exception_id": "exc-…",
"review_url": "https://app.kiff.dev/needs-you/exc-…",
"hold_expires_at": "2026-10-01T12:10:00Z",
"retry": "same_call",
"retry_after_s": 30
}
state:held,refused,failed,unknown,deciding,forwarding, orforwardedon a repeated call.sent: whether the tool received the call:no,yesorunknown.retry:same_call(repeat the identical call to get the answer),new_call(this call is over; a new attempt needs a newkiff_operation_id, after the next step in the text) ornone(do not retry, for example when the owner rejected it or the result is unknown).reasons: machine-readable codes, such asno_mandate_issued,mandate_per_action_exceededormandate_owner_expired.hold_expires_at: on a held call, when it stops waiting for the owner and is refused.repeat:truewhen an identical call without an id was answered with an earlier call’s outcome.
Limits to understand
- Only calls to tools connected through KIFF are governed. A direct connection, shell, browser or another route to the same service sits outside this Card.
- The gateway currently connects remote MCP servers only. It does not connect arbitrary HTTP APIs or use OAuth.
- The prompt in the chat works only with clients KIFF has verified: today Claude Code 2.1.287 or later, on MCP revision 2026-07-28. Every other client gets the held result with its link, and the owner still gets the email and Needs you.
- Claude Code and Codex passed production smoke tests with a temporary test tool. That verified the gateway path; it was not a customer transaction or a test of every MCP client and server.
- Codex prompts for approval before every MCP tool call by default. Add
default_tools_approval_mode = "approve"under[mcp_servers.kiff]in your Codex config (not at the top level) to let it forward calls without pausing.
For the product story and the production test details, see Putting KIFF between an agent and its tools.