You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The CLI already knows Outpost projects exist — project list reports them and project use will
switch to one — but nothing can be done with one once selected. Every gateway command rejects an
Outpost project by design, so a user whose active project is Outpost has a CLI that can run login, whoami and project use and nothing else.
Hookdeck Outpost has a managed API and published SDKs, but no CLI and no MCP server. This epic adds
both: a hookdeck outpost … command group mirroring the shape of hookdeck gateway …, and hookdeck outpost mcp.
Scope
The full managed Outpost API: tenants, destinations, events, attempts, retry, publish, topics,
destination types, metrics, operator config, custom domain, and deployment status.
hookdeck outpost
├── tenant|tenants list · get · upsert · delete · token · portal
├── destination|destinations list · get · create · update · delete · enable · disable
├── event|events list · get · retry
├── attempt|attempts list · get
├── publish
├── topic|topics list
├── destination-type|destination-types list · get
├── metrics events · attempts
├── config get · set · custom-domain (get · set · delete)
├── status
└── mcp [--allow-write]
Inventory
Phase 1 — API client
Outpost API base URL constant + GetOutpostAPIClient(), reusing the existing client (auth,
project header, telemetry, verbose transport and error types all carry over unchanged)
Hidden --outpost-api-base flag for dev/test, registered in the global-flag arity map
IsOutpostProject() alongside IsGatewayProject()
Per-resource client methods. Two shape differences from the Gateway API: array query params use
bracket notation (id[0]=…) and time filters use time[gte] / time[lte]; and the
tenant destination list is not paginated — it returns a bare array
Destination config/credentials validated dynamically from GET /destination-types rather
than hardcoded per-type structs, mirroring how source types are handled today. Warn and continue
if the fetch fails; let the API be the authority
Live validation of Phase 1 (done)
Phase 1's unit tests run against stub servers, so they could only assert their own assumptions. A
build-tagged live suite (//go:build outpostlive, test/acceptance/outpost_live_test.go) makes real
requests. It found two genuine bugs that no stub test could have:
Destination-type options is [{label, value}], not []string — decoding the real /destination-types list failed outright. The stub fixture had encoded the wrong shape, which is
exactly why the unit tests passed. Surfaced on kafka, the type the OpenAPI enum omits.
Only HTTP 200 was treated as success. The Event Gateway API answers 200 to everything, so
this had never surfaced; Outpost uses 201 on create and 202 on publish/retry, so every write
failed. Fixed with an opt-in Client.AcceptAnySuccessStatus set on the Outpost client only, so
Gateway behaviour is unchanged.
Now proven end to end against a real project: CLI-key auth against the Outpost host, tenant and
destination CRUD, the topics union decoding the bare "*" form, tenant token minting, publish with a
Project API key, event and attempt reads, and the tenant-scoped attempts route.
Phase 2 — Commands
outpost command group with an Outpost-project guard mirroring the Gateway one
Resource groups with singular Use + plural alias, --output json, and shared help text
Destructive commands (tenant delete, destination delete, config custom-domain delete) use
the existing confirm + --force helper
config set gets --dry-run — it changes delivery behaviour for every tenant at once
outpost publish needs a Project API key. The Publish API accepts a Project API key only;
the credentials stored by hookdeck login are not accepted for it. publish therefore takes --api-key (defaulting to HOOKDECK_API_KEY), like hookdeck ci. Every other outpost
command uses the stored credentials. Document in AGENTS.md, the command help, and the README
REFERENCE.md blocks + README section
Phase 2 notes
Two decisions worth recording, both made while building:
Config and credential fields are repeatable key=value pairs, not flat per-field flags. Cobra registers flags at init, but destination fields differ per type and are only known after fetching the schema, so flat flags would require a network call before a command could parse its own arguments. The schema is still used, for validation and for help. Dotted paths (--config a.b=c) are supported so a nested type shipping server-side would not need a CLI release. See Standardize config input: --config means two different things across gateway and outpost #347 for standardizing this with gateway.
--type <type> --help lists that type's fields. Cobra parses flags before running the help function, so once a type is named the help can show exactly its fields — cache-first, degrading silently to static help when unauthenticated, offline or cold. Plain --help is unchanged and needs no network. REFERENCE.md is provably unaffected, since the generator reads command metadata directly and never invokes help.
Phase 3 — MCP
Extract the product-agnostic MCP plumbing (input parsing, response envelope, error translation,
auth gate, login and projects tools, telemetry wrapper, server scaffolding) out of pkg/gateway/mcp into a shared package. Gateway behaviour must not change — its existing
tests are the regression gate
Generalise the argv sniffing in root.go so MCP stdout hygiene applies to any <group> mcp,
not just gateway mcp
Parameterise the tool-name prefix — the login and help tools currently hardcode hookdeck_ in
user-facing strings
Outpost tool set, one tool per resource with an action enum, outpost_-prefixed so both
servers can be configured in one client without name collisions
--allow-write (default off, HOOKDECK_MCP_ALLOW_WRITE). Write actions are omitted from the
schema entirely in read-only mode, with a handler-side guard behind it. Accept a bare --read-only as an explicit no-op alias; if both are passed, read-only wins
Treat credential-returning reads as writes for gating: tenant token mints a 24-hour tenant JWT
and portal returns a URL granting portal access. Both are GETs, so a naive read/write split
would leave them available in read-only mode
Set ReadOnlyHint / DestructiveHint tool annotations (the gateway server sets none today).
Per the MCP spec these are client UX hints and explicitly not a security boundary — the flag
does the enforcing
Fix the type check in projects use: it currently switches to any project id without checking
the type, so an agent can move the session to a project the server cannot serve
Testing prerequisites — blocking, needs someone with access
A development Outpost project with at least one topic configured, plus a Project API key for
testing publish
A separate throwaway CI Outpost project and repo secret for the acceptance slice. The
existing acceptance slices all authenticate against Gateway projects, so a new matrix entry and
secret are required. Keep it separate from the development project: these tests create and
delete tenants and destinations, and config set coverage mutates project-wide settings
On write tools in MCP
#228 sets out the split — skills + CLI for
building, MCP for investigation — and #340
restates that hookdeck gateway mcp is deliberately read-only.
hookdeck outpost mcp takes a different approach deliberately: it ships write tools, gated behind an
explicit --allow-write opt-in and off by default. The read-only default preserves the safe
out-of-the-box behaviour, while making the full surface available to operators who choose it.
The reasoning behind #228 does not transfer cleanly to Outpost. That split assumed skills + CLI
already covered the build path — true for the Event Gateway, which has published agent skills and full
CRUD commands, and not yet true for Outpost. More importantly, Outpost is operator-facing: managing
tenants and destinations is routine operational work, not just setup, and it is often done from
exactly the chat contexts #228 identifies as MCP's home ground.
The flag name follows the convention used by MCP servers that are read-only by default and opt in to
writes (the awslabs/mcp servers use exactly --allow-write). The more common --read-only spelling
is not used here because every server that uses it defaults to read-write, which is the wrong default
for this.
Why
The CLI already knows Outpost projects exist —
project listreports them andproject usewillswitch to one — but nothing can be done with one once selected. Every
gatewaycommand rejects anOutpost project by design, so a user whose active project is Outpost has a CLI that can run
login,whoamiandproject useand nothing else.Hookdeck Outpost has a managed API and published SDKs, but no CLI and no MCP server. This epic adds
both: a
hookdeck outpost …command group mirroring the shape ofhookdeck gateway …, andhookdeck outpost mcp.Scope
The full managed Outpost API: tenants, destinations, events, attempts, retry, publish, topics,
destination types, metrics, operator config, custom domain, and deployment status.
Inventory
Phase 1 — API client
GetOutpostAPIClient(), reusing the existing client (auth,project header, telemetry, verbose transport and error types all carry over unchanged)
--outpost-api-baseflag for dev/test, registered in the global-flag arity mapIsOutpostProject()alongsideIsGatewayProject()bracket notation (
id[0]=…) and time filters usetime[gte]/time[lte]; and thetenant destination list is not paginated — it returns a bare array
config/credentialsvalidated dynamically fromGET /destination-typesratherthan hardcoded per-type structs, mirroring how source types are handled today. Warn and continue
if the fetch fails; let the API be the authority
Live validation of Phase 1 (done)
Phase 1's unit tests run against stub servers, so they could only assert their own assumptions. A
build-tagged live suite (
//go:build outpostlive,test/acceptance/outpost_live_test.go) makes realrequests. It found two genuine bugs that no stub test could have:
optionsis[{label, value}], not[]string— decoding the real/destination-typeslist failed outright. The stub fixture had encoded the wrong shape, which isexactly why the unit tests passed. Surfaced on
kafka, the type the OpenAPI enum omits.this had never surfaced; Outpost uses 201 on create and 202 on publish/retry, so every write
failed. Fixed with an opt-in
Client.AcceptAnySuccessStatusset on the Outpost client only, soGateway behaviour is unchanged.
Now proven end to end against a real project: CLI-key auth against the Outpost host, tenant and
destination CRUD, the
topicsunion decoding the bare"*"form, tenant token minting, publish with aProject API key, event and attempt reads, and the tenant-scoped attempts route.
Phase 2 — Commands
outpostcommand group with an Outpost-project guard mirroring the Gateway oneUse+ plural alias,--output json, and shared help texttenant delete,destination delete,config custom-domain delete) usethe existing confirm +
--forcehelperconfig setgets--dry-run— it changes delivery behaviour for every tenant at onceoutpost publishneeds a Project API key. The Publish API accepts a Project API key only;the credentials stored by
hookdeck loginare not accepted for it.publishtherefore takes--api-key(defaulting toHOOKDECK_API_KEY), likehookdeck ci. Every otheroutpostcommand uses the stored credentials. Document in
AGENTS.md, the command help, and the READMEREFERENCE.mdblocks + README sectionPhase 2 notes
Two decisions worth recording, both made while building:
key=valuepairs, not flat per-field flags. Cobra registers flags at init, but destination fields differ per type and are only known after fetching the schema, so flat flags would require a network call before a command could parse its own arguments. The schema is still used, for validation and for help. Dotted paths (--config a.b=c) are supported so a nested type shipping server-side would not need a CLI release. See Standardize config input: --config means two different things across gateway and outpost #347 for standardizing this with gateway.--type <type> --helplists that type's fields. Cobra parses flags before running the help function, so once a type is named the help can show exactly its fields — cache-first, degrading silently to static help when unauthenticated, offline or cold. Plain--helpis unchanged and needs no network. REFERENCE.md is provably unaffected, since the generator reads command metadata directly and never invokes help.Phase 3 — MCP
auth gate, login and projects tools, telemetry wrapper, server scaffolding) out of
pkg/gateway/mcpinto a shared package. Gateway behaviour must not change — its existingtests are the regression gate
root.goso MCP stdout hygiene applies to any<group> mcp,not just
gateway mcphookdeck_inuser-facing strings
actionenum,outpost_-prefixed so bothservers can be configured in one client without name collisions
--allow-write(default off,HOOKDECK_MCP_ALLOW_WRITE). Write actions are omitted from theschema entirely in read-only mode, with a handler-side guard behind it. Accept a bare
--read-onlyas an explicit no-op alias; if both are passed, read-only winstenant tokenmints a 24-hour tenant JWTand
portalreturns a URL granting portal access. Both are GETs, so a naive read/write splitwould leave them available in read-only mode
ReadOnlyHint/DestructiveHinttool annotations (the gateway server sets none today).Per the MCP spec these are client UX hints and explicitly not a security boundary — the flag
does the enforcing
projects use: it currently switches to any project id without checkingthe type, so an agent can move the session to a project the server cannot serve
Testing prerequisites — blocking, needs someone with access
testing
publishexisting acceptance slices all authenticate against Gateway projects, so a new matrix entry and
secret are required. Keep it separate from the development project: these tests create and
delete tenants and destinations, and
config setcoverage mutates project-wide settingsOn write tools in MCP
#228 sets out the split — skills + CLI for
building, MCP for investigation — and #340
restates that
hookdeck gateway mcpis deliberately read-only.hookdeck outpost mcptakes a different approach deliberately: it ships write tools, gated behind anexplicit
--allow-writeopt-in and off by default. The read-only default preserves the safeout-of-the-box behaviour, while making the full surface available to operators who choose it.
The reasoning behind #228 does not transfer cleanly to Outpost. That split assumed skills + CLI
already covered the build path — true for the Event Gateway, which has published agent skills and full
CRUD commands, and not yet true for Outpost. More importantly, Outpost is operator-facing: managing
tenants and destinations is routine operational work, not just setup, and it is often done from
exactly the chat contexts #228 identifies as MCP's home ground.
The flag name follows the convention used by MCP servers that are read-only by default and opt in to
writes (the
awslabs/mcpservers use exactly--allow-write). The more common--read-onlyspellingis not used here because every server that uses it defaults to read-write, which is the wrong default
for this.
Prior art
output-contract and non-interactive work there applies to these commands too
--output jsonduplication; the new commands should not add more hand-rolled output branches than necessary
Sequencing
--allow-writePhases 1–2, 3a and 3b are three separate PRs; 3a lands on its own so the gateway refactor is reviewed
without new tools mixed in.