# Proxy API reference

> HTTP routes on `ati proxy`: `/call`, `/mcp`, `/help`, `/tools`, `/skills`, `/skillati/*`, `/health`, JWKS, optional `/admin/keys/*`, auth requirements, and request/response shapes.

- Repository: Parcha-ai/ati
- GitHub: https://github.com/Parcha-ai/ati
- Human docs: https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa
- Complete Markdown: https://grok-wiki.com/public/docs/parcha-ai-ati-a9d4398f11fa/llms-full.txt

## Source Files

- `src/proxy/server.rs`
- `src/proxy/client.rs`
- `README.md`
- `tests/proxy_test.rs`
- `tests/proxy_skills_test.rs`
- `scripts/test_proxy_server_e2e.sh`

---

---
title: "Proxy API reference"
description: "HTTP routes on `ati proxy`: `/call`, `/mcp`, `/help`, `/tools`, `/skills`, `/skillati/*`, `/health`, JWKS, optional `/admin/keys/*`, auth requirements, and request/response shapes."
---

`ati proxy` is an Axum HTTP server that holds manifests, the encrypted keyring, optional Postgres state, and optional passthrough routes. Sandboxed agents set `ATI_PROXY_URL` and call these endpoints; the proxy injects credentials, enforces JWT or virtual-key scopes, and returns JSON. Named routes are registered in `build_router`; unmatched paths can fall through to `handler = "passthrough"` manifests when `--enable-passthrough` is set.

```mermaid
sequenceDiagram
    participant Agent as Sandbox agent
    participant Proxy as ati proxy
    participant Sig as sig_verify middleware
    participant Auth as auth_middleware
    participant Up as Upstream API or MCP

    Agent->>Proxy: POST /call + Bearer JWT
    Proxy->>Sig: HMAC check (mode-dependent)
    Sig->>Auth: JWT or Ati-Key
    Auth->>Proxy: TokenClaims in extensions
    Proxy->>Proxy: Scope + rate limit
    Proxy->>Up: HTTP / MCP / CLI / file_manager
    Up-->>Proxy: Raw response
    Proxy-->>Agent: {"result": ..., "error": null}
```

## Authentication

Requests pass through **HMAC signature verification** (`sig_verify_middleware`, outermost) then **JWT / virtual-key auth** (`auth_middleware`). Public routes skip JWT only; they still run sig-verify unless the path is exempt.

| Mechanism | Header | When | Behavior |
|-----------|--------|------|----------|
| JWT (default) | `Authorization: Bearer <jwt>` | `jwt_config` present at startup | Validated with `ATI_JWT_PUBLIC_KEY` or `ATI_JWT_SECRET`; claims stored in request extensions |
| Virtual key | `Authorization: Ati-Key <raw>` | `ATI_DB_URL` + `db` feature | SHA-256 hash lookup; synthetic `TokenClaims` from row scopes |
| Admin master token | `Authorization: Bearer <ATI_ADMIN_TOKEN>` | `/admin/keys/*` only | Separate router; constant-time compare |
| Dev mode | (none) | No JWT keys configured | Named routes allowed without bearer; scopes unrestricted |
| Passthrough | HMAC only | Non-named host+path match + `--enable-passthrough` | JWT skipped; identity is HMAC-signed sandbox |

<ParamField header="Authorization" type="string">
Bearer JWT or `Ati-Key` raw token. Required on all named routes when JWT is configured. Scopes live in the token, not the request body.
</ParamField>

<ParamField header="X-Sandbox-Signature" type="string">
HMAC payload `t=<unix>,s=<hex>` where message is `{t}.{METHOD}.{path}`. Secret from keyring entry `sandbox_signing_shared_secret`. Controlled by `--sig-verify-mode` (`log` default, `warn`, `enforce`).
</ParamField>

<ParamField header="X-Ati-Upstream-Url" type="string">
Optional per-request MCP upstream override. Proxy validates against operator allowlist `{provider}_allowed_urls` in the keyring. Used with provider `mcp_url_env`.
</ParamField>

