STRAZAdocs

Any MCP client

Your MCP client reaches its tools through the Straza gateway, and you have seen what the gateway answers when it allows a call and when it holds one for a person.

Who
You, on the machine where your MCP client runs
Where
A terminal, and a browser for the approval
Profile
Standalone and enterprise
On this page

You connect an MCP client that has no hooks to the Straza gateway, on the machine where the client runs. At the end, the client reaches its tools through the gateway, and you have seen what the gateway answers when it allows a call and when it holds one for a person.

The client talks to the gateway in place of the upstream MCP servers. The gateway checks the session, serves each role its own tool catalog and decides every tools/call against policy. It also resolves the upstream credential in memory and audits the result. Because the decision, the catalog and the credential all stay on the server, the gateway holds at a boundary with no code on the agent’s side. It cannot see what the agent does outside MCP, so a gateway-only setup governs MCP servers and leaves local commands ungoverned.

Before you start

  • This machine enrolled, as Enroll a machine shows.
  • An MCP client that can start a stdio MCP server.
  • A role that gives you a catalog. The example runs as a person who holds the seeded developer and demo-tools-readers roles of the demo stack, against http://127.0.0.1:8420. Put your own server’s address in its place.
  • A session at or above the server’s governance.minAttestation, because the gateway refuses one below it. The enterprise profile sets that minimum to managed, as Attestation levels explains.

