# Manifest reference

> TOML schema for `[provider]` and `[[tools]]`: `handler`, auth types, MCP/OpenAPI/CLI/passthrough fields, response `extract`/`format`, overrides, and validation errors at load time.

- 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/core/manifest.rs`
- `manifests/README.md`
- `manifests/finnhub.toml`
- `manifests/github-mcp.toml`
- `manifests/google-workspace.toml`
- `tests/manifest_test.rs`
- `src/core/response.rs`

---

---
title: "Manifest reference"
description: "TOML schema for `[provider]` and `[[tools]]`: `handler`, auth types, MCP/OpenAPI/CLI/passthrough fields, response `extract`/`format`, overrides, and validation errors at load time."
---

Each provider is one `*.toml` file under `$ATI_DIR/manifests/` (default `~/.ati/manifests/`). `ManifestRegistry::load` reads every file, parses `[provider]` plus optional `[[tools]]`, runs handler-specific validation, auto-registers OpenAPI and `file_manager` tools, and builds a flat `tool_name → (provider, tool)` index used by `ati run` and the proxy.

```mermaid
flowchart TB
  subgraph disk["$ATI_DIR"]
    M["manifests/*.toml"]
    S["specs/*.json"]
    C["cache/providers/*.json"]
  end
  subgraph load["ManifestRegistry::load"]
    P["toml::from_str → Manifest"]
    V["Handler validation"]
    O["openapi::load_and_register"]
    FM["register_file_manager_provider"]
    I["tool_index HashMap"]
  end
  M --> P --> V
  S --> O
  O --> I
  V --> I
  C --> P
  FM --> I
```

## File layout

| Path | Role |
|------|------|
| `$ATI_DIR/manifests/<name>.toml` | Permanent provider definition |
| `$ATI_DIR/specs/<spec>.json` | OpenAPI specs referenced by `openapi_spec` |
| `$ATI_DIR/cache/providers/<name>.json` | Ephemeral providers from `ati provider load` (skipped when a TOML manifest with the same `name` exists) |

One file = one `[provider]` block. Hand-written HTTP providers add one or more `[[tools]]` sections. MCP, OpenAPI, CLI (optional), passthrough, and `file_manager` providers often ship with zero `[[tools]]` and rely on discovery or built-in registration.

## Top-level shape

```toml
[provider]
name = "my_api"
description = "Human-readable summary"
handler = "http"          # default when omitted
base_url = "https://api.example.com/v1"
auth_type = "bearer"
auth_key_name = "my_api_key"

[[tools]]
name = "search"
description = "Search endpoint"
endpoint = "/search"
method = "POST"
scope = "tool:search"

[tools.input_schema]
type = "object"
required = ["query"]

[tools.input_schema.properties.query]
type = "string"