<Note>
`GET /health` and `GET /.well-known/jwks.json` skip JWT. Default sig-verify exempt paths include `/health`, `/.well-known/jwks.json`, plus operator overrides via `--sig-exempt-paths`.
</Note>

## Endpoint inventory

| Method | Path | Auth | Purpose |
|--------|------|------|---------|
| GET | `/health` | None | Liveness and counts |
| GET | `/.well-known/jwks.json` | None | ES256 public JWKS (when configured) |
| POST | `/call` | JWT / Ati-Key / dev | Execute a tool |
| POST | `/mcp` | JWT / Ati-Key / dev | MCP JSON-RPC gateway |
| POST | `/help` | JWT / Ati-Key / dev | LLM tool discovery assist |
| GET | `/tools` | JWT / Ati-Key / dev | List scope-visible tools |
| GET | `/tools/{name}` | JWT / Ati-Key / dev | Tool detail |
| GET | `/skills` | JWT / Ati-Key / dev | List local skills |
| GET | `/skills/{name}` | JWT / Ati-Key / dev | Skill body or metadata |
| GET | `/skills/{name}/bundle` | JWT / Ati-Key / dev | Full skill directory |
| POST | `/skills/resolve` | JWT / Ati-Key / dev | Resolve skills from scope list |
| POST | `/skills/bundle` | JWT / Ati-Key / dev | Batch skill bundles |
| GET | `/skillati/catalog` | JWT / Ati-Key / dev | Remote GCS catalog |
| GET | `/skillati/{name}` | JWT / Ati-Key / dev | Level-2 SKILL.md activation |
| GET | `/skillati/{name}/resources` | JWT / Ati-Key / dev | List remote resources |
| GET | `/skillati/{name}/file` | JWT / Ati-Key / dev | Read remote file |
| GET | `/skillati/{name}/refs` | JWT / Ati-Key / dev | List references |
| GET | `/skillati/{name}/ref/{reference}` | JWT / Ati-Key / dev | Read reference |
| POST | `/admin/keys/issue` | Admin bearer | Issue virtual key (`db` feature) |
| GET | `/admin/keys` | Admin bearer | List keys for `user_id` |
| GET | `/admin/keys/{hash}` | Admin bearer | Key metadata |
| DELETE | `/admin/keys/{hash}` | Admin bearer | Revoke key |
| POST | `/admin/keys/bulk-revoke` | Admin bearer | Bulk revoke |

## Public endpoints

:::endpoint GET /health
Proxy liveness and configuration summary. No authentication.
:::

<ResponseField name="status" type="string">
Always `"ok"` when the process is serving.
</ResponseField>

<ResponseField name="version" type="string">
Crate version from `CARGO_PKG_VERSION`.
</ResponseField>

<ResponseField name="tools" type="number">
Count of public manifest tools.
</ResponseField>

<ResponseField name="providers" type="number">
Provider count.
</ResponseField>

<ResponseField name="skills" type="number">
Local `SkillRegistry` skill count.
</ResponseField>

<ResponseField name="auth" type="string">
`"jwt"` when JWT validation is enabled; `"disabled"` in dev mode.
</ResponseField>

<ResponseField name="db" type="string">
`"disabled"` or `"connected"` depending on `ATI_DB_URL` and pool state.
</ResponseField>

<RequestExample>

```bash
curl -s http://127.0.0.1:8090/health
```

</RequestExample>

<ResponseExample>

```json
{
  "status": "ok",
  "version": "0.1.0",
  "tools": 42,
  "providers": 8,
  "skills": 3,
  "auth": "jwt",
  "db": "disabled"
}
```

</ResponseExample>

:::endpoint GET /.well-known/jwks.json
JWKS document for ES256 JWT validation. Precomputed at startup from `ATI_JWT_PUBLIC_KEY`.
:::

