# How to bootstrap an integration from a catalog SDK with an agent

> Install a generated SDK from the voxgig-sdk catalog, point the agent at its entity methods, and keep the glue off raw calls with a test, a gate, and one live check.

Source: https://voxgig.com/howto/bootstrap-an-integration-from-a-catalog-sdk

- Audience: api-consumer
- Level: intermediate
- Languages: go
- Verified: 2026-10-21
- Published: 2026-10-19

## Short answer

Pin the voxgig-sdk catalog's Go module for the API in `go.mod`, and tell the agent in AGENTS.md to read the SDK's README and call entities, never `Direct`. Build the client with `allow.op` set to `load,list`, so a raw call returns `ok: false`, and add a source check that fails `go test` on the first `Direct`. Test the glue offline against the SDK's seeded mock, then make one live call, because the mock cannot see the upstream change.

---
## You will need

Go 1.24 or later, a coding agent that reads AGENTS.md, and an API the catalog covers. Verified
2026-10-21 against Go 1.24.7 and `github.com/voxgig-sdk/open-meteo-sdk/go` v0.0.7, resolved
from the Go module proxy. The service wants the current temperature at a coordinate and
nothing else. The SDK is the catalog's
[Open-Meteo client](https://voxgig.com/voxgig-sdk/open-meteo-sdk), generated from the
[forecast API's OpenAPI document](https://github.com/open-meteo/open-meteo/blob/main/openapi/forecast.yml).
Open-Meteo's [documentation](https://open-meteo.com/en/docs) asks for a key only for
commercial use, so the live call at the end needs no secret.

Voxgig maintains the SDK catalog. This page compares it with Open-Meteo's official SDK and
with a client the agent writes from the OpenAPI document.

## Approaches compared

| Approach | When it fits | What it costs you | When to pick something else |
| --- | --- | --- | --- |
| [A client the agent writes from the OpenAPI document](https://github.com/open-meteo/open-meteo/blob/main/openapi/forecast.yml) | Three endpoints, a house style no SDK matches, and a document you trust | Code only the agent has read, whose retries, errors, and types exist only if the prompt asked for them | The surface is large, or you want regeneration when the API moves |
| [Open-Meteo's official SDK](https://github.com/open-meteo/sdk) | You want the vendor's own types and its FlatBuffers wire format for large hourly arrays | Schema files with no HTTP calls in them, so the fetch, the retries, and the offline mode are yours to write | You want a client that arrives with a transport and a test mode |
| [voxgig-sdk catalog SDK](https://voxgig.com/voxgig-sdk/open-meteo-sdk) | A typed entity client with an offline mock, for an API whose vendor ships none in your language | An unofficial build that tracks upstream on its own schedule, and packaging that lags: of this SDK's six languages, the Go module alone resolves from a registry | The vendor's SDK exists in your language and is maintained |

The official SDK and the catalog SDK answer different questions. Open-Meteo's repository holds
compiled FlatBuffers schemas and says the files contain no code to perform HTTP calls, so the
transport is yours to write and the vendor's types are the gain.[^1] The catalog client arrives
with a transport, entities, and a mock, at the cost of a lag. Its README marks the TypeScript
and Python packages publish pending, and the Go module is the one that resolves. The
agent-written client fits your code, but only the agent has read it.

## Pin the module and tell the agent where the entities are

The catalog README's install line says `@latest`. Pin a version instead, so the agent, the
tests, and CI read the same generated code, and an upstream regeneration arrives as a diff in
`go.mod`:

```text title="go.mod"
module example.com/weatherdesk

go 1.24

require github.com/voxgig-sdk/open-meteo-sdk/go v0.0.7
```

Then tell the agent what to read and what to leave alone. The module ships its own
`README.md` and `REFERENCE.md`, with an entity table that lists `WeatherForecast`,
`MarineForecast`, and `Historical`, each with one operation, `load`. The agent finds them with
[`go list -m`](https://go.dev/ref/mod), which prints the module's directory in the cache.
The instruction file names that, names the entity call, and bans the two raw methods:

```md title="AGENTS.md"
## Weather data

- The Open-Meteo client is `github.com/voxgig-sdk/open-meteo-sdk/go`, pinned in `go.mod`. It is
  generated and unofficial. Before writing a call, read its `README.md` and `REFERENCE.md` from
  the module cache: `go list -m -f '{{.Dir}}' github.com/voxgig-sdk/open-meteo-sdk/go`.
- Call the API through its entities, `client.WeatherForecast(nil).Load(match, nil)`, then
  `DataTyped()` for the typed record. The entity table in the README lists what exists; if the
  endpoint you need is not in it, stop and say so instead of working around it.
- Do not call `Direct` or `Prepare`. The client in `weather/` is built with `allow.op` set to
  `load,list`, so a raw call returns `ok: false`, and `go test ./...` fails on the first raw call
  it finds under `weather/`.
```

An instruction on its own is a suggestion the agent weighs against the example it half
remembers, so the next step makes the rule hold without it.

## Close the raw-call door in the client, not in the prompt

Every client the catalog generates gates its raw methods on an option called `allow.op`. The
default, read from the SDK's source, is a list of eight operations that includes `direct` and
`graphql`.[^2] The glue builds its client with the two entity operations it needs and nothing
else, so a `Direct` call returns `ok: false` with the reason. The same options go into the
offline client the tests use:

```go title="weather/weather.go"
func options() map[string]any {
	return map[string]any{"allow": map[string]any{"op": "load,list"}}
}

// New returns a live client that can reach the entities and nothing below them.
func New() *sdk.OpenMeteoSDK {
	return sdk.NewOpenMeteoSDK(options())
}

// NewOffline returns a client on the SDK's in-memory transport, seeded with the given records
// under the entity name the model uses. Each key becomes the record's id.
func NewOffline(forecasts map[string]any) *sdk.OpenMeteoSDK {
	return sdk.TestSDK(map[string]any{"entity": map[string]any{"weather_forecast": forecasts}}, options())
}
```

A runtime refusal catches the call when it runs; a source check catches it when the agent
writes it. `rawcheck` parses the package and the directories below it with
[`go/ast`](https://pkg.go.dev/go/ast), and reports every call to a method named `Direct` or
`Prepare`, and every such method taken as a value to call later:

```go title="rawcheck/rawcheck.go"
	ast.Inspect(file, func(n ast.Node) bool {
		if call, ok := n.(*ast.CallExpr); ok {
			if sel, ok := call.Fun.(*ast.SelectorExpr); ok {
				called[sel] = true
			}
			return true
		}
		sel, ok := n.(*ast.SelectorExpr)
		if !ok || !raw[sel.Sel.Name] {
			return true
		}
```

Run it over the glue and over the agent's first draft, kept under `pitfall/` as the shape to
refuse:

```bash
go run ./cmd/rawcheck ./weather ./pitfall
```

```text output
./weather: ok, every call goes through an entity
pitfall/pitfall.go:13: client.Direct(...) bypasses the entity model; use client.WeatherForecast(nil).Load
```

The command exits non-zero on a finding, so it belongs in CI beside `go test`. The message
names the entity call to use, because an agent reads a failing check as its next prompt.

## Run the loop offline, then make one live call

The glue itself is one entity call and one typed read. `Load` takes the match the API expects
as query parameters, and `DataTyped` hands back the generated `WeatherForecast` record, whose
`Current` field the compiler knows about:

```go title="weather/weather.go"
func CurrentTemperature(client *sdk.OpenMeteoSDK, lat, lon float64) (float64, error) {
	match := map[string]any{"latitude": lat, "longitude": lon, "current": "temperature_2m"}
	ent, err := client.WeatherForecast(nil).Load(match, nil)
	if err != nil {
		return 0, fmt.Errorf("weather forecast: %w", err)
	}
	forecast, ok := ent.(*entity.WeatherForecastEntity)
	if !ok {
		return 0, fmt.Errorf("weather forecast: unexpected result %T", ent)
	}
```

The demo runs the glue and the agent's draft against the same seeded offline client, then
runs the draft once more on a client with the default `allow.op`. Last, it captures the
request the glue's entity call sends, through the SDK's `system.fetch` option, without sending
it:

```bash
go run ./cmd/demo
```

```text output
offline, seeded with one forecast for Dublin
  weather.CurrentTemperature   12.4 degrees C, err=<nil>
  pitfall.CurrentTemperature   err=forecast: status <nil>, OpenMeteoSDK: direct: operation not allowed by SDK option allow.op value: "load,list"
the same draft on a client with the default allow.op
  pitfall.CurrentTemperature   err=forecast: status 404, <nil>
the request the entity call sends, captured and not sent
  GET https://api.open-meteo.com/v1/forecast?current=temperature_2m&latitude=53.35&longitude=-6.26
```

Three lines carry the argument. The glue reads `12.4` from the seeded record, so the test
loop has something to assert on. The closed client refuses the draft with a message that
names the option. On an open client the draft gets a `404` instead, because the SDK's mock
serves entity operations only: code that bypasses the entities also bypasses the SDK's one
offline test mode.

The offline loop proves the glue against the model, not the model against the API, so the
final step is one call that leaves the machine:

```bash
go run ./cmd/live
```

It prints the current temperature in Dublin, and a change upstream shows up here first. Run
it once per change and paste the line into the pull request; the test loop stays offline.

## Check it worked

Six tests cover the offline read, a record with no `current` block, the refusal, the request
the entity call sends, the source check's reach, and the absence of raw calls in the package.
The mock answers with the seeded record whatever the coordinates, so only the request test
fails when the glue swaps latitude and longitude.

```bash
go test -v ./weather | grep -E '^(=== RUN|PASS$|FAIL$)'
```

```text output
=== RUN   TestCurrentTemperatureOffline
=== RUN   TestMissingCurrentBlockIsAnError
=== RUN   TestDirectIsRefusedByThisClient
=== RUN   TestTheEntityCallSendsTheCoordinates
=== RUN   TestRawCheckReachesSubdirectoriesAndMethodValues
=== RUN   TestNoRawCallsInThePackage
PASS
```

`TestNoRawCallsInThePackage` is the one the agent runs into. Move one `Direct` call into
`weather/` and the run ends in `FAIL` with the file and line, before any request is made. The
`grep` keeps the page free of timings, and a pipeline exits with the status of its last
command, so CI runs `go test ./...` on its own.

## When it goes wrong

The agent reports that the SDK is broken. Its draft used `Direct`, the client refused it with
`operation not allowed by SDK option allow.op value: "load,list"`, and the agent read the
refusal as a defect. The message is the gate working. Feed it back with the entity call from
AGENTS.md, and the next draft goes through `WeatherForecast(nil).Load`.

The live call answers `429`, times out, or never returns. Open-Meteo's free tier limits
requests, a proxy can stall a TLS handshake, and the SDK fetches through Go's
`http.DefaultClient`, which sets no overall timeout. That is why the live call is one command
run by hand: a loop that retries it is a loop that gets rate limited.

The upstream API moved and the catalog SDK did not. The catalog regenerates on its own
schedule, so a client can keep targeting a path the vendor has retired while every offline
test passes, because the mock never sees the vendor.[^3] The live call is the only step on
this page that notices.

## When not to do this

Do not adopt a catalog SDK for an API whose vendor ships a maintained client in your language.
The catalog's own README calls each package unofficial and says it is not affiliated with the
provider, and that sentence is the whole of its support commitment.

Do not point the agent at the TypeScript or Python package of this SDK. The README marks both
publish pending, and an agent that guesses the package name gets a 404 from the registry and
invents a client instead.

Do not close `allow.op` in a client handed to code that needs an endpoint the entity model
lacks. The README's own answer for that case is `Direct`, and a gate across the only route to
a needed endpoint produces a workaround rather than a fix. Decide per repository: a closed
client for glue that stays inside the model, an open one where the model stops short, and
the source check either way.

Do not treat the offline mock as a contract test. It replays the records you seeded, in the
shape the generated model declares, and knows nothing of what the vendor returns this week.

## Related how-tos

- [Install an API skill and check a coding agent uses it](/howto/install-an-api-skill-for-a-coding-agent)

- [Stop a coding agent inventing endpoints not in the spec](/howto/stop-a-coding-agent-inventing-endpoints)

- [Judge whether an unofficial SDK is safe to depend on](/howto/judge-an-unofficial-sdk-before-adoption)

- [Let a coding agent probe a live API through an MCP server](/howto/probe-live-api-responses-through-an-mcp-server)

- [Let a coding agent customize a generated SDK safely](/howto/customise-a-generated-sdk-with-an-agent-safely)

## Last verified

Verified 2026-10-21 against Go 1.24.7 and `github.com/voxgig-sdk/open-meteo-sdk/go` v0.0.7,
fetched from the Go module proxy. Every output block is what the command preceding it printed,
and every command except the live one runs without a network. `go run ./cmd/live` was run by
hand three times on 2026-10-19: one TLS handshake timeout from the session's proxy, one 429
from the API, then a reading of 11.1 degrees. On the verification day it printed 9.3 degrees.
The `allow.op` default and the matching gate in the TypeScript and Python clients were read
from the SDK's source, not run. The catalog's REST Countries client was run once against its upstream, which
answered every call with a deprecation notice for the path it targets. No agent was run.

[^1]: The [official repository](https://github.com/open-meteo/sdk) holds compiled FlatBuffers
schemas for five languages and says the biggest advantage of FlatBuffers is that large floating
point arrays can be encoded directly. A request gains the format by appending
`format=flatbuffers`. An SDK that consists of a schema is an SDK in the first sense of the
word: a kit, with the assembly left to the developer.

[^2]: The default is a string of eight comma-separated tokens, `create,update,load,list,remove,command,direct,graphql`,
set in the SDK's `utility/make_options.go`, and a raw method checks whether its own name is a
substring of it. The Python client makes the same check in `_op_allowed`, and the TypeScript
client with `allow.op.includes('direct')`. The generator emits the same gate in each of its
languages, open by default. The [catalog page](https://voxgig.com/voxgig-sdk/open-meteo-sdk)
does not mention the option, which is one way to keep a default.

[^3]: The catalog's REST Countries client is generated against `/v3.1`. The
[upstream](https://restcountries.com/docs/countries/legacy-api-deprecation) answers every
request to that path with a notice that the version has been deprecated, and a link to its
successor. The client's offline mode is untroubled by this. The mock has never heard of the
vendor, and in that respect it is a faithful model of the code it tests.