[tools.response]
extract = "$.results[*]"
format = "markdown_table"
```

<Note>
Serde accepts unknown keys on `[provider]` silently. Only fields defined on `Provider` and `Tool` in `src/core/manifest.rs` affect runtime.
</Note>

## `[provider]` — common fields

| Field | Type | Default | Purpose |
|-------|------|---------|---------|
| `name` | string | — | Provider id; used in tool prefixes and dispatch |
| `description` | string | — | Shown in `ati tool list` / assist context |
| `handler` | string | `http` | Dispatch path: `http`, `openapi`, `mcp`, `cli`, `file_manager`, `passthrough` |
| `base_url` | string | `""` | HTTP/OpenAPI base; passthrough upstream URL (required for passthrough) |
| `auth_type` | enum | `none` | `bearer`, `header`, `query`, `basic`, `oauth2`, `url`, `none` |
| `auth_key_name` | string? | — | Keyring entry for static auth |
| `auth_header_name` | string? | — | Header name when `auth_type = "header"` (default `X-Api-Key`) |
| `auth_query_name` | string? | — | Query param when `auth_type = "query"` (default `api_key`) |
| `auth_value_prefix` | string? | — | Prefix before key in header value (e.g. `Token `) |
| `extra_headers` | map | `{}` | Headers on every request; values may use `${keyring_key}` |
| `oauth2_token_url` | string? | — | Token endpoint (relative to `base_url` or absolute) |
| `auth_secret_name` | string? | — | Keyring key for OAuth2 client secret |
| `oauth2_basic_auth` | bool | `false` | Send client credentials as Basic Auth on token request |
| `auth_session_token_env` | string? | — | Env var for per-provider JWT to proxy (default `ATI_SESSION_TOKEN`) |
| `internal` | bool | `false` | Hide from public tool listing; skips auto-scope on tools |
| `category` | string? | — | Discovery grouping (e.g. `finance`) |
| `skills` | string[] | `[]` | Skill catalog URLs/names bound to this provider |

### Auth types

| `auth_type` | Runtime behavior |
|-------------|------------------|
| `bearer` | `Authorization: Bearer <keyring[key]>` |
| `header` | Custom header from `auth_header_name` + optional `auth_value_prefix` |
| `query` | Appends `auth_query_name=<key>` to query string |
| `basic` | HTTP Basic; keyring value must be `user:password` |
| `oauth2` | Client credentials flow via `oauth2_token_url` + `auth_secret_name` |
| `url` | Key interpolated into URL placeholders (e.g. MCP URL `${serpapi_api_key}`) |
| `none` | No credential injection |

### Dynamic credentials — `[provider.auth_generator]`

Optional subprocess that mints short-lived credentials at call time (proxy or local mode).

| Field | Purpose |
|-------|---------|
| `type` | `command` or `script` |
| `command` / `args` | Executable for `command` type |
| `interpreter` / `script` | Inline script for `script` type |
| `cache_ttl_secs` | Cache duration (`0` = no cache) |
| `output_format` | `text` (trimmed stdout) or `json` |
| `env` | Subprocess env; supports `${key}`, `${JWT_SUB}`, `${JWT_SCOPE}`, `${TOOL_NAME}`, `${TIMESTAMP}` |
| `inject` | For JSON output: map JSON paths → `{ type = "header"\|"env"\|"query", name = "..." }` |
| `timeout_secs` | Subprocess cap (default `30`) |

## `[provider]` — handler-specific fields

### `handler = "http"` (default)

Hand-written `[[tools]]` with `endpoint`, `method`, and JSON Schema. Parameters without `x-ati-param-location` use legacy routing: GET → query string, POST/PUT/DELETE → JSON body.

### `handler = "openapi"`

Tools come from the spec at load time; manifest `[[tools]]` are ignored.

| Field | Purpose |
|-------|---------|
| `openapi_spec` | Filename under `$ATI_DIR/specs/` or URL |
| `openapi_include_tags` / `openapi_exclude_tags` | Tag filters |
| `openapi_include_operations` / `openapi_exclude_operations` | `operationId` filters |
| `openapi_max_operations` | Cap tool count (e.g. `50` on Finnhub) |
| `openapi_overrides.<operationId>` | Per-operation metadata (see below) |

Discovered tool names are `{provider}:{operationId}` (auto-generated ids when the spec omits `operationId`). Scopes default to `tool:{prefixed_name}` unless overridden.

**OpenAPI override block** — table key is the bare `operationId`:

```toml
[provider.openapi_overrides.getPetById]
hint = "Fetch one pet by numeric ID"
description = "Get a pet (overridden)"
tags = ["lookup"]
scope = "tool:petstore:getPetById"
response_extract = "$.pet"
response_format = "json"   # markdown_table | json | raw | (else text)
```

If the spec file is missing or fails to parse, load logs a warning and continues with **zero tools** for that provider (graceful degradation).

### `handler = "mcp"`

| Field | Purpose |
|-------|---------|
| `mcp_transport` | `stdio` (default) or `http` |
| `mcp_command` / `mcp_args` | Stdio subprocess (e.g. `npx`, `["-y", "@modelcontextprotocol/server-github"]`) |
| `mcp_url` | HTTP/Streamable HTTP endpoint |
| `mcp_env` | Env for stdio; `${github_token}` resolves from keyring |
| `mcp_url_env` | **HTTP only.** Sandbox env var whose value the proxy reads as upstream URL override |

MCP tools register at runtime as `{provider}:{mcp_tool_name}` after `tools/list`. No `[[tools]]` required in the manifest.

```toml
# manifests/github-mcp.toml — stdio MCP
[provider]
name = "github"
handler = "mcp"
mcp_transport = "stdio"
mcp_command = "npx"
mcp_args = ["-y", "@modelcontextprotocol/server-github"]
auth_type = "none"

