Skip to content

Spec source

A spec source is a machine-readable description of an API that Tinspec keeps reading. Endpoints are generated from it. In the app these are surfaced as API providers.

It started as OpenAPI-only. At v0.1.0-preview.10 it is source-kind-agnostic: a document you fetch, a gRPC server’s own reflection service, a GraphQL endpoint’s introspection, and an MCP server’s tool catalogue are all the same thing to Tinspec — something you point at, generate operations from, and re-resolve on an interval.

kindlocation points atWhat an “endpoint” is
openapi (the default)An OpenAPI document, by URL or file pathOne operation
grpcReflectionA grpc:// / grpcs:// target whose reflection service is queriedOne method
graphqlThe HTTP endpoint the introspection query is POSTed toOne root field
mcpAn MCP server’s streamable-HTTP endpoint, whose tools/list is calledOne tool

Absent kind means openapi, so every spec source written before the field existed keeps its exact meaning and serializes byte-identically.

A source kind names how the description is obtained, not how the resulting requests are sent. A gRPC service behind a transcoding gateway is still described by grpcReflection even though its requests go out as HTTP; and the mcp source kind (how a tool catalogue is described) is a different axis from the mcp protocol (how one tool call is sent).

Because every kind produces the same endpoint shape, the sidebar, the docs pane, overlays, the seen baseline, Spec Changes and the agent’s tools all work for a gRPC service and a GraphQL endpoint without learning anything new.

Importing a spec copies it once. Binding keeps reading it.

That is the whole difference from every tool that treats reflection or introspection as a one-shot import: you press a button, it writes a .proto or an SDL file, and that file rots.

pollIntervalSecs is what turns a source into a binding. It is in seconds, and absent (or 0) means never poll — an explicit default, so nothing starts making background requests because a field was missing. With it set, each resolve produces the current endpoint set, so:

  • an operation added to the description appears;
  • one removed from it disappears;
  • one whose parameters, body, or responses changed updates in place.

You are never testing against last quarter’s snapshot. When the service is unreachable, the last successfully fetched document is served from the local cache, so the endpoints stay usable offline.

A poll is an ordinary authenticated call: a reflection RPC rides the same channel, TLS material and injected metadata as the RPC it describes, and an introspection query is an ordinary GraphQL send. So a service behind a bearer token is pollable with no special casing.

File-backed specs are watched, not just polled

Section titled “File-backed specs are watched, not just polled”

A large OpenAPI document is split — openapi.yaml plus schemas/user.yaml plus responses/errors.yaml — and an edit to any of them changes the API.

For a local file source, Tinspec walks the root document and every local $ref reachable from it, then stamps that set as mtime:size per file. A tick compares the stamp with the previous one; a difference means something under this spec changed on disk. It is deliberately a stat, not a content hash — a spec split across a hundred files should cost a hundred stats per tick, not a hundred reads.

Remote $refs are not watched. They are a real part of the spec, but nothing on this machine can watch an HTTP URL, so they are left to the ordinary poll interval. A ref that cannot be read at all is reported rather than dropped.

A gRPC request says where its descriptor comes from, independently of whether there is a spec source at all: descriptorSource is proto (a .proto file at protoPath), descriptorSet (a compiled FileDescriptorSet — a protoset from protoc --descriptor_set_out or buf build), or reflection (the running server’s own grpc.reflection.v1 service, with a v1alpha fallback).

Absent still means proto, so an existing .proto-backed request writes no new key and stays byte-identical in your git repo.

Saving a reflection poll for offline use writes a protoset — a byte copy of what the poll returned, not a lossy export back into .proto source.

Two limits at this release: a .proto-backed provider gets no schema diff at all, and a gRPC reflection poll costs two round trips.

For a GraphQL source the unit is one root field — query.user, not the User type. That is what a caller actually calls; a type is not callable, and every root field POSTs to the same URL. Grouping follows the root operation, so the list reads Query / Mutation / Subscription with their fields under them. The __-prefixed meta-types are skipped.

The introspected schema also drives a type explorer and query completion in the editor. Nothing there changes what a send carries — introspection is a side channel that changes what the editor can offer, never the {query, variables, operationName} the engine posts.

Completion is a brace-and-paren scanner, not a GraphQL parser, and it is honest about it: field names at any depth, argument names, input types after $name:, aliases, inline fragments and fragment definitions all resolve. Fragment spreads, a union’s selection set, argument and enum values, variable names, and anything past a syntax error all return nothing, deliberately — an empty popup is the signal that the cursor is somewhere the scanner cannot reason about, and a confidently wrong completion costs more.

A live binding that silently rewrote things under you would be worse than a snapshot. So every difference lands in the Spec Changes view, which keeps a per-endpoint snapshot of what it last showed you and diffs against it field by field.

