catalog

package
v0.7.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 25, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package catalog holds a binding's view of what an MCP server offers, as a sequence of immutable generations.

A Generation is the unit of catalog identity: it is built once, from data a server supplied, and never changes afterwards. Everything a caller reads back is a copy, so a consumer can neither observe a torn value nor rewrite one another consumer is relying on. That immutability is what lets several readers hold different generations at once — one loop still serving a turn against generation 4 while another has already adopted generation 5 — without any of them locking.

The package is deliberately ignorant of two things. It does not fetch (discover.go drives a connection, but the connection is injected), and it does not decide what a *model* may see: a Generation records what the server offered, in full. Client-side policy — a ToolFilter, a permission rule — shapes the projection above this package, never the record inside it. Keeping server truth and host policy in separate layers is what makes a generation comparable across configurations: the same server yields the same digest whatever the host allows.

Everything here originates from an untrusted peer. Bounds are enforced before retention, names are validated before they become identifiers, and nothing panics on malformed input.

Index

Constants

View Source
const (
	// WhatCatalogPages is reported when a family needs more pages than the
	// binding allows.
	WhatCatalogPages = "catalog_pages"
	// WhatCatalogItems is reported when a family carries more items than the
	// binding allows.
	WhatCatalogItems = "catalog_items"
)

Values for limits.OverLimitError.What reported by discovery.

View Source
const MaxModelNameBytes = 64

MaxModelNameBytes bounds a model-facing tool name.

64 is the tightest constraint in common use across inference providers, so a name that fits it fits everywhere. The design requires names to "remain within inference-provider limits"; picking the minimum is what makes one catalog usable from any provider rather than only the one it was discovered under.

View Source
const MaxRawNameBytes = 512

MaxRawNameBytes bounds a server-supplied identifier: a tool or prompt name, a resource URI, a URI template.

It is generous because it covers URIs as well as names, and its job is not to enforce taste — it is to stop a server from making an identifier into a memory or log-volume problem. An identifier is retained for the life of a generation, indexed in a map, rendered into names, warnings and telemetry, and echoed back on the wire.

View Source
const MaxWarnings = 64

MaxWarnings caps the warnings one generation retains, so that a server which produces a tolerated defect per item cannot turn diagnostics into unbounded memory.

The text of a warning past the cap is dropped; the fact of it is not. The last slot is spent on a summary naming how many were raised in all, because a cap that simply stopped appending would make a server with 64 tolerated defects indistinguishable from one with 6400 — and a reader would take the 64 messages for the whole story. The cap bounds how much is said, never whether it is said.

View Source
const ShortDigestBytes = 4

ShortDigestBytes is how much of a digest Short renders. Four bytes (eight hex characters) is a disambiguation suffix, not a security boundary: it only has to separate a handful of tool names that already collide within one server, where the alternative is an arbitrary winner.

Variables

This section is empty.

Functions

This section is empty.

Types

type Builder

type Builder struct {
	// Binding is the configured name of the server binding this catalog
	// belongs to. It qualifies every model-facing tool name, so it is part of
	// the catalog's identity rather than ambient context.
	Binding string
	// Number is the generation's ordinal within the binding. It orders
	// generations; it deliberately does not participate in the digest (see
	// Digest).
	Number uint64

	ProtocolVersion protocol.ProtocolVersion
	Capabilities    protocol.ServerCapabilities
	Server          protocol.ServerIdentity
	// Instructions is the server's usage hint, already bounded by the
	// conversion that produced it.
	Instructions string

	Tools             []protocol.ToolSpec
	Prompts           []protocol.PromptSpec
	Resources         []protocol.ResourceSpec
	ResourceTemplates []protocol.ResourceTemplateSpec

	// Warnings records defects tolerated during discovery.
	Warnings []string
	// DroppedWarnings is how many warnings the caller raised but could not keep
	// before Build was reached — discovery bounds each family as it fetches it.
	// It is carried so that the summary Build writes counts every warning that
	// was ever raised, not merely those that survived to here.
	DroppedWarnings int
	// Decisions records what discovery did about each family.
	Decisions []Decision
	// Tolerances is the compatibility policy Build applies. The zero value
	// tolerates nothing: a defect that is not explicitly tolerated makes the
	// generation invalid.
	Tolerances Tolerances
}

Builder accumulates the parts of a Generation. It is an ordinary mutable value: a caller fills it in, calls Build, and the result is immutable. A Builder may be discarded, reused or mutated afterwards without affecting a Generation it already produced — Build copies everything it keeps.

func (Builder) Build

