ADR-004: Product Catalog Pattern for Agent Domain Modeling¶
Date: 2026-04-11 Status: Accepted Related: ADR-001 (technical mapping), ADR-003 (complementary pattern)
Context¶
AgentLens catalogs AI agents across multiple protocols (A2A, MCP, future protocols). Each protocol defines different capabilities: A2A agents have skills, security schemes, interfaces, extensions, and signatures; MCP agents have tools, resources, and prompts. The domain model must:
- Represent heterogeneous agents uniformly — a single catalog view over agents with fundamentally different capability sets.
- Extend to new protocols without schema changes — adding MCP prompts or a future protocol's capabilities must not require database migrations or changes to the API response format.
- Support structured querying — "find all agents with an
mcp.toolnamedsearch" or "which agents require OAuth 2.0" must be efficient queries, not full-table JSON scans. - Separate identity from presentation — the same agent type (protocol + endpoint + capabilities) may appear in multiple catalog views with different display names, categories, or validity windows.
A naive approach (one table per protocol, or capabilities as a JSON blob on the agent row) breaks under these requirements.
Decision¶
Model the agent catalog using the Product Archetype pattern from softwarearchetypes/product, translated to Go idioms.
Core separation¶
- AgentType (= Product Type) — what the agent IS: protocol, endpoint, version, capabilities. Identity is
AgentKey = SHA256(protocol + endpoint). - CatalogEntry (= Catalog Entry) — how the agent is offered: display name, description, categories, lifecycle state, health, validity window. 1:1 FK to AgentType.
- Capability (= Product Feature) — polymorphic interface with
Kind()discriminator. Each protocol defines its own capability kinds. All stored in a singlecapabilitiestable with promotedname/descriptioncolumns +propertiesJSON for kind-specific fields.
How the two architectural patterns complement each other¶
The microkernel (ADR-003) and product catalog patterns address orthogonal concerns:
| Concern | Pattern | Role |
|---|---|---|
| How new capabilities enter the system | Microkernel (plugins) | Parser plugins convert protocol-specific cards into AgentType + []Capability |
| How capabilities are structured, stored, and queried | Product Catalog (archetype) | Polymorphic Capability interface + normalized storage + registry-based deserialization |
A parser plugin (microkernel extension point) produces domain objects (product catalog entities). The capability registry (product catalog infrastructure) validates and deserializes what plugins produce. Neither pattern works alone:
- Without the microkernel: adding a new protocol means modifying the core parsing logic.
- Without the product catalog: each plugin would define its own storage schema, breaking uniform querying and API responses.
Use Cases¶
1. Multi-protocol catalog view¶
An operator lists all agents: A2A agents show skills and security schemes, MCP agents show tools and resources. The API returns a uniform JSON array where each entry has a capabilities array with kind-discriminated objects. The frontend filters by kind prefix (a2a.* vs mcp.*) — no protocol-specific API endpoints needed.
2. Capability-based discovery¶
A developer searches "which agents offer an MCP tool named code-review?" This is a query on the capabilities table: WHERE kind = 'mcp.tool' AND name = 'code-review', joined back to catalog_entries for the full listing. The promoted name column enables indexed lookups without JSON parsing.
3. Adding a new capability kind¶
MCP adds a "sampling" capability in a future spec version. Implementation: define MCPSampling struct implementing Capability, register it with RegisterCapability("mcp.sampling", ...) in an init() function, update the MCP parser plugin to extract it. No database migration — the capabilities table already handles arbitrary kinds via the kind+properties columns.
4. Security scheme modeling without parallel storage¶
Security schemes are complex (OAuth 2.0 flows with scopes, mutual TLS, API keys in different locations). Rather than a separate security_details table, they live in the existing capabilities table as a2a.security_scheme kind entries. The A2ASecurityScheme struct uses a union pattern with a Type discriminator for sub-variants (oauth2, apiKey, http, openIdConnect, mutualTls). Nested structures like OAuthFlows serialize into the properties JSON column. This keeps all capability data in one table with one query path.
5. Catalog presentation independence¶
An internal catalog entry shows an agent as "Translation Service (APAC)" while an external catalog entry for the same agent shows "AI Translator". Both reference the same AgentType with the same capabilities. CatalogEntry handles display concerns; AgentType handles identity and capabilities. This separation is enforced by reflection-based tests (archetype_test.go) that verify field ownership between the two structs.
6. Full-text search across capabilities¶
An operator searches "authentication" across the catalog. The description column on the capabilities table participates in search queries directly, finding agents whose security schemes, skills, or tools mention authentication — without deserializing JSON blobs.
Consequences¶
Positive¶
- Single
capabilitiestable serves all protocols and all capability kinds — no N+1 tables problem. - New capability kinds require zero database migrations.
- Promoted
nameanddescriptioncolumns enable indexed queries and full-text search. - CatalogEntry
MarshalJSON()flattens AgentType fields into a clean REST response — API consumers see a simple flat object. - The registry + factory pattern gives type-safe deserialization with graceful handling of unknown kinds (skip, don't crash).
Negative / Trade-offs¶
- The
propertiesJSON column means some capability fields are not individually indexable. If a future query pattern requires filtering by a field insideproperties(e.g., "all agents using OAuth 2.0 authorization code flow"), it requires either promoting that field to a column (migration) or scanning JSON. - Union structs (like
A2ASecuritySchemewith all sub-variant fields) trade Go type safety for serialization simplicity. A capability can haveAPIKeyLocationset even whenTypeisoauth2— runtime validation (Validate()) catches this, not the compiler. - Full-replacement updates (delete-all + re-insert capabilities) are simple but generate more write I/O than diff-based updates. Acceptable at current scale.
Neutral¶
- The pattern originates from retail/product management domains. The mapping to AI agent catalogs is natural but not obvious — new contributors need to understand the archetype to understand why capabilities are on
AgentTypeand notCatalogEntry. - ADR-001 documents the detailed Go translation of each archetype principle. This ADR documents only the rationale for choosing the pattern and how it interacts with the microkernel.
Alternatives considered¶
| Option | Why rejected |
|---|---|
One table per protocol (a2a_skills, mcp_tools, etc.) |
Breaks uniform querying; adding a protocol requires migrations; N+1 tables for N capability kinds |
| Capabilities as JSON blob on AgentType row | Cannot index by kind or name; full-text search requires JSON parsing; no unique constraints on (agent, kind, name) |
| Separate domain models per protocol | Duplicates CRUD logic, API endpoints, and storage code for each protocol; discovery/listing requires protocol-aware union queries |
| EAV (Entity-Attribute-Value) model | Flexible but query-hostile; reconstructing a capability requires N rows; no type safety on attribute names |
| GraphQL schema-per-protocol | Shifts complexity to the API layer; still needs a unified storage model underneath; premature for current scale |