[provider.mcp_env]
GITHUB_PERSONAL_ACCESS_TOKEN = "${github_token}"
```

```toml
# manifests/deepwiki-mcp.toml — HTTP MCP
[provider]
name = "deepwiki"
handler = "mcp"
mcp_transport = "http"
mcp_url = "https://mcp.deepwiki.com/mcp"
auth_type = "none"
```

### `handler = "cli"`

| Field | Purpose |
|-------|---------|
| `cli_command` | Binary name (e.g. `gws`, `gh`) |
| `cli_default_args` | Prepended to every invocation |
| `cli_env` | `${key}` = keyring string; `@{key}` = credential file path |
| `cli_timeout_secs` | Default `120` |
| `cli_output_args` | Flags like `--output` whose value is a capture path (proxy mode) |
| `cli_output_positional` | Map subcommand prefix → 0-based index of output file arg |

If `[[tools]]` is empty, load injects one implicit tool named after the provider with scope `tool:{provider_name}`.

```toml
# manifests/google-workspace.toml
[provider]
name = "google_workspace"
handler = "cli"
cli_command = "gws"
cli_timeout_secs = 120
auth_type = "none"
skills = ["google-workspace"]

[provider.cli_env]
GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE = "@{google_workspace_credentials}"
```

### `handler = "file_manager"`

Declares upload allowlist; built-in `file_manager:download` and `file_manager:upload` attach when `[[tools]]` is empty.

```toml
[provider]
name = "file_manager"
handler = "file_manager"
upload_default_destination = "fal"

[provider.upload_destinations.fal]
kind = "fal_storage"
key_ref = "fal_api_key"

[provider.upload_destinations.gcs]
kind = "gcs"
bucket = "my-bucket"
prefix = "uploads"
```

Empty `upload_destinations` disables uploads at runtime.

### `handler = "passthrough"`

Raw HTTP reverse-proxy routes on `ati proxy` — typically **no** `[[tools]]`.

| Field | Default | Purpose |
|-------|---------|---------|
| `host_match` | — | Route by `Host` header |
| `path_prefix` | — | Route by URI prefix (must start with `/`; trailing `/` normalized) |
| `strip_prefix` | `true` | Remove prefix before forwarding |
| `path_replace` | — | Tuple `["/from", "/to"]` after strip |
| `host_override` | — | Upstream `Host` header only (SNI follows `base_url`) |
| `deny_paths` | `[]` | Post-rewrite path globs → 403 |
| `forward_authorization_paths` | `[]` | Forward sandbox `Authorization` instead of manifest auth |
| `connect_timeout_seconds` | `5` | TCP connect |
| `read_timeout_seconds` | `300` | Full response read |
| `idle_timeout_seconds` | `60` | Pool idle |
| `max_request_bytes` / `max_response_bytes` | 100MB / 500MB | `0` = unlimited |
| `forward_websockets` | `false` | Requires `http://` or `https://` `base_url` when enabled |

At least one of `host_match` or `path_prefix` is required; `base_url` must be non-empty.

## `[[tools]]` — per-tool fields

| Field | Default | Purpose |
|-------|---------|---------|
| `name` | — | Registry key (see naming below) |
| `description` | — | Tool docs / assist |
| `endpoint` | `""` | Path appended to `base_url` (HTTP/OpenAPI) |
| `method` | `GET` | `GET`, `POST`, `PUT`, `DELETE` (case-insensitive aliases accepted) |
| `scope` | auto | JWT scope gate; see below |
| `input_schema` | — | JSON Schema object for CLI args |
| `response` | passthrough | Post-processing (see next section) |
| `tags` | `[]` | Discovery tags |
| `hint` | — | Short LLM guidance |
| `examples` | `[]` | Example invocations |

### Tool naming conventions

| Handler | Indexed `name` | Example |
|---------|----------------|---------|
| HTTP (hand-written) | As written in manifest | `hackernews_top_stories` |
| OpenAPI | `{provider}:{operationId}` | `finnhub:quote` |
| MCP (discovered) | `{provider}:{mcp_name}` | `github:search_repositories` |
| CLI (auto) | `{provider}` | `google_workspace` |
| `file_manager` (built-in) | `file_manager:download`, `file_manager:upload` | Fixed |

Duplicate `name` values across files: **last loaded file wins** in the `tool_index` map.

### Scope auto-assignment

After load, any tool without `scope` on a non-`internal` provider gets `scope = "tool:{tool.name}"`. Internal providers (e.g. `_llm` for `ati assist`) leave `scope` unset so tools stay outside default JWT filtering.

Shared scopes are valid — multiple Hacker News tools use `scope = "tool:hackernews_stories"` for one JWT grant.

## `[tools.response]` — extract and format

Processed in `core/response.rs` after the upstream returns JSON.

| Field | Values | Behavior |
|-------|--------|----------|
| `extract` | JSONPath string | Subset of body; empty match → `null`; multiple matches → array |
| `format` | `text` (default), `json`, `markdown_table`, `raw` | CLI/table rendering hint |