func (b Builder) Build() (*Generation, error)

Build validates the accumulated parts and returns the immutable Generation.

It is where a generation's derived identity is settled: raw names are validated, the collections are put in a canonical order, model-facing names are constructed for the tool set as a whole (which is why naming happens here and not per item — collision resolution needs to see every sibling), schemas are digested, and the catalog digest is computed over the result.

Build fails closed. A duplicate raw name, an unusable name, or an empty binding is a defect that makes the catalog ambiguous rather than merely smaller, so it rejects the whole generation instead of publishing a partial one.

type Config

type Config struct {
	// Binding is the configured name of the binding being discovered.
	Binding string
	// Number is the generation number to assign.
	Number uint64
	// Handshake is what initialize settled. Capabilities gates which families
	// are fetched at all.
	Handshake protocol.InitializeResult
	// Limits bounds the fetch.
	Limits Limits
	// Tolerances is the compatibility policy the generation is built under. The
	// zero value tolerates nothing.
	Tolerances Tolerances
}

Config is everything discovery needs that does not come from the connection. The handshake fields are supplied by the caller because the caller performed the handshake; discovery must not repeat it.

type Decision

type Decision struct {
	Family Family
	Action DecisionAction
}

Decision records what discovery did about one family. It is a compatibility decision in the design's sense: the record of a choice the client made against what the server advertised, kept so that "the server has no prompts" and "we never asked" are distinguishable after the fact.

type DecisionAction

type DecisionAction uint8

DecisionAction is what discovery did about a family, and why it is a closed enum: a compatibility decision is a fact the binding reports, not free text.

const (
	// ActionFetched means the family was advertised and fetched.
	ActionFetched DecisionAction = iota + 1
	// ActionSkippedNotAdvertised means the server did not advertise the
	// capability, so the method was never called. This is the compatibility
	// rule in force, not an error: a client that calls an unadvertised method
	// is guessing.
	ActionSkippedNotAdvertised
)

The decisions discovery can reach for a family.

func (DecisionAction) String

func (a DecisionAction) String() string

String returns the action's stable identifier, or "unknown".

type DefectError

type DefectError struct {
	// Family is the catalog family the defect was found in. It is the zero
	// value for a defect that belongs to no family, such as an empty binding.
	Family Family
	// Reason is a bounded description of the defect. It may quote a
	// server-supplied identifier, which validateRawName has already checked for
	// control characters, so it is safe to render.
	Reason string
}

DefectError reports catalog data that cannot be made into a usable generation: a name that is not an identifier, an ambiguous duplicate, a server that broke a protocol invariant.

It is distinct from a limits.OverLimitError, which reports a catalog that is well-formed but larger than this binding will accept. The difference matters to a caller: an over-limit catalog may become acceptable with a raised bound, while a defective one is broken whatever the bounds are.

func (*DefectError) Error

func (e *DefectError) Error() string

Error renders "catalog: <family>: <reason>", omitting the family when there is none.

type Digest

type Digest [sha256.Size]byte

Digest is a SHA-256 digest. It is a value type, so it can be compared with ==, used as a map key, and copied out of an immutable Generation without a clone.

func DigestBytes

func DigestBytes(b []byte) Digest

DigestBytes returns the digest of a raw byte string, domain-separated from the catalog encoding so that a schema document can never be confused with a catalog that happens to contain the same bytes.

func (Digest) IsZero

func (d Digest) IsZero() bool

IsZero reports whether d is the zero digest, which this package uses to mean "absent" (see Tool.OutputSchemaDigest). It is not a digest any input can produce in practice: a preimage for the zero digest is not known.

func (Digest) Short

func (d Digest) Short() string

Short renders the first ShortDigestBytes bytes as hex. It is for a name suffix and a human-readable label — never for an equality check, which must always compare the whole digest.

func (Digest) String

func (d Digest) String() string

String renders the digest as lowercase hex.

type Family

type Family uint8

Family names one catalog family — one list method's worth of a server's offering. It is typed rather than a bare string because discovery, warnings and (later) change notifications all route on it.

const (
	// FamilyTools is tools/list.
	FamilyTools Family = iota + 1
	// FamilyPrompts is prompts/list.
	FamilyPrompts
	// FamilyResources is resources/list.
	FamilyResources
	// FamilyResourceTemplates is resources/templates/list.
	FamilyResourceTemplates
)

The catalog families this module fetches.

func (Family) String

func (f Family) String() string

String returns the family's stable identifier, or "unknown" for any value outside the declared range.

type Generation

type Generation struct {
	// contains filtered or unexported fields
}

