Skip to content

Track 2 — API Reusability

By the end of Track 1, your capability could list, inspect, plan, and enrich. Five tools, two APIs, one contract that survived the journey from mock to production.

But the Shipyard doesn't live alone. Partner agents want a catalog they can discover. Logic keeps getting copy-pasted between tools. The operations dashboard speaks REST, not MCP. This track adds three layers on top of Track 1 without touching the core tools:

Step Adds Why
8 — Skill groups type: skill + SkillTool.from Discoverable grouping of MCP tools
9 — Aggregates & ref aggregates: + ref: Share logic across MCP, Skill, and REST
10 — REST adapter type: rest Same logic, HTTP-shaped door for non-AI clients

Same YAML. Still no code.

Before you start. Complete Track 1, Steps 1–7. Files live in ikanos-docs/tutorial/ in the public Ikanos repository.


Step 8 — Skill Groups

File: step-8-shipyard-skill-groups.yml

Five tools today. Twenty tomorrow. Fifty next quarter. An agent landing on your MCP endpoint sees a flat list and has to figure out on its own which tools matter for planning a voyage versus auditing the fleet. That's noise.

Agent Skills are the table of contents for your toolbox. They group related tools under a business-facing label (fleet-ops, voyage-ops).

- type: skill
  port: 3002
  namespace: shipyard-skills
  description: "Shipyard skill groups for structured agent discovery"
  skills:                                 # alpha3 — keyed map by skill name
    fleet-ops:
      description: "Fleet management  list, search, and inspect ships"
      tools:                              # alpha3 — keyed map by tool name
        list-ships:
          from: { sourceNamespace: shipyard-tools, action: list-ships }
        get-ship:
          from: { sourceNamespace: shipyard-tools, action: get-ship }
    voyage-ops:
      description: "Voyage planning  create and manage voyages"
      tools:
        create-voyage:
          from: { sourceNamespace: shipyard-tools, action: create-voyage }

No logic duplication — each skill just references existing MCP tools via from.sourceNamespace + action. Purely additive: your MCP tools keep working exactly as before.

The SKILL adapter also auto-exposes a read-only HTTP API on port 3002 for remote discovery (GET /skills, GET /skills/{name}, GET /skills/{name}/download).

Run it

curl -L -o step-8-shipyard-skill-groups.yml \
  https://raw.githubusercontent.com/naftiko/ikanos/main/ikanos-docs/tutorial/step-8-shipyard-skill-groups.yml

ikanos validate step-8-shipyard-skill-groups.yml
ikanos serve    step-8-shipyard-skill-groups.yml

Probe the Skill discovery endpoint:

curl http://localhost:3002/skills
curl http://localhost:3002/skills/fleet-ops

🧭 What you learned: type: skill, SkillTool.from, SKILL HTTP API for remote discovery.


Step 9 — Aggregates & Ref

File: step-9-shipyard-aggregates.yml

Step 7 defined a three-step chain inside get-ship-with-crew. Step 11 will chain seven steps for the Fleet Manifest. And the REST adapter (Step 10) wants to expose get-ship-with-crew too. That's the same logic in three places.

Aggregates are named, reusable computation units. flows inside them define the steps. Tools and REST operations reference them with ref: instead of inlining logic. Each flow declares semantics (safe, idempotent, cacheable) and the framework derives MCP hints automatically.

aggregates:                                # alpha3 — keyed map (under capability)
  crew-resolver:
    display: "Crew Resolver"
    flows:                                 # alpha3 — keyed map (was `functions`)
      resolve-crew-for-ship:
        semantics:
          safe: true
          idempotent: true
          cacheable: true
        inputParameters:
          imo: { type: string, required: true }
        steps:                             # alpha3 — keyed map
          get-ship:
            type: call
            call: registry.get-ship
            with: { imo_number: crew-resolver.imo }
          list-crew:
            type: call
            call: registry.list-crew
          resolve-crew:
            type: lookup
            index: list-crew
            match: crewId
            lookupValue: "$.get-ship.assignedCrew"
            outputParameters: ["fullName", "role"]
        mappings:
          - { target: imo,  value: "$.get-ship.imo_number" }
          - { target: name, value: "$.get-ship.vessel_name" }
          - { target: crew, value: "$.resolve-crew" }

