One URL in front of everything.
Your client talks to one address. The agent sees ten tools on base (nine on free), however many servers you connect. Every call takes the same eleven steps in the same order and lands in one log. The servers stay where they are.
Connect a client with one URL, or one pasted block.
Give Claude, Codex, Cursor or ChatGPT the project URL, https://api.safe-mcp.com/mcp/<project>, and nothing else. The gateway is its own OAuth authorization server: the client registers itself, you approve it once in the browser, and it gets a token scoped to that project for an hour, refreshed while it stays in use.
Or mint a personal token in the app and paste the config it hands you. Both speak Streamable HTTP. There is no SSE transport.
{
"mcpServers": {
"acme": {
"type": "http",
"url": "https://api.safe-mcp.com/mcp/acme",
"headers": { "Authorization": "Bearer smcp_…" }
}
}
}
Example, not real data. The app generates this with your token filled in.
The agent sees ten tools, and none of yours directly.
Downstream tools are never mounted into the model's context one by one. The agent searches the catalog, reads one tool's schema together with the rules that apply to it, and calls it by name with a one-sentence reason. That is the whole surface: ten tools.
Eleven steps, always in this order.
The request is written to the log first, with everything the agent sent. Each step writes its decision before it refuses, so a refused call is as durable as a successful one. The code on the right is what the log records at that step: the refusal code where it stops the call, the event where it passes.
- loggedthe full request, before anything is decided
tool_call.started - cancellationan already-aborted request is refused, not run
cancelled - existencethe server must be known and up; a suppressed tool does not exist
unknown_tool - reasonone self-contained sentence, at least six words
invalid_reason - policypreset rules: allowed keys, pinned values injected and recorded
policy_argument - schemathe tool's own JSON schema, with the failing path disclosed
invalid_arguments - goala write with no goal in force is refused; reads always run
goal_required - evidencea write that earlier moved the goal away is refused with the evidence
prior_harm_unacknowledged - testing modewith testing on, every non-read-only tool is blocked here
testing_blocked - dispatchthe downstream call, with cancellation and progress passed through
downstream_failed - resultwritten as succeeded or failed; a large body becomes a handle
tool_call.succeeded
One process, one runtime per project.
Your project is its own runtime: its own catalog, connections, credentials, goal and log. One gateway process serves the app, its JSON API and the MCP endpoint for every project. Editing a project's servers or presets rebuilds that runtime in place; sessions already running finish on the old configuration.
If you have never looked at what an agent actually sends, the history page is the place: every request as it was written, the step that answered it, and the time it took.
Layout, not a screen.
Limits a client will meet.
Defaults today. Only the free allowance is per account.