> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rotascale.com/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP proxy

> Govern an MCP server you did not write, without touching it. rotascale-mcp-proxy sits in front and authorises every tool call.

```bash theme={"system"}
pip install rotascale-mcp
rotascale-mcp-proxy -- npx some-vendor-mcp-server
```

The proxy speaks MCP on both sides. Your agent talks to it, it talks to the
upstream server, and every tool call in between is authorised against a grant
first. The upstream is unmodified and unaware, which is the point: you are
usually not able to change it.

## What it does per call

<Steps>
  <Step title="Opens a trajectory">
    One per session, tagged with the upstream server name, so the whole
    conversation is one governed unit rather than a series of unrelated calls.
  </Step>

  <Step title="Resolves a grant for the tool">
    Per tool, not per server. A server exposing ten tools is not one authority.
  </Step>

  <Step title="Works out the amount">
    From an argument you nominate. See below, because this is the part with a
    sharp edge in it.
  </Step>

  <Step title="Authorises, then forwards or refuses">
    A refusal goes back as an MCP error carrying the outcome and the reason, so
    the agent can tell "not permitted" from "out of allowance" and stop
    retrying the second.
  </Step>
</Steps>

## Money: `unpriced` and `unresolved`

The proxy cannot know which of somebody else's tool arguments is an amount, so
you tell it:

```bash theme={"system"}
export ROTASCALE_MCP_AMOUNT_FIELDS="payments.transfer:amount,billing.refund:value"
```

Two outcomes follow, and the distinction is deliberate:

* **`unpriced`**. Nobody declared this tool as carrying money, so it is
  authorised at zero. Honest: the operator states which of their tools move
  money, and silence means no.
* **`unresolved`**. A field **was** declared and the call did not carry a
  usable number. **Refused.** Declared-but-absent is a real problem, not a free
  action, and treating it as zero is how a spending limit quietly becomes
  optional.

<Warning>
  If a grant carries a spending budget and its tool declares no amount field,
  **every call to it is refused**. The proxy logs this loudly at startup, naming
  the grant, the tool, and the exact `ROTASCALE_MCP_AMOUNT_FIELDS` entry to add.
  Read the startup output the first time.
</Warning>

## Untrusted by default

Everything the upstream returns is untrusted content unless you say otherwise.
You did not write it, and an agent that treats a third-party tool result as
instruction can be steered by whoever did.

Where a downstream action requires a clean context, gate 5 refuses it and the
record names the server, the tool and the step.

## Environment

| Variable                             | Meaning                                    |
| ------------------------------------ | ------------------------------------------ |
| `ROTASCALE_URL`, `ROTASCALE_API_KEY` | Your deployment                            |
| `ROTASCALE_MCP_AMOUNT_FIELDS`        | `tool:argument` pairs, comma-separated     |
| `ROTASCALE_MCP_AMOUNT_FIELD`         | A single fallback field name for all tools |
| `ROTASCALE_MCP_LOG`                  | `WARNING` by default                       |

## When to use the proxy rather than the SDK

Use the **proxy** for a server you do not control. Use the
[guarded server](/sdk/mcp) for tools you are writing yourself, where the gate
belongs inside rather than in front and there is no unguarded path to leave
open.