Tools and REST ops now point to the aggregate:

tools:
  get-ship-with-crew:
    inputParameters:
      imo: { type: string, required: true }
    ref: crew-resolver.resolve-crew-for-ship
    with:
      imo: shipyard-tools.imo

No hints: block. The engine sees safe: true → derives readOnly: true, destructive: false. idempotent: true → carried through to MCP ToolAnnotations. cacheable → unlocks response caching.

The output is byte-for-byte identical to Step 7. The spec got dramatically cleaner.

Run it

curl -L -o step-9-shipyard-aggregates.yml \
  https://raw.githubusercontent.com/naftiko/ikanos/main/ikanos-docs/tutorial/step-9-shipyard-aggregates.yml

ikanos validate step-9-shipyard-aggregates.yml
ikanos serve    step-9-shipyard-aggregates.yml

Then, in MCP Inspector at http://localhost:3001, call get-ship-with-crew(imo: "IMO-9321483") and confirm the response matches Step 7 byte-for-byte.

🧭 What you learned: aggregates, flows, semantics, ref, automatic semantics→hints derivation.


Step 10 — REST adapter

File: step-10-shipyard-rest-adapter.yml

Not every consumer is an AI agent. The ops dashboard is a plain React app. The partner logistics company speaks OpenAPI. type: rest exposes HTTP endpoints mapped to the same consumed operations — or, better, the same aggregate functions from Step 9.

- type: rest
  port: 3003
  namespace: shipyard-api
  resources:
    - name: ships
      path: "/ships"
      operations:
        - { name: list-ships, method: GET, call: registry.list-ships }
        - name: get-ship
          method: GET
          path: "/ships/{imo}"
          inputParameters:
            - { name: imo, in: path, required: true }
          call: registry.get-ship
          with: { imo_number: shipyard-api.imo }
        - name: get-ship-with-crew
          method: GET
          path: "/ships/{imo}/crew"
          inputParameters:
            - { name: imo, in: path, required: true }
          ref: crew-resolver.resolve-crew-for-ship
          with: { imo: shipyard-api.imo }

Two things to notice:

  1. REST operations declare HTTP-level parameter placement (in: path, in: query, in: body) that MCP tools don't need.
  2. get-ship-with-crew uses ref: crew-resolver.resolve-crew-for-ship — the exact same aggregate the MCP tool uses.

Three expose blocks (MCP, Skill, REST) now coexist in the same capability, all backed by the same consumes and aggregates.

Run it

curl -L -o step-10-shipyard-rest-adapter.yml \
  https://raw.githubusercontent.com/naftiko/ikanos/main/ikanos-docs/tutorial/step-10-shipyard-rest-adapter.yml

ikanos validate step-10-shipyard-rest-adapter.yml
ikanos serve    step-10-shipyard-rest-adapter.yml

Three doors, three smoke tests:

# MCP (port 3001) — via MCP Inspector
npx @modelcontextprotocol/inspector

# Skill catalog (port 3002)
curl http://localhost:3002/skills

# REST adapter (port 3003)
curl http://localhost:3003/ships
curl http://localhost:3003/ships/IMO-9321483
curl http://localhost:3003/ships/IMO-9321483/crew

🧭 What you learned: type: rest, HTTP verbs and paths, inputParameters with in: path|query|body, ref on REST operations, MCP ↔ Skill ↔ REST coexistence.


Recap

You now have one capability that serves three classes of consumer:

  • AI agents via MCP
  • Skill-aware agents via the SKILL HTTP catalog
  • Web/mobile/partner systems via REST

…all from the same consumes adapters and aggregates — no logic duplication.

Move on to Track 3 — Agent Orchestration for the Fleet Manifest capstone.