| Status | Body |
|--------|------|
| 200 | JWKS JSON (`keys` array) |
| 404 | `{"error": "JWKS not configured"}` |

## Tool execution

:::endpoint POST /call
Execute one tool by name. Dispatches on provider `handler`: `mcp`, `cli`, `file_manager`, or default HTTP/OpenAPI path via `core/http`.
:::

<ParamField body="tool_name" type="string" required>
Manifest tool name, e.g. `finnhub:quote` or `mock_search`. Underscore names are auto-resolved to colon form (`finnhub_quote` → `finnhub:quote`).
</ParamField>

<ParamField body="args" type="object | array | string" required={false}>
HTTP/MCP/OpenAPI: JSON object of parameters (default `{}`). CLI: JSON array of positional strings, a single whitespace-separated string, or `{"_positional": [...]}`.
</ParamField>

<ParamField body="raw_args" type="string[]" required={false}>
Deprecated CLI path; when set, takes precedence over `args` for positional parsing.
</ParamField>

<ResponseField name="result" type="object">
Processed tool output (may be upstream JSON after `response.extract` / `format`).
</ResponseField>

<ResponseField name="error" type="string">
Human-readable failure; omitted on success.
</ResponseField>

| HTTP status | Typical cause |
|-------------|----------------|
| 200 | Success (`error` may still be set in body for logical failures returned as 200 from client) |
| 400 | Body read failure; invalid `X-Ati-Upstream-Url` without `mcp_url_env` |
| 403 | Missing JWT; scope denial; upstream URL not on allowlist |
| 404 | Unknown tool |
| 422 | Invalid JSON body |
| 429 | Rate limit from JWT `ati` rate claims |
| 502 | MCP/CLI/HTTP upstream failure |
| 500 | Response processing error (raw upstream kept in `result`) |

<Warning>
`POST /call` body limit is ~1.37 GiB (derived from `file_manager::MAX_UPLOAD_BYTES` base64 overhead). Large uploads still hit per-tool `MAX_UPLOAD_BYTES` (1 GiB) inside the handler.
</Warning>

<RequestExample>

```bash
export ATI_PROXY_URL=http://127.0.0.1:8090
export ATI_SESSION_TOKEN=eyJ...

curl -s -X POST "$ATI_PROXY_URL/call" \
  -H "Authorization: Bearer $ATI_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"tool_name":"mock_search","args":{"query":"hello"}}'
```

</RequestExample>

<ResponseExample>

```json
{
  "result": {
    "results": [{"title": "Result for: hello", "score": 0.95}],
    "total": 2
  }
}
```

</ResponseExample>

## MCP JSON-RPC

:::endpoint POST /mcp
MCP protocol gateway. Accepts a single JSON-RPC 2.0 object; returns JSON-RPC envelope (HTTP 200 even for JSON-RPC errors).
:::

Supported methods:

| Method | Behavior |
|--------|----------|
| `initialize` | Returns protocol `2025-03-26`, server `ati-proxy` |
| `notifications/initialized` | HTTP 202, null body |
| `tools/list` | Scope-filtered tools with `inputSchema` |
| `tools/call` | Executes tool; MCP providers use native MCP; CLI/HTTP bridged |

JSON-RPC error codes used by the proxy:

| Code | Meaning |
|------|---------|
| -32601 | Unknown method |
| -32602 | Invalid params (e.g. missing tool name, bad upstream header) |
| -32001 | Scope or allowlist denial |

`tools/call` success wraps non-string JSON as pretty-printed text in `content[0].text`.

## Help (LLM assist)

:::endpoint POST /help
LLM-powered discovery over scope-visible tools and resolved skills. Requires internal `_llm` provider (`_chat_completion` tool) and keyring API key.
:::

<ParamField body="query" type="string" required>
User question for the assist model.
</ParamField>

<ParamField body="tool" type="string" required={false}>
When set, narrows the system prompt to that tool or provider; returns 403 if not visible in caller scopes.
</ParamField>