Generation is one immutable snapshot of a server's catalog.

Every field is unexported and every accessor returns a copy: a Generation is shared by reference across goroutines with no lock, which is only sound because nothing can mutate it — including a caller who was handed a slice out of it.

Build is the only constructor. The zero value is not a usable Generation.

func Discover

func Discover(ctx context.Context, l Lister, cfg Config) (*Generation, error)

Discover fetches every advertised catalog family and builds a Generation.

Only families the server advertised are fetched. This is the design's compatibility rule — "checks server capabilities before using a method" — and it is not merely polite: a server that did not advertise prompts has not promised prompts/list exists, and calling it produces a method-not-found error that is indistinguishable from a real failure. Asking would turn a server with no prompts into a server that failed discovery. What was skipped, and why, is recorded as a Decision, so "this server has no prompts" and "we never asked" stay distinguishable afterwards.

Failures are all-or-nothing: any error returns no generation at all.

func (*Generation) AppliedTolerances

func (g *Generation) AppliedTolerances() []Tolerance

AppliedTolerances returns a copy of the compatibility tolerances this generation needed, in a deterministic order. It is empty for a server that implements the specification faithfully.

It is deliberately not part of the catalog digest, for three reasons that point the same way:

  • The digest answers "is this the same server offering?", and two hosts with different compatibility profiles looking at one server must agree on the answer — the same reason a ToolFilter is not in it (see the package doc).
  • The design's configuration manifest lists "capability, filter, limits, and compatibility policy digests" separately from "adopted catalog digest and tool schema digests". They are two identity components, not one: the policy's identity is the profile's own digest (client.Profile.Digest), and the catalog's is this one.
  • Including it could not distinguish anything anyway. What a tolerance changed is already digested through the value it changed — a normalized ModelName and an absent OutputSchema are both covered — so "same digest, different applied tolerances" describes two catalogs that are, tool for tool and schema for schema, the same catalog.

func (*Generation) Binding

func (g *Generation) Binding() string

Binding returns the binding name this catalog belongs to.

func (*Generation) Capabilities

func (g *Generation) Capabilities() protocol.ServerCapabilities

Capabilities returns what the server advertised at initialize.

func (*Generation) Decisions

func (g *Generation) Decisions() []Decision

Decisions returns a copy of the compatibility decisions discovery made.

func (*Generation) Digest

func (g *Generation) Digest() Digest

Digest returns the canonical catalog digest. See computeDigest for what it covers.

func (*Generation) Instructions

func (g *Generation) Instructions() string

Instructions returns the server's bounded usage hint.

func (*Generation) Number

func (g *Generation) Number() uint64

Number returns the generation's ordinal within its binding.

func (*Generation) Prompts

func (g *Generation) Prompts() []protocol.PromptSpec

Prompts returns a deep copy of the prompt set, in canonical order.

func (*Generation) ProtocolVersion

func (g *Generation) ProtocolVersion() protocol.ProtocolVersion

ProtocolVersion returns the negotiated protocol version.

func (*Generation) ResourceTemplates

func (g *Generation) ResourceTemplates() []protocol.ResourceTemplateSpec

ResourceTemplates returns a copy of the template set, in canonical order.

func (*Generation) Resources

func (g *Generation) Resources() []protocol.ResourceSpec

Resources returns a copy of the resource set, in canonical (URI) order.

func (*Generation) Server

func (g *Generation) Server() protocol.ServerIdentity

Server returns the raw server identity, exactly as the server claimed it.

func (*Generation) ToolByModelName

func (g *Generation) ToolByModelName(modelName string) (Tool, bool)

ToolByModelName returns the tool a model knows as modelName. This is the reverse mapping that makes routing possible without ever parsing a display name back into its parts.

func (*Generation) ToolByRawName

func (g *Generation) ToolByRawName(rawName string) (Tool, bool)

ToolByRawName returns the tool the server calls rawName.

func (*Generation) ToolCount

func (g *Generation) ToolCount() int

ToolCount reports how many tools the generation holds, without copying them.

func (*Generation) Tools

func (g *Generation) Tools() []Tool

Tools returns a deep copy of the tool set, in canonical (raw-name) order.

func (*Generation) Warnings

func (g *Generation) Warnings() []string

Warnings returns a copy of the defects tolerated during discovery.

It holds at most MaxWarnings entries. When more were raised than that, the last entry is a summary naming how many there were in all, so a list that stops short is never mistaken for the whole story.

type Limits

