Manifest Reference
A manifest is an author-declared entitlements file: it states exactly which image runs, which hosts it may reach, which credentials it needs and how they are delivered, and which tools it exposes. One file per server version, at manifests/<name>/<version>.yaml. The path must match the manifest's name and version.
The authoritative schema is the Go module in schema/ (Apache-2.0). The gateway embeds the same module, so a manifest that passes registry lint parses identically at install time. Parsing is strict: unknown fields are errors, so a typo in a security-relevant field (e.g. egres:) cannot silently grant nothing.
Example
A real sealed-tier manifest (manifests/linear/0.1.0.yaml):
schemaVersion: 1
name: linear
version: 0.1.0
source:
repo: github.com/gigmcp/toolpack
tag: v0.1.0
image:
ref: ghcr.io/gigmcp/linear-mcp
# PLACEHOLDER until build-images CI emits the real digest — see README.
digest: sha256:0000000000000000000000000000000000000000000000000000000000000000
entrypoint: /app/server
builder: toolpack
tier: sealed
entitlements:
egress:
- api.linear.app
credentials:
- id: linear_token
type: oauth2
provider: linear
scopes: [read, write, issues:create, comments:create]
inject:
header: Authorization
format: "Bearer {token}"
tools:
- name: list_issues
default: true
- name: create_issue
default: false
# ...
Top-level fields
| Field | Type | Required | Constraints |
|---|---|---|---|
schemaVersion | int | yes | Must be 1 |
name | string | yes | Unique; lowercase [a-z0-9-], must start and end alphanumeric, no underscores. Used as the tool-namespace prefix |
version | string | yes | Strict semver MAJOR.MINOR.PATCH (no leading zeros, no v prefix) |
source | object | yes | Where CI builds the image from — see below |
image | object | yes | What the gateway runs — see below |
tier | string | yes | sealed or entrusted |
entitlements | object | yes | Egress allowlist — see below |
credentials | array | no | Credential schema — see below |
tools | array | no | Tool subset and defaults — see below |
source
| Field | Type | Required | Constraints |
|---|---|---|---|
source.repo | string | yes | The author's git repo (e.g. github.com/gigmcp/toolpack) |
source.tag | string | yes | The git tag CI builds from |
source.package | string | no | Directory within the repo containing the server's main package (the go-static builder runs go build . there). Default .. Must match ^[a-zA-Z0-9._/-]+$, must not contain .., must not be absolute (path-injection guards) |
image
| Field | Type | Required | Constraints |
|---|---|---|---|
image.ref | string | yes | OCI image reference (e.g. ghcr.io/gigmcp/linear-mcp) |
image.digest | string | yes | sha256: + 64 hex chars. This is the platform (linux/amd64) image-manifest digest, not a multi-arch index digest — the gateway compares the pulled image's digest against it. The approved digest is what runs |
image.entrypoint | string | yes | Absolute path of the static binary inside the image — by convention /app/server (see builders) |
image.builder | string | no | Build recipe: go-static (default when omitted), toolpack, node, or python. Anything else is rejected |
:::note Placeholder digests
Catalog entries that are not yet built use sha256:0000…0000 (64 zeros) so fakes are visually obvious. The gateway will not install them — the digest comparison fails by construction.
:::
tier
Either sealed or entrusted:
sealed— the sandbox never sees the real secret. The egress proxy replaces a placeholder token with the real credential, only for allowlisted hosts.entrusted— the real secret is delivered as an environment variable inside the sandbox. Reserved for services where header injection is impossible (e.g. key-in-URL APIs).
The tier dictates the shape of every credential's inject block (validated, see below).
entitlements
| Field | Type | Required | Constraints |
|---|---|---|---|
entitlements.egress | string array | no | Hostnames the server may reach. Exact host (api.linear.app) or leading wildcard *.suffix (*.atlassian.net). Semantics match the proxy's allowed() exactly: case-insensitive exact match, or *.suffix matching true subdomains only — the bare suffix itself does not match |
Each egress entry must pass CheckEgressEntry:
- Lowercase hostname labels only (
a-z0-9, hyphens inside a label). - Wildcards only as a leading
*., and the suffix must keep at least two labels (*.example.comis valid;*.comis too broad and rejected). - No bare or embedded wildcards (
api.*.comis rejected). - No ports, no paths (
:and/are rejected). - No raw IP literals.
credentials
Each entry describes one credential the server needs:
| Field | Type | Required | Constraints |
|---|---|---|---|
id | string | yes | Unique within the manifest, non-empty |
type | string | yes | oauth2, api_key, basic, or custom_env |
provider | string | yes | The credential provider name (e.g. linear) |
scopes | string array | no | OAuth scopes, where applicable |
inject | object | yes | Secret-delivery mode — exactly one mode, dictated by tier |
inject
| Field | Type | Used by | Constraints |
|---|---|---|---|
inject.header | string | sealed | HTTP header the proxy rewrites (e.g. Authorization) |
inject.format | string | sealed | Header value template; must contain {token} (e.g. "Bearer {token}") |
inject.env | string | entrusted | Environment variable name the real secret is delivered in (e.g. ALCHEMY_API_KEY) |
Validation enforces tier coherence per credential:
- sealed:
inject.headerandinject.format(containing{token}) are required;inject.envmust be empty. - entrusted:
inject.envis required;inject.headerandinject.formatmust be empty.
An entrusted example (manifests/alchemy/0.1.0.yaml) — Alchemy embeds the API key in the URL path, so header injection cannot work:
tier: entrusted
entitlements:
egress:
- "*.g.alchemy.com"
credentials:
- id: alchemy_api_key
type: api_key
provider: alchemy
inject:
env: ALCHEMY_API_KEY
tools
| Field | Type | Required | Constraints |
|---|---|---|---|
name | string | yes | Unique within the manifest, non-empty |
default | bool | no (defaults false) | Whether the tool is exposed by default; non-default tools must be explicitly enabled |
For builder: toolpack manifests, the tool set must exactly equal the paired toolspec's tool set — enforced by registryctl lint-toolspecs.
Validation vs. lint
Two layers of checking, both run by registryctl:
Validate()— structural rules that need no external data: schema version, name/version formats, required fields, digest format, builder whitelist, tier, egress syntax, credential/tier coherence, tool uniqueness.Lint()—Validate()plus registry policy: no egress entry may equal or be a subdomain of an entry indenylist/exfil-domains.txt(known data-exfiltration and request-capture services such aswebhook.site,pastebin.com,ngrok.io, plus internal/link-local names likelocalhost). Comment lines (#) and blanks in the denylist are ignored.
CI runs lint on every PR. The same Validate() runs in the gateway when it parses the signed index.
Manifest hash and re-consent
Each manifest has a canonical hash: SHA-256 over the canonical JSON encoding of the parsed manifest (deterministic struct field order, no maps). A changed manifest changes the hash and triggers re-consent in the gateway; YAML formatting changes do not. See profiles for how consent is surfaced.
The signed index
On merge, CI compiles all manifests into index.json:
{
"schemaVersion": 1,
"generated": "<RFC3339 timestamp>",
"servers": {
"<name>": {
"latest": "<highest semver>",
"versions": { "<version>": { /* full manifest */ } }
}
}
}
The index is signed with ed25519 over the exact published bytes; the gateway verifies the raw bytes against GIG_REGISTRY_PUBKEY before parsing anything. Install refs resolve as name (latest), name@version, or sha256:<digest>.