<ResponseField name="content" type="string">
Model completion text (from `/choices/0/message/content`).
</ResponseField>

<ResponseField name="error" type="string">
Configuration or upstream failure message.
</ResponseField>

<Info>
The handler uses model `zai-glm-4.7` with `max_completion_tokens: 1536` and `temperature: 0.3`. Include `help` in JWT scopes when issuing tokens if you want agents to use assist consistently (see scope reference).
</Info>

## Tool discovery

:::endpoint GET /tools
List tools visible to the caller's JWT scopes.
:::

Query parameters: `provider`, `search` (matches name, description, tags).

Response: JSON array of `{ name, description, provider, method, tags, skills, input_schema }`.

:::endpoint GET /tools/{name}
Detailed tool record including `endpoint`, `hint`, merged `skills`, and `scope`.
:::

Returns 404 when the tool is missing or not allowed by scopes.

## Local skills (`/skills`)

Skills come from `~/.ati/skills/` (or `--ati-dir`). All routes filter by `visible_skill_names` derived from JWT scopes.

:::endpoint GET /skills
List installed skills. Query: `category`, `provider`, `tool`, `search`.
:::

:::endpoint GET /skills/{name}
Default: `{ name, version, description, content }` (SKILL.md body). `?meta=true` returns metadata only. `?refs=true` adds `references` array.
:::

:::endpoint GET /skills/{name}/bundle
All files under the skill directory. Text files as strings; binary as `{ "base64": "..." }`.
:::

:::endpoint POST /skills/resolve
Resolve skills for an explicit scope list (does not expand caller JWT scopes into the request—intersection with caller visibility still applies).
:::

<ParamField body="scopes" type="string[]" required>
Scope strings, e.g. `["tool:finnhub:*", "skill:financial-analysis"]`.
</ParamField>

<ParamField body="include_content" type="boolean" required={false}>
When true, embed SKILL.md `content` in each resolved entry.
</ParamField>

:::endpoint POST /skills/bundle
Batch fetch bundles for up to 50 skill names.
:::

<ParamField body="names" type="string[]" required>
Skill names to bundle.
</ParamField>

Response: `{ "skills": { "<name>": { "files": { ... } } }, "missing": ["..."] }`.

## Remote SkillATI (`/skillati`)

Requires `ATI_SKILL_REGISTRY` (e.g. `gcs://bucket` on the proxy host). Catalog and files are fetched lazily; results are scope-filtered like local skills.

:::endpoint GET /skillati/catalog
Remote skill catalog. Optional `?search=` (filtered to 25 matches).
:::

Response: `{ "skills": [ RemoteSkillMeta, ... ] }` where each entry includes `name`, `description`, `skill_directory`, optional `when_to_use`, `keywords`, `tools`, `providers`, `categories`.

:::endpoint GET /skillati/{name}
Level-2 activation payload (`SkillAtiActivation`): `name`, `description`, `skill_directory`, `content` (SKILL.md with `${ATI_SKILL_DIR}` / cross-skill refs rewritten to `skillati://` URIs).
:::

:::endpoint GET /skillati/{name}/resources
List resource paths. Query: `prefix`.
:::

:::endpoint GET /skillati/{name}/file
Read one file. Query: `path` (required).
:::

:::endpoint GET /skillati/{name}/refs
List reference documents.
:::

:::endpoint GET /skillati/{name}/ref/{reference}
Read one reference by name.
:::

SkillATI errors return `{ "error": "<message>" }` with status 503 (not configured), 404 (not found), 400 (invalid path), or 502 (GCS/proxy failure).

## Admin virtual keys (`/admin/keys`)

<Warning>
Requires `cargo build --features db`, `ATI_DB_URL`, and `ATI_ADMIN_TOKEN`. Without them, admin routes return 503 with `admin endpoints require ATI_DB_URL + ATI_ADMIN_TOKEN`.
</Warning>

Admin routes use a **separate** bearer (`ATI_ADMIN_TOKEN`), not agent JWTs.

