Add a command
Model a real airbnb record and expose it as a command, a route, and a tool at once.
An airbnb operation is declared once and shows up everywhere: as a CLI
subcommand, as an HTTP route under serve, as an MCP tool, and as an
airbnb:// URI a host can dereference. You add one by touching three files, and
every surface updates itself. The room command is the worked example below.
1. Model the record
In airbnb/types.go, a struct describes the thing you fetch. The kit and
table struct tags decide how a host addresses it and how it prints:
type Room struct {
ID string `json:"id" kit:"id"` // the room id, the URI id
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty" kit:"body"` // what cat and Markdown print
PropertyType string `json:"property_type,omitempty"`
Price float64 `json:"price,omitempty"` // nightly, when dates are given
Currency string `json:"currency,omitempty"`
Rating float64 `json:"rating,omitempty"`
HostID string `json:"host_id,omitempty" kit:"link,kind=airbnb/host"` // an edge to the host
URL string `json:"url"`
}
kit:"id"marks the field that becomes the URI id.kit:"body"marks the prose thatcatand the Markdown export render.kit:"link,kind=<scheme>/<type>"marks an outbound edge.Room.host_idpoints at the host, which is what lets a host program walk the graph.json:",omitempty"keeps a record honest: a field Airbnb did not serve is absent rather than zero.
2. Fetch it
In airbnb/room.go, a client method returns the record. The client is a single
web and GraphQL client; there is no API backend to fall back to, so an error
flows straight up:
func (c *Client) GetRoom(ctx context.Context, ref string) (*Room, error) {
id := roomID(ref) // accept a bare id or a /rooms/ URL
body, err := c.get(ctx, c.BaseURL+"/rooms/"+id)
if err != nil {
return nil, err // ErrBlocked, ErrRateLimited, ErrNotFound flow up unchanged
}
// read the data-deferred-state-0 JSON island, parse it into a Room ...
return r, nil
}
The static detail comes from the page island. A date-specific nightly price is
filled from the GraphQL sections only when --checkin and --checkout are
given.
3. Declare the operation
In airbnb/ops.go, add an input struct and a handler. The struct tags tell
kit what is a positional argument and where the client is injected:
type roomRef struct {
ID string `kit:"arg" help:"room id or /rooms/ URL"`
Client *Client `kit:"inject"`
}
func getRoom(ctx context.Context, in roomRef, emit func(*Room) error) error {
r, err := in.Client.GetRoom(ctx, in.ID)
if err != nil {
return mapErr(err)
}
return emit(r)
}
Then register it in Register in airbnb/domain.go:
kit.Handle(app, kit.OpMeta{
Name: "room", Group: "read", Single: true,
Summary: "Show one listing by id",
URIType: "room", Resolver: true,
Args: []kit.Arg{{Name: "id", Help: "room id or /rooms/ URL"}},
}, getRoom)
That is the whole change. kit.Handle reflects the input for flags and the
output for the record shape, so the operation immediately becomes:
airbnb room 12345 # the command
curl 'localhost:7777/v1/room/12345' # the route, under serve
ant get airbnb://room/12345 # the URI dereference, via a host
Resolver ops and list ops
Two flags shape how a host treats an operation:
Single: truewithResolver: truemarks the canonical one-record fetch for aURIType. It answersant get.roomandhost showare the resolvers.List: truemarks a member-lister for a parent resource. It answersant ls. A list op emits records that are themselves addressable, so every member is a URI a host can follow.search,reviews,calendar,experiences, andhost listingsdo this, each tagged with its own collection authority so they never shadow one another.
Map errors to exit codes
Return through mapErr so every surface reports the same outcome with the same
exit code: the edge wall reads as need-auth (exit 4), a throttle as rate-limited
(exit 5), a missing listing as not-found (exit 6):
case errors.Is(err, ErrNotFound):
return errs.NotFound("%s", err.Error())
case errors.Is(err, ErrRateLimited):
return errs.RateLimited("%s", err.Error())
case errors.Is(err, ErrBlocked):
return errs.NeedAuth("%s", err.Error())
See output formats for how records render, and resource URIs for how a host addresses them.