Ikanos — Schema — Aggregates¶
A domain aggregate, in the DDD sense,
groups reusable flows — transport-neutral invocable units that
adapters (MCP tools, REST operations) reference via ref:. Aggregates
factor domain behavior so it is defined once and reused across REST, MCP,
Skill, and future adapters without duplication.
Introduced in schema v1.0.0-alpha1. Refactored in v1.0.0-alpha3 to use keyed maps (
aggregates: {ns: ...},flows: {name: ...}) and renamefunctions:→flows:.
Why aggregates¶
Without aggregates you copy the same steps: block across the MCP tool, the
Skill catalog, and the REST operation that all want to expose the same
domain operation. That's where capabilities go to die.
With aggregates:
- Logic lives once, at capability level.
- Adapters declare
ref: {aggregate-namespace}.{flow-name}and inherit the chain. - Adapter-local explicit fields override inherited ones.
semanticsare derived into MCPhintsautomatically.
Aggregate object¶
capability:
aggregates: # keyed map; key IS the namespace
forecast:
display: "Forecast"
flows: { ... }
When used as a standalone document (root-level aggregates, no capability:)
the shape is a list, with namespace declared per entry:
| Field | Required | Description |
|---|---|---|
display |
✅ | Human-readable name |
namespace |
✅ (root form only) | IdentifierKebab; used as the first segment in ref:. Under capability.aggregates the map key IS the namespace. |
flows |
✅ | At least one flow |
Rules¶
namespaceMUST be unique across all aggregates.- No additional properties.
AggregateFlow¶
flows: # keyed map; key IS the flow name
get-forecast:
description: "Fetch current weather forecast for a location."
semantics:
safe: true
idempotent: true
inputParameters:
location: { type: string, description: "City or coordinates" }
call: "weather-api.get-forecast"
with: { location: location }
outputParameters:
- { type: string, mapping: "$.forecast" }
| Field | Required | Description |
|---|---|---|
| (map key) | ✅ | IdentifierKebab, unique within the aggregate — IS the flow name |
description |
✅ | Inherited by adapter units that omit their own |
semantics |
Transport-neutral behavior — see below | |
inputParameters |
Flow-level input parameters (keyed map) | |
call + with |
Simple mode | |
steps + mappings |
Orchestrated mode | |
outputParameters |
Flow-level output parameters |
Rules¶
- Exactly one mode (
call:orsteps:). - Flow names (map keys) MUST be unique within an aggregate.
- No chained refs — a flow cannot itself use
ref:.
Semantics¶
| Field | Default | Meaning |
|---|---|---|
safe |
false |
If true, the flow does not modify state |
idempotent |
false |
If true, repeating the call has no additional effect |
cacheable |
false |
If true, the result can be cached |
All optional. Omitted fields fall back to their defaults.
Semantics → MCP hints derivation¶
When an MCP tool references an aggregate flow via ref:, the flow's
semantics are automatically derived into MCP hints:
semantics |
Derived MCP hints |
Rationale |
|---|---|---|
safe: true |
readOnly: true, destructive: false |
A safe flow doesn't change state |
safe: false (or absent) |
readOnly: false |
Default — may have side effects |
idempotent: true |
idempotent: true |
Direct 1:1 mapping |
cacheable |
(not mapped) | No MCP equivalent; informational for future adapters |
Override rule: Each non-null field in the tool-level hints wins over the
derived value. Absent tool-level fields still inherit from semantics.
Using ref from adapters¶
exposes:
- type: mcp
namespace: forecast-mcp
tools: # keyed map; key IS the tool name
get-forecast:
ref: forecast.get-forecast # minimal — everything inherited
weather:
ref: forecast.get-forecast # override only the name (via map key)
weather-open:
ref: forecast.get-forecast
hints:
openWorld: true # add MCP-specific hint; semantics still derive the rest
- type: rest
namespace: forecast-rest
resources: # keyed map
forecast:
path: "/forecast/{location}"
operations: # keyed map
get-forecast:
ref: forecast.get-forecast
method: GET
inputParameters:
location: { in: path, type: string }
The merge rules:
- Explicit fields on the tool/operation override inherited fields.
- Fields not set on the tool/operation are inherited from the flow.
description:is optional when usingref:— it defaults to the flow's value. The tool/operation name is taken from the map key.
When to introduce an aggregate¶
| Symptom | Solution |
|---|---|
Same steps: block appears in two tools |
Move into an aggregate flow |
| MCP tool and REST operation share logic | Reference the same aggregate from both |
Manual hints: { readOnly: true } repeated |
Move semantics to the aggregate and let derivation do the work |
| Logic spans more than five steps | Aggregate it; the spec stays scannable |