:::endpoint POST /admin/keys/issue
Create a revocable virtual key. Returns 201 with `raw_key`, `hash`, `alias`, `expires_at`.
:::

<ParamField body="user_id" type="string" required>
Owner identifier for listing and audit.
</ParamField>

<ParamField body="alias" type="string" required>
Human-readable key alias.
</ParamField>

<ParamField body="tools" type="string[]" required={false}>
Allowed tool scopes stored on the key row.
</ParamField>

<ParamField body="providers" type="string[]" required={false}>
Allowed provider scopes.
</ParamField>

<ParamField body="categories" type="string[]" required={false}>
Allowed category scopes.
</ParamField>

<ParamField body="skills" type="string[]" required={false}>
Allowed skill scopes.
</ParamField>

<ParamField body="expires_in_secs" type="number" required={false}>
TTL from issuance time.
</ParamField>

:::endpoint GET /admin/keys?user_id={id}
List active sessions for a user. `user_id` query param required.
:::

:::endpoint GET /admin/keys/{hash}
Metadata for one key (no raw secret).
:::

:::endpoint DELETE /admin/keys/{hash}
Revoke one key. Optional `?by=` audit actor (default `admin`).
:::

:::endpoint POST /admin/keys/bulk-revoke
Revoke by `user_id`, `alias_prefix`, and/or `hashes[]`. Returns `{ "revoked": <count> }`.
:::

Agents use issued keys via `Authorization: Ati-Key <raw_key>` on standard proxy routes.

## Proxy client (`ATI_PROXY_URL`)

When `ATI_PROXY_URL` is set, the CLI proxy client forwards calls instead of using the local keyring:

| CLI action | HTTP call |
|------------|-----------|
| `ati run <tool>` | `POST /call` |
| MCP integrations | `POST /mcp` |
| `ati assist` (proxy mode) | `POST /help` |
| Skill listing | `GET /skills` |
| Skill resolve | `POST /skills/resolve` |

Token resolution: `ATI_SESSION_TOKEN`, `<NAME>_FILE`, or per-provider `auth_session_token_env` with fallback to `ATI_SESSION_TOKEN`. Timeout: 120 seconds.

## Passthrough fallback

With `--enable-passthrough`, unmatched host+path requests hit `core/passthrough::handle_passthrough` after named routes fail. Those requests **skip JWT** and rely on HMAC sig-verify plus manifest-defined upstream routing. Named ATI paths always require JWT when configured, even if a passthrough manifest would match a similar URL.

## Verification

<Steps>
<Step title="Start proxy">
Run `ati proxy --port 8090 --ati-dir ~/.ati` (add `--migrate` and `ATI_DB_URL` when using persistence).
</Step>
<Step title="Probe health">
`curl -sf http://127.0.0.1:8090/health` should return `"status":"ok"`.
</Step>
<Step title="Exercise /call">
Set `ATI_PROXY_URL` and `ATI_SESSION_TOKEN`, then `ati run <tool> --output json` or `bash scripts/test_proxy_server_e2e.sh` for a full client→proxy→upstream round-trip.
</Step>
</Steps>

## Related pages

<CardGroup>
<Card title="Deploy proxy server" href="/deploy-proxy-server">
Bind address, `--env-keys`, passthrough, sig-verify modes, and production probes.
</Card>
<Card title="Configure JWT and keys" href="/configure-jwt-and-keys">
Token issuance, JWKS setup, and session token env vars.
</Card>
<Card title="JWT and scopes reference" href="/jwt-scopes-reference">
Scope grammar, wildcards, and validation failures.
</Card>
<Card title="Skills and SkillATI" href="/skills-and-skillati">
Progressive disclosure and remote registry layout.
</Card>
<Card title="Environment variables" href="/environment-variables">
`ATI_PROXY_URL`, `ATI_SESSION_TOKEN`, `ATI_DB_URL`, `ATI_ADMIN_TOKEN`, and SkillATI registry vars.
</Card>
</CardGroup>
