Skip to main content

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

FieldTypeRequiredConstraints
schemaVersionintyesMust be 1
namestringyesUnique; lowercase [a-z0-9-], must start and end alphanumeric, no underscores. Used as the tool-namespace prefix
versionstringyesStrict semver MAJOR.MINOR.PATCH (no leading zeros, no v prefix)
sourceobjectyesWhere CI builds the image from — see below
imageobjectyesWhat the gateway runs — see below
tierstringyessealed or entrusted
entitlementsobjectyesEgress allowlist — see below
credentialsarraynoCredential schema — see below
toolsarraynoTool subset and defaults — see below

source

FieldTypeRequiredConstraints
source.repostringyesThe author's git repo (e.g. github.com/gigmcp/toolpack)
source.tagstringyesThe git tag CI builds from
source.packagestringnoDirectory 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

FieldTypeRequiredConstraints
image.refstringyesOCI image reference (e.g. ghcr.io/gigmcp/linear-mcp)
image.digeststringyessha256: + 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.entrypointstringyesAbsolute path of the static binary inside the image — by convention /app/server (see builders)
image.builderstringnoBuild 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

FieldTypeRequiredConstraints
entitlements.egressstring arraynoHostnames 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.com is valid; *.com is too broad and rejected).
  • No bare or embedded wildcards (api.*.com is rejected).
  • No ports, no paths (: and / are rejected).
  • No raw IP literals.

credentials

Each entry describes one credential the server needs:

FieldTypeRequiredConstraints
idstringyesUnique within the manifest, non-empty
typestringyesoauth2, api_key, basic, or custom_env
providerstringyesThe credential provider name (e.g. linear)
scopesstring arraynoOAuth scopes, where applicable
injectobjectyesSecret-delivery mode — exactly one mode, dictated by tier

inject

FieldTypeUsed byConstraints
inject.headerstringsealedHTTP header the proxy rewrites (e.g. Authorization)
inject.formatstringsealedHeader value template; must contain {token} (e.g. "Bearer {token}")
inject.envstringentrustedEnvironment variable name the real secret is delivered in (e.g. ALCHEMY_API_KEY)

Validation enforces tier coherence per credential:

  • sealed: inject.header and inject.format (containing {token}) are required; inject.env must be empty.
  • entrusted: inject.env is required; inject.header and inject.format must 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

FieldTypeRequiredConstraints
namestringyesUnique within the manifest, non-empty
defaultboolno (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 in denylist/exfil-domains.txt (known data-exfiltration and request-capture services such as webhook.site, pastebin.com, ngrok.io, plus internal/link-local names like localhost). 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.

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>.