On a standalone server you sign in as a local user. Its catalog starts empty, because a fresh standalone server has one starter PolicySet and no MCP servers.

  1. Register the proxy in your client

    Register straza mcp as a stdio MCP server in your client. The client’s config then carries no credential at all. The proxy keeps the rotating session token on the wire for you and joins the same governed session as any hooks on the machine.

    Its --harness flag names the harness the session checks in under. Without the flag it takes the STRAZA_HARNESS environment variable, and without that it uses claude-code. With a server name, such as straza mcp views-demo, the proxy serves that one server with its own tool names and views, as Show MCP Apps views shows.

  2. Check the catalog over stdio

    A short exchange over stdin shows the handshake and the catalog without a client. The sleep holds the pipe open while the replies come back, the way a real client holds it open.

    Terminal on the client's machine
    { printf '%s\n' \
      '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"stdio","version":"1"}}}' \
      '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
      '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'; sleep 5; } | straza mcp --harness claude-code

    You should see

    An initialize answer from the server named straza, then a tools/list answer with the tools your roles reach.

    {"jsonrpc":"2.0","id":1,"result":{"capabilities":{"logging":{},"tools":{"listChanged":true}},"protocolVersion":"2025-06-18","serverInfo":{"name":"straza","version":"v1.1.0"}}}
    {"jsonrpc":"2.0","id":2,"result":{"tools":[{"description":"Echoes back the input string",...,"name":"demo-tools__echo"},...,{"description":"Returns the sum of two numbers",...,"name":"demo-tools__get-sum"},...]}}

    The tools/list reply is trimmed here. In full, it lists the tools of every MCP server these roles reach, and a tool a policy denies is absent from it.

    With no MCP servers installed on a standalone server, the second line is {"jsonrpc":"2.0","id":2,"result":{"tools":[]}}. The two calls below then need an MCP server you install and give a role access to first, because neither demo-tools nor its approval rule exists outside the demo stack.

  3. Call a tool

    Behind the proxy, the gateway speaks streamable HTTP at POST /mcp on your server, with the session token as a bearer. The token lives 300 seconds, and straza mcp renews it for you. Straza has no command today that hands a session token to a client it did not start, so connect clients through the proxy. The requests below show the gateway’s answers on the wire, with $STRAZA_SESSION_TOKEN standing for the token of a live session.

    An allowed call runs upstream and returns its result.

    curl -s --max-time 15 -X POST http://127.0.0.1:8420/mcp \
      -H "Authorization: Bearer $STRAZA_SESSION_TOKEN" -H 'Content-Type: application/json' \
      -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"demo-tools__echo","arguments":{"message":"hello from any MCP client"}}}'
    {"id":3,"jsonrpc":"2.0","result":{"content":[{"type":"text","text":"Echo: hello from any MCP client"}]}}
    The handshake and the catalog over HTTP

    The same initialize over HTTP names the gateway itself:

    curl -s --max-time 15 -X POST http://127.0.0.1:8420/mcp \
      -H "Authorization: Bearer $STRAZA_SESSION_TOKEN" -H 'Content-Type: application/json' \
      -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'
    {"id":1,"jsonrpc":"2.0","result":{"capabilities":{"tools":{"listChanged":true}},"protocolVersion":"2025-06-18","serverInfo":{"name":"straza-gateway","title":"Straza MCP Gateway","version":"v1.1.0"}}}

    A tools/list returns the same catalog as the proxy, including demo-tools__echo, demo-tools__get-sum and the midpoint__* and straza__approval_* tools:

    curl -s --max-time 15 -X POST http://127.0.0.1:8420/mcp \
      -H "Authorization: Bearer $STRAZA_SESSION_TOKEN" -H 'Content-Type: application/json' \
      -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
  4. Approve a held call

    A call that a rule holds for approval does not answer at once. The gateway keeps the connection open until a person decides, the rule’s window ends, or approval.gatewayHoldSeconds passes, whichever comes first. That setting is 120 seconds by default.

    Here get-sum needs a yes from the person behind the agent. This example runs as a person with no sponsor, so the call waits for that person’s own confirmation. Give the client more time than the window, which is two minutes on this rule.

    curl -s --max-time 130 -X POST http://127.0.0.1:8420/mcp \
      -H "Authorization: Bearer $STRAZA_SESSION_TOKEN" -H 'Content-Type: application/json' \
      -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"demo-tools__get-sum","arguments":{"a":2,"b":3,"_straza_justification":"checking the gateway"}}}'

    While the command waits, open /self-service/ on the same server in a browser you enabled under This browser, and approve the request.

    1. Requests
    2. Waiting
    3. Approve
    4. Approve request

    Approve in the browser shows how to enable the browser, and an enrolled phone shows the same request.

    You should see

    The call returns the moment you decide, with the tool’s result.

    {"id":4,"jsonrpc":"2.0","result":{"content":[{"type":"text","text":"The sum of 2 and 3 is 5."}]}}

    In the example, a script enrolled a key the way Enable this browser does and signed the approval. The request’s record then named the person as the decider and browser as the channel, with the reason given.

    The console and strazactl refuse a decision on your own request, because an agent on your machine could make the same call. Only a device that signs confirms it, unless the operator sets approval.unsignedOwnDecisions. Approve in the console covers each place a person decides.

    When nobody answers within the window, the call comes back as a tool-level error that the model can read. The same call made again opens a new request.

    {"id":4,"jsonrpc":"2.0","result":{"content":[{"type":"text","text":"Straza: approval request expired after 120s with no decision (ref 01a0e9a3-acb2-7b53-af02-710d6f946c30)"}],"isError":true}}

    An AI agent’s call waits the same way for the person behind the agent, its sponsor. A rule that names an approver role sends the request to whoever holds that role. When the hold ends before the rule’s window does, the call answers that the approval is pending, and What the agent reads back quotes that answer and the approval tools an agent uses to follow it.

When the gateway refuses

The gateway fails closed. A call with no token is rejected before any tool is considered:

curl -s --max-time 15 -X POST http://127.0.0.1:8420/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":5,"method":"tools/list"}'
{"error":"missing session token"}

A revoked token is refused too. When a token’s attestation is below the configured minimum, the gateway refuses it with the level it needs. Because the upstream credential stays on the server, the gateway governs the call itself, so it holds even for a client you do not control.

Next

Walked on v1.1.0 on 2026-09-28. A Linux container against the demo stack, as a person who holds the seeded developer and demo-tools-readers roles, with a script that signed the approval the way an enabled browser does. The standalone notes come from an earlier run against a standalone server.

Search documentation

Search page titles, commands, and article text.

↑ ↓ Choose resultEnter OpenEsc Close