Microservices were designed for deterministic clients. A developer reads the contract, writes the call sequence, handles the auth, picks the fields that matter — once.
An agent decides all of that at runtime, pays for every byte it reads, and retries when unsure. Expose your services one-to-one as MCP tools and every decision the developer made once, the model now makes on every request. The services are fine. The unit of exposure is wrong.
Same container infrastructure as your microservices #
This is not a new tier of infrastructure. A running Ikanos capability uses the same container infrastructure as your microservices: the naftiko/ikanos image from Docker Hub serving one spec file, deployed in your Kubernetes cluster as a Deployment behind a Service.
It scales like any Deployment, sits behind your gateway, and a control port exposes /health/live, /health/ready and /metrics for your probes and Prometheus. Your platform team operates it with the tools it already has. What differs is the contract: it is declared for an agent, not coded for a program.
Five decisions the agent should not be making #
1. Granularity. POST /orders, GET /users/{id}/payment-methods, POST /inventory/reserve are each correct; none of them is “fulfil this order”. An Ikanos aggregate composes them in steps — call and lookup — and exposes the flow as one tool.
2. Formats. The legacy service returns XML, the partner feed CSV. Each consumed operation declares its outputRawFormat (JSON, XML, Avro, Protobuf, CSV, YAML and more); the engine parses it and the agent only sees JSON.
3. Payload size. Responses carry database keys, trace IDs and audit fields. A capability maps only the fields it declares; everything else never leaves the engine.
4. Credentials. Auth is declared per consumed service (basic, apikey, bearer, digest, oauth2) with values resolved from binds at runtime. The model sees a tool name and its inputs, never a secret.
5. Side effects. An unsure agent retries, which is harmless for GET and costly for POST. Flows declare semantics (safe, idempotent, cacheable) and the engine derives MCP hints from them. Endpoints you don’t expose are tools the agent can’t call; rate limiting stays in your gateway.
One file, five answers #
Trimmed from step 9 of the Shipyard tutorial:
capability:
consumes:
- namespace: registry # JSON, bearer
type: http
baseUri: "https://registry.internal"
authentication: { type: bearer, token: "{{REGISTRY_TOKEN}}" }
- namespace: legacy # XML, API key
type: http
baseUri: "https://dockyard.internal"
authentication: { type: apikey, key: X-Dock-Key, placement: header, value: "{{DOCKYARD_API_KEY}}" }
aggregates:
- namespace: crew-resolver
flows:
resolve-crew-for-ship:
semantics: { safe: true, idempotent: true, cacheable: true }
steps:
get-ship: { type: call, call: registry.get-ship, with: { imo_number: "{{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: name, value: "$.get-ship.vessel_name" }
- { target: crew, value: "$.resolve-crew" }
exposes:
- type: mcp
namespace: shipyard-tools
tools:
get-ship-with-crew:
description: "Get ship details with resolved crew names"
ref: crew-resolver.resolve-crew-for-ship
Two services, two auth schemes, one goal-shaped tool. None of the services changed.
The takeaway #
- Microservices are the right way to build the backend, and the wrong unit to hand an agent.
- Capabilities run on the same container infrastructure: one container, one spec, in the cluster you already run.
- Make the five decisions once, in a reviewable spec, and expose the result as MCP, REST or a skill.