A modified endpoint expands to show exactly what moved. A removed endpoint keeps its last-known shape so you can still read what it used to be.

Acknowledging is always an explicit act — a per-row tick, Mark seen for one provider, or Mark all seen. Opening an endpoint to inspect it does not acknowledge anything.

From the same view you can tick endpoints and copy a minimal, valid OpenAPI subset of just those operations — with path-level parameters, every transitively referenced schema, and the security schemes, and nothing else. Useful for a ticket, a code review, or handing an agent exactly the part of the API under discussion. Removed endpoints can be reconstructed into the subset from their last-seen snapshot.

Differences between two polls are classified by the engine, so a change set separates the breaking ones from the additive ones instead of presenting a flat list.

The classifier has one thing it cannot say, and it is worth knowing: “DELETE /legacy was removed and two of your chains still call it.” Usage is resolved against live endpoints, so an operation that has gone is an operation nothing can be matched to. What the same machinery does answer is the deprecation case — which deprecated endpoints your saved requests and chains still point at — because those are still in the spec.

An operation marked deprecated gets a badge, and so does one marked x-internal.

Internal endpoints are hidden by default, behind Settings → Endpoints → Show internal endpoints. Every surface that hides them reports how many (N internal hidden), so a count never silently lies about what you are looking at.

An OpenAPI document usually declares several servers. Tinspec builds endpoint URLs from the first, so the rest were invisible; a provider action now turns them into project environments, skipping any name that already exists rather than overwriting it.

Read the caveat, because it depends on the document: a spec with no servers[] yields {{baseUrl}}/path endpoint URLs, so the environment really does decide where a request goes. A spec with servers[] yields absolute URLs built from servers[0], in which {{baseUrl}} appears nowhere — so creating these environments does not retarget the generated endpoints. It supplies {{baseUrl}} to requests you author or fork, to chains, and to the gateway base-URL field. The dialog says so.

Reconciling generated endpoints with your edits

Section titled “Reconciling generated endpoints with your edits”

The tension in a live binding: the endpoint is regenerated every poll, but you tweaked it.

Tinspec resolves this with overlays. When you edit a generated endpoint, it diffs your version against a pristine clone and records only the field groups that differ, as committable YAML in .tinspec/overlays/<provider-id>.yaml. Every fresh resolve re-applies them.

There are nine groups: body, query, headers, auth, grpc, graphql, prerunChain, routeMatcher, and settings.

Rules worth knowing:

  • An overlaid group replaces the generated one wholesale — it is not a field-level merge.
  • A group written as null records that you deleted the generated value, as distinct from absent, which means untouched. Without that distinction a reset did not survive the next poll.
  • URL and method are not overlayable. They form the endpoint’s identity, which is also the key for overlays, the change baseline, and tab state.
  • When a poll changes an endpoint underneath an existing edit, the editor shows an amber banner offering Keep edits or Reset to spec. It will not choose for you.

An edited endpoint carries an Edited chip in the editor and a dot in the navigator, and offers Reset to spec at any time.

The overlay file is also where a saved example lives — a response you kept under a name on the endpoint it came from. It is deliberately not a request group, so saving an example never changes what the endpoint sends.

The overlay file on disk is a bare endpointId → overlay map with no formatVersion key at this release, and so is a provider file under .tinspec/providers/.

A bound spec can also serve — an HTTP mock built from the same resolved spec the endpoint list, the drift banner and the version timeline come from, re-read on every request. There is no generated artifact to regenerate and no import step to forget, so the mock is only ever as old as the last poll, and Spec Changes is literally the changelog of what changed in the mock.

Three things follow from being bound rather than generated: responses are the spec’s own examples (or the same synthesized ones the docs pane shows); inbound requests are validated against the declared shape, so a caller’s missing required parameter fails instead of getting a cheerful 200; and an injected fault can only be one of the operation’s own declared errors.

Bodies can be seeded from your real 2xx sends, secret-scrubbed, so the mock returns real ids and real nesting.

The limits are worth stating plainly: HTTP only — no gRPC and no GraphQL mocks; no stored state — a POST does not make the next GET return the created row; no TLS (it binds loopback); options persist but nothing auto-starts; and mocks stop when you switch projects.

A spec source in a collection is a name, a location, an optional kind and poll interval, and an optional route matcher binding:

specSources:
- name: Users API
location: http://localhost:8080/openapi.json
pollIntervalSecs: 60
routeMatcher:
matcher: edge-kong
upstream: users-service
gatewayUrl: "{{gatewayUrl}}"
- name: Billing gRPC
kind: grpcReflection
location: grpc://localhost:50051
pollIntervalSecs: 30

In a project with a folder, API providers are also written one YAML file per provider under .tinspec/providers/.

A provider’s spec source URL is read-only after creation at v0.1.0-preview.10 — the settings view shows it with a copy button and no edit field.