Omit `[tools.response]` to return the raw parsed body. Invalid JSONPath fails the call with `JSONPath extraction failed`.

```toml
[tools.response]
extract = "$.results[*]"
format = "markdown_table"
```

OpenAPI overrides use `response_extract` / `response_format` on the override table instead of a nested `[tools.response]` block.

## Load-time validation and errors

`ManifestError` variants:

| Variant | When |
|---------|------|
| `NoDirectory` | Manifest path is not a directory |
| `Io` | Read/glob failure |
| `Parse` | Invalid TOML (`toml::de::Error`) |
| `Invalid` | Semantic validation failed |

### `Invalid` messages (fail load for that file)

| Condition | Message pattern |
|-----------|-----------------|
| `file_manager` + `upload_default_destination` not in map | `upload_default_destination 'X' is not present in [provider.upload_destinations]` |
| `mcp_url_env` empty/whitespace | `mcp_url_env must not be empty when set` |
| `mcp_url_env` bad POSIX name | `mcp_url_env '…' is not a valid POSIX env var name` |
| `mcp_url_env` on non-HTTP MCP | `mcp_url_env requires handler = "mcp" and mcp_transport = "http"` |
| Passthrough empty `base_url` | `passthrough provider requires non-empty base_url` |
| Passthrough no route | `passthrough provider requires at least one of host_match or path_prefix` |
| `path_prefix` without leading `/` | `passthrough path_prefix must start with '/'` |
| Empty `host_match` | `passthrough host_match must be non-empty when set` |
| `forward_websockets` + bad scheme | `forward_websockets requires base_url to use http:// or https://` |
| `path_replace` source without `/` | `passthrough path_replace source must start with '/'` |

OpenAPI spec errors do **not** produce `Invalid`; they warn and register the provider with no tools.

After all TOML files load, `register_file_manager_provider` ensures `file_manager` exists (default empty destinations if no operator manifest).

## Minimal examples by handler

<CodeGroup>
```toml title="HTTP — manifests/hackernews.toml"
[provider]
name = "hackernews"
base_url = "https://hacker-news.firebaseio.com/v0"
auth_type = "none"

[[tools]]
name = "hackernews_top_stories"
endpoint = "/topstories.json"
method = "GET"
scope = "tool:hackernews_stories"

[tools.response]
format = "json"
```

```toml title="OpenAPI — manifests/finnhub.toml"
[provider]
name = "finnhub"
handler = "openapi"
base_url = "https://finnhub.io/api/v1"
openapi_spec = "finnhub.json"
auth_type = "query"
auth_query_name = "token"
auth_key_name = "finnhub_api_key"
openapi_max_operations = 50
```

```toml title="Internal — manifests/_llm.toml"
[provider]
name = "_llm"
internal = true
auth_type = "bearer"
auth_key_name = "cerebras_api_key"

[[tools]]
name = "_chat_completion"
endpoint = "/chat/completions"
method = "POST"
```
</CodeGroup>

## Verification

<Steps>
<Step title="Place manifest">
Copy `my_provider.toml` into `$ATI_DIR/manifests/`. For OpenAPI, place the spec in `$ATI_DIR/specs/`.
</Step>
<Step title="List tools">
Run `ati tool list` and confirm provider tools appear (MCP tools appear after first discovery call or proxy startup).
</Step>
<Step title="Inspect one tool">
Run `ati tool info <name>` — for OpenAPI/MCP use the prefixed name (`finnhub:quote`, `github:search_repositories`).
</Step>
</Steps>

<Warning>
A silent OpenAPI load failure looks like an empty provider in `ati tool list`. Check logs with `RUST_LOG=warn` or verify the spec path under `$ATI_DIR/specs/`.
</Warning>

## Related pages

<CardGroup>
<Card title="Providers and handlers" href="/providers-and-handlers">
How each `handler` maps to `core/http`, `mcp_client`, and CLI execution.
</Card>
<Card title="CLI and HTTP manifests" href="/cli-and-http-manifests">
Hand-written tools, `${key}` / `@{key}`, and `cli_output_args`.
</Card>
<Card title="Import OpenAPI" href="/import-openapi">
Spec import, filters, and `x-ati-param-location`.
</Card>
<Card title="Add MCP providers" href="/add-mcp-providers">
`ati provider add-mcp` and namespaced MCP tool calls.
</Card>
<Card title="JWT and scopes reference" href="/jwt-scopes-reference">
Scope grammar matched against `tools.scope` and auto-assigned scopes.
</Card>
</CardGroup>