type Limits struct {
	// MaxPages caps the list round trips one family may take.
	MaxPages int
	// MaxTools caps the tools accepted from one server.
	MaxTools int
	// MaxPrompts caps the prompts accepted from one server.
	MaxPrompts int
	// MaxResources caps the resources accepted from one server. It is applied
	// to concrete resources and to templates separately: they are two lists,
	// and a server with many templates is not thereby allowed fewer resources.
	MaxResources int
}

Limits is the narrow view of the client's Limits that discovery enforces.

The item bounds are per family, not a shared budget: they answer "is this server's tool list reasonable", which is a different question per family and is configured separately. A non-positive bound is not "unbounded" — like every other bound in this module it fails closed, rejecting the first page — so callers pass a normalized value.

type Lister

type Lister interface {
	ListTools(ctx context.Context, cursor string) (protocol.ToolPage, error)
	ListPrompts(ctx context.Context, cursor string) (protocol.PromptPage, error)
	ListResources(ctx context.Context, cursor string) (protocol.ResourcePage, error)
	ListResourceTemplates(ctx context.Context, cursor string) (protocol.ResourceTemplatePage, error)
}

Lister is the connection surface discovery needs: the paginated list methods, and nothing else.

It is declared here, at the consumer, rather than taken as a protocol.Conn. Discovery cannot initialize, call a tool, or close a connection, and an interface that offered it those would be an interface it could misuse. It also makes the fetch sequence testable against a scripted server — page cycles and hostile cursors are trivial to produce through this and awkward to produce through a real one.

protocol.Conn satisfies it.

type Tolerance

type Tolerance uint8

Tolerance is a safe deviation this catalog applied to a server that does not implement the specification perfectly.

The set is closed and every member is one of the design's *safe* tolerances. The unsafe ones — replacing an invalid input schema with unconstrained arguments, treating malformed framing as valid, treating an auth failure as success — are not here, and their absence is the point: a tolerance that cannot be named cannot be applied, whatever a configuration asks for.

const (
	// ToleranceInvalidOutputSchema means a defective *optional* output schema
	// was dropped and its tool kept. The input schema — the one that constrains
	// what a model may send — is never tolerated this way: a tool with a
	// defective input schema is rejected, because keeping it would mean
	// widening its arguments.
	ToleranceInvalidOutputSchema Tolerance = iota + 1
	// ToleranceNormalizedDisplayName means a raw name that inference providers
	// will not accept was normalized into one they will. The raw name is
	// preserved and remains what goes on the wire (see identity.go).
	ToleranceNormalizedDisplayName
)

The tolerances this package can apply.

func (Tolerance) String

func (t Tolerance) String() string

String returns the tolerance's stable identifier, or "unknown".

type Tolerances

type Tolerances struct {
	// InvalidOutputSchema allows a tool whose optional output schema was
	// defective to be kept without it. When false, such a tool makes the whole
	// generation invalid.
	InvalidOutputSchema bool
	// NormalizeDisplayNames allows a raw name that is not provider-compatible
	// to be sanitized or truncated into one that is. When false, such a tool
	// makes the whole generation invalid.
	NormalizeDisplayNames bool
}

Tolerances is the compatibility policy in force for one build: which safe deviations this binding is willing to apply.

The zero value tolerates nothing, which is the strict reading of the specification and the fail-closed default for a package that cannot know what its caller configured. The client passes what its profile permits.

type Tool

type Tool struct {
	// RawName is the server's own name for the tool, exactly as sent.
	RawName string
	// ModelName is the sanitized, binding-qualified identity a model sees.
	ModelName string

	Title       string
	Description string

	// InputSchema is the tool's argument schema. It is always present: a tool
	// without one is rejected at conversion.
	InputSchema json.RawMessage
	// OutputSchema is the optional result schema, nil when the server sent none
	// or when a defective one was dropped (see Warnings).
	OutputSchema json.RawMessage

	// InputSchemaDigest is the digest of InputSchema's bytes.
	InputSchemaDigest Digest
	// OutputSchemaDigest is the digest of OutputSchema's bytes, zero when there
	// is no output schema.
	OutputSchemaDigest Digest

	// Annotations are the server's behavioural hints. They are untrusted policy
	// input and never authority. Nil when the server sent none.
	Annotations *protocol.ToolAnnotations
	// Warnings records defects tolerated when this tool was converted.
	Warnings []string
}

Tool is one tool in a generation, with the derived identity and digests the rest of the module routes on.

The two names are not interchangeable and neither is derived from the other at use time. RawName is what goes on the wire; ModelName is what a model sees. The mapping between them is this struct — a caller resolves a ModelName by looking it up here, never by parsing it (see identity.go).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL