MastertheMesh
Solo · AgentRegistry · arctl · MCP · skill · agent · kagent · kind
Built · scaffolded, published, hosted on kagent, tested live

AgentRegistry end to end, part 1: arctl from init to an agent hosted on kagent

AgentRegistry is a catalog for the building blocks of agentic systems: agents, MCP servers, and skills. arctl is its CLI, and this lab drives it through the whole lifecycle. We scaffold all three artifact kinds with arctl init, customize them into something real (a text-utilities MCP server, a house-style summary skill, and a summarizer agent that uses both), prove the trio works locally with arctl run, build the scaffolded Dockerfiles into OCI images and push them with arctl build, and publish everything to the catalog with arctl apply. Then the payoff: a Kubernetes Runtime points the registry at a kind cluster, an AgentRegistry Deployment lands the agent on Solo Enterprise for kagent, and we test the hosted agent through the controller's OIDC-protected A2A endpoint with a real Keycloak token. Every output below is captured from the live run.

Series: AgentRegistry end to end. This is part 1: the lifecycle, from scaffold to a hosted, OIDC-protected agent. Part 2 picks up on this exact cluster, it depends on the Keycloak realm, registry daemon, and catalog this lab deploys, and adds registry identity, AccessPolicies, and per-team catalog visibility on top.

The use case

An agent on a laptop is a demo. An agent your team can find, reuse, and run is a platform, and that needs three things the laptop never provides: a catalog that says what exists, packaging that makes each piece reproducible, and a runtime that hosts the result behind real authentication. That is exactly the AgentRegistry split. Artifacts are scaffolded and built as OCI images, published to a catalog other people can query, and deployed by reference onto a runtime. Here the runtime is Solo Enterprise for kagent, so the moment the agent lands in the cluster it inherits the controller's OIDC enforcement: callers need a valid Keycloak token before they ever reach the model.

Three artifacts, one catalog

The lab builds one of each artifact kind, deliberately small so the wiring stays legible. The agent consumes the other two: it calls the MCP server's tools at runtime, and the skill is folded into its instruction at build time.

MCPServer · acme/textkit

textkit

what
Two tools: word_count and extract_links
stack
FastMCP, Python, stdio transport
ships as
OCI image built from the scaffolded Dockerfile
Skill · summary-style

Summary house style

what
A SKILL.md: length budget, bullet rules, sources line
stack
Markdown with YAML frontmatter
ships as
Git-sourced; baked into the agent image at build
Agent · summarizer

summarizer

what
Summarizes pasted text in the house format
stack
ADK Python, Anthropic claude-haiku-4-5
uses
textkit tools via MCP + the baked skill

The flow

YOUR LAPTOP arctl init · run · build · apply AgentRegistry daemon catalog + UI · :12121 local OCI registry localhost:5001 KIND CLUSTER · arctl-lab kagent controller OIDC validate · A2A :8083 Solo Enterprise for kagent Keycloak realm solo · alice/field-fte summarizer agent pod ADK · claude-haiku-4-5 textkit MCP pod kmcp · word_count · links alice Keycloak bearer 1 · apply 2 · build --push 3 · Runtime 4 · kagent CRDs 5 · pull 6 · A2A MCP

Step 1: scaffold with arctl init

One command per artifact kind. Each produces a complete project: source layout, manifest, Dockerfile, env wiring, tests. The flags pick the framework, language, and model up front, and --local-mcp wires the agent's local dev config at the textkit server so the pair work together from the first run.

arctl init mcp acme/textkit   --framework fastmcp --language python
arctl init skill summary-style
arctl init agent summarizer   --framework adk --language python \
    --model-provider anthropic --model-name claude-haiku-4-5 \
    --local-mcp ./textkit

What lands on disk is the part that usually takes a day of boilerplate. The MCP project gets a FastMCP server with a dynamic tool loader and a multi-stage Dockerfile; the agent project gets an ADK application, an A2A agent card, a Dockerfile based on the kagent ADK base image, and a .env.example already pointing at the local textkit; the skill gets a SKILL.md and its catalog manifest.

textwhat arctl init generates (trimmed)
artifacts/
├── textkit/                  # arctl init mcp acme/textkit
│   ├── mcp.yaml              #   catalog manifest (kind: MCPServer)
│   ├── Dockerfile            #   multi-stage python + uv build
│   ├── src/
│   │   ├── main.py           #   entrypoint (stdio or http transport)
│   │   ├── core/server.py    #   FastMCP app + dynamic tool loader
│   │   └── tools/            #   one file per tool, discovered by name
│   └── tests/                #   pytest discovery + server tests
├── summary-style/            # arctl init skill summary-style
│   ├── skill.yaml            #   catalog manifest (kind: Skill)
│   └── SKILL.md              #   the skill body
└── summarizer/               # arctl init agent summarizer
    ├── agent.yaml            #   catalog manifest (kind: Agent)
    ├── Dockerfile            #   FROM kagent-adk base image
    ├── .env.example          #   ANTHROPIC_API_KEY + MCP_SERVERS_CONFIG
    └── summarizer/
        ├── agent.py          #   ADK root_agent definition
        ├── agent-card.json   #   A2A card
        └── mcp_tools.py      #   MCP wiring read from env

Step 2: make the artifacts real

The scaffolds run as generated, but they are templates. Three focused edits turn them into the demo. First, the MCP server gets two real tools. The loader in the scaffold discovers a tool per file by naming convention, so each tool is just a decorated function:

pythonartifacts/textkit/src/tools/word_count.py
@mcp.tool(
    description=(
        "Count the words, characters, and sentences in a block of text. "
        "Call this before summarizing so the summary length is proportional "
        "to the input size."
    )
)
def word_count(text: str) -> dict:
    words = re.findall(r"\b\w+\b", text)
    sentences = re.findall(r"[.!?]+(?:\s|$)", text)
    return {
        "words": len(words),
        "characters": len(text),
        "sentences": max(len(sentences), 1 if text.strip() else 0),
    }
pythonartifacts/textkit/src/tools/extract_links.py
_URL_RE = re.compile(r"https?://[^\s<>\")']+")

@mcp.tool(
    description=(
        "Extract every http/https URL from a block of text, de-duplicated and "
        "in first-seen order. Use this to list the source links for a summary."
    )
)
def extract_links(text: str) -> dict:
    seen: list[str] = []
    for match in _URL_RE.findall(text):
        url = match.rstrip(".,);]")
        if url not in seen:
            seen.append(url)
    return {"count": len(seen), "links": seen}

Second, the skill. A skill is instructions as an artifact: versioned, published, reusable by any agent. This one defines the house summary format, including the workflow that tells the model to call the textkit tools before writing:

markdownartifacts/summary-style/SKILL.md (trimmed)
# Summary house style

## Workflow
1. Call the `word_count` tool on the input first. Aim for roughly 10% of the
   input length, never more than 150 words.
2. Call the `extract_links` tool. If it returns links, list them under a
   `Sources:` line at the end.
3. Write the summary in the format below.

## Format
- Open with a single bold one-line takeaway, no more than 20 words.
- Follow with 3 to 5 bullet points, one sentence each.
- End with a `Sources:` line listing the URLs, comma separated.

Third, the agent reads both. Its instruction is the role plus the skill body, and the skill ships inside the agent image: the SKILL.md is copied into the agent project before the build and a small loader folds it into the instruction at startup. That makes the published agent image self-contained, with the exact skill version it was built against:

pythonartifacts/summarizer/summarizer/agent.py (the wiring)
_ROLE = (
    "You are a summarization assistant. The user gives you a block of text and "
    "you return a summary. You have textkit MCP tools available: word_count and "
    "extract_links. Always follow the summary house style below."
)
_INSTRUCTION = _ROLE + "\n\n" + load_baked_skills()   # SKILL.md body, frontmatter stripped

root_agent = Agent(
    model=LiteLlm(model="anthropic/claude-haiku-4-5"),
    name="summarizer_agent",
    instruction=build_instruction(_INSTRUCTION),
    tools=get_mcp_tools(),                            # textkit, injected via env
)

Step 3: prove it locally first

Before any cluster exists, arctl run gives the inner dev loop. The lab's test-local.sh starts the textkit MCP server on the host with the HTTP transport, then runs the agent; arctl run builds the agent image, starts it, waits for the endpoint, and drops you into an interactive A2A chat. The agent's .env points MCP_SERVERS_CONFIG at the host-side textkit, so the full agent-to-tools path is exercised with nothing but Docker:

# terminal 1 (or let test-local.sh manage both)
cd artifacts/textkit && uv run python src/main.py --transport http --port 3000

# terminal 2: build + run the agent, interactive A2A chat
arctl run ./artifacts/summarizer

Step 4: build the images, publish to the catalog

Two of the three artifacts are container images, and arctl build builds them with the Dockerfiles the scaffold generated. The scaffolds default their image identifiers to localhost:5001/..., which is exactly the local OCI registry the lab runs next to kind, so --push needs no extra configuration. The skill is git-sourced and has no image.

arctl build ./artifacts/textkit    --push     # localhost:5001/textkit:latest
arctl build ./artifacts/summarizer --push     # localhost:5001/summarizer:latest

The textkit Dockerfile is the scaffold's multi-stage Python build: a builder layer resolves dependencies with uv, the production layer runs as a non-root user with a healthcheck. The agent's Dockerfile is even shorter because the heavy lifting lives in the kagent ADK base image:

dockerfileartifacts/summarizer/Dockerfile (generated by arctl)
ARG DOCKER_REGISTRY=ghcr.io
ARG VERSION=0.8.0-beta6
FROM $DOCKER_REGISTRY/kagent-dev/kagent/kagent-adk:$VERSION

WORKDIR /app
COPY summarizer/ summarizer/
COPY pyproject.toml pyproject.toml
COPY README.md README.md
COPY .python-version .python-version

RUN uv sync
ENV OTEL_SERVICE_NAME=summarizer
CMD ["summarizer"]
dockerfileartifacts/textkit/Dockerfile (generated by arctl, trimmed)
FROM python:3.12-slim AS builder
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
WORKDIR /app
COPY pyproject.toml .python-version* uv.lock* README.md ./
RUN uv sync --frozen --no-dev --no-cache --no-install-project
COPY src/ ./src/
COPY mcp.yaml ./

FROM python:3.12-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
RUN groupadd -r mcpuser && useradd -r -g mcpuser mcpuser
WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
COPY --from=builder /app/src /app/src
COPY --from=builder /app/mcp.yaml /app/mcp.yaml
ENV PATH="/app/.venv/bin:$PATH" PYTHONPATH=/app PYTHONUNBUFFERED=1
USER mcpuser
# ENTRYPOINT (not CMD) so transport args append instead of replacing
ENTRYPOINT ["python", "src/main.py"]

Publishing is arctl apply on each manifest, against the local AgentRegistry daemon (Docker Compose, API and UI on localhost:12121). Order matters once: the MCPServer must exist before the Agent that references it, because the registry resolves spec.mcpServers at apply time.

arctl apply -f artifacts/textkit/mcp.yaml          # MCPServer  acme/textkit
arctl apply -f artifacts/summary-style/skill.yaml  # Skill      summary-style
arctl apply -f artifacts/summarizer/agent.yaml     # Agent      summarizer
yamlthe three catalog manifests (trimmed)
apiVersion: ar.dev/v1alpha1
kind: MCPServer
metadata:
  name: acme/textkit
spec:
  title: textkit
  description: Text utilities for summarization agents.
  source:
    package:
      identifier: localhost:5001/textkit:latest
      registryType: oci
      transport: { type: stdio }
---
apiVersion: ar.dev/v1alpha1
kind: Skill
metadata:
  name: summary-style
spec:
  title: Summary house style
  description: The house format for summaries.
  source:
    repository:
      url: https://github.com/tjorourke/solo-labs.git
      branch: main
      subfolder: agentregistry-arctl-kind/artifacts/summary-style
---
apiVersion: ar.dev/v1alpha1
kind: Agent
metadata:
  name: summarizer
spec:
  description: Summarizes pasted text in the house format, using textkit MCP tools.
  modelName: claude-haiku-4-5
  modelProvider: anthropic
  source:
    image: localhost:5001/summarizer:latest
  mcpServers:
    - kind: MCPServer
      name: acme/textkit

Once published, the catalog is the shared source of truth: anyone pointed at the registry can browse what exists, who it belongs to, and where it came from. Note the provenance built into the artifacts themselves. The MCP server and agent resolve to OCI images by digestable identifier, and the skill links straight back to its source repository, branch, and subfolder, so a catalog entry is traceable to code:

$ arctl get mcp acme/textkit && arctl get skill summary-style && arctl get agent summarizer
NAME           TAG      DESCRIPTION
acme/textkit   latest   Text utilities for summarization agents — word_count an...

NAME            TAG      DESCRIPTION
summary-style   latest   The house format for summaries — length budget, bullets...

NAME         TAG      PROVIDER    MODEL
summarizer   latest   anthropic   claude-haiku-4-5

Step 5: point the registry at the cluster

A Runtime is the registry's pointer to somewhere agents can run. For Kubernetes it is a namespace plus a kubeconfig. There is one genuinely fiddly bit on a laptop: the daemon runs in Docker, so 127.0.0.1 in a normal kubeconfig means the daemon's own container. The lab joins the daemon to the kind Docker network and feeds the Runtime the cluster's internal kubeconfig (kind get kubeconfig --internal), whose server address is the control-plane container's hostname and whose TLS SAN actually matches it.

yamlthe Runtime (kubeconfig filled in by the script)
apiVersion: ar.dev/v1alpha1
kind: Runtime
metadata:
  name: kind-kagent
spec:
  type: Kubernetes
  config:
    namespace: kagent
    kubeconfig: |
      # `kind get kubeconfig --internal` output: the server is the
      # control-plane container hostname, reachable from the daemon
      # over the kind docker network

Step 6: deploy the agent onto kagent

Deployment is also a registry resource: it binds an Agent to a Runtime and carries per-instance environment. Apply it with arctl and the registry's Kubernetes adapter translates it into kagent resources in the target namespace:

yamlthe Deployment that hosts summarizer on the kind Runtime
apiVersion: ar.dev/v1alpha1
kind: Deployment
metadata:
  name: summarizer
spec:
  targetRef:  { kind: Agent,   name: summarizer }
  runtimeRef: { kind: Runtime, name: kind-kagent }
  env:
    ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY}"   # the agent's model key
    # MCP wiring is injected by the registry from the Agent's spec.mcpServers

What lands in the cluster is the part worth staring at. The registry creates a kagent Agent of type BYO for the summarizer image and a kmcp MCPServer for textkit, and the kagent controller takes it from there: pods are scheduled, images pulled from localhost:5001 (the nodes are wired to resolve it), and the agent is registered on the controller's A2A surface.

$ kubectl -n kagent get agents,pods
NAME                                            TYPE   READY   ACCEPTED
agent.kagent.dev/summarizer-latest-summarizer   BYO    True    True

NAME                                                READY   STATUS
pod/summarizer-latest-summarizer-9748fdccc-k7rp2    1/1     Running
pod/acme-textkit-summarizer-5689d64f47-svrst        1/1     Running   # the MCP
pod/kagent-controller-bbd97cbf7-r29wq               1/1     Running
Two sharp edges, baked into the scripts. Keep the Deployment name short: the registry derives the in-cluster MCP service name from the MCP and Deployment names combined, and an over-long result gets truncated, which silently breaks the agent's MCP wiring. And the OCI/stdio MCP runs through a kmcp MCPServer whose relay launches the server by an explicit command, so the lab patches spec.deployment.cmd to python src/main.py after the resource appears.

Step 7: test it through kagent

The hosted agent sits behind Solo Enterprise for kagent's OIDC interceptor, so the test is honest: mint a real token for alice from the in-cluster Keycloak, then call the agent over the controller's A2A endpoint. Alice is in group field-fte, which the install maps to the kagent Admin role via the token's groups claim, so she may invoke agents. The lab's ask.sh does both steps with plain curl:

./scripts/ask.sh "summarize this: <paste a paragraph with a couple of links>"
bashwhat ask.sh does (token + A2A message/send by hand)
# 1. mint alice's token from the in-cluster Keycloak (realm solo)
TOKEN=$(curl -s -X POST \
  "http://localhost:18080/realms/solo/protocol/openid-connect/token" \
  -d "grant_type=password&client_id=kagent&username=alice&password=alice" \
  | jq -r .access_token)

# 2. send the task over A2A (trailing slash on the agent path matters:
#    without it the controller 307-redirects and drops the POST body)
curl -X POST "http://localhost:8083/api/a2a/kagent/summarizer-latest-summarizer/" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":"1","method":"message/send",
       "params":{"message":{"role":"user","messageId":"m1",
         "parts":[{"kind":"text","text":"summarize this: ..."}]}}}'

The claims in alice's token, and the reply that came back from the hosted agent, both exactly as captured from the live cluster:

jsonalice's token claims (decoded, trimmed)
{
  "iss": "http://keycloak.keycloak.svc.cluster.local/realms/solo",
  "aud": "kagent",
  "preferred_username": "alice",
  "groups": ["field-fte"],
  "email": "alice@example.com"
}
A2A · summarizer-latest-summarizer as alice · captured live
A
alice

summarize this: AgentRegistry is an open catalog for AI agents, MCP servers, skills and prompts. The arctl CLI scaffolds a new artifact from a template, builds it into an OCI image, and publishes it to a registry so other people can discover and reuse it. The registry daemon exposes an API and a web UI on port 12121. A Kubernetes Runtime adapter translates a Deployment resource into kagent CRDs, which means a published agent can be hosted on Solo Enterprise for kagent with OIDC authentication enforced in front of it. Docs live at https://aregistry.ai and the source is at https://github.com/agentregistry-dev/agentregistry. The project is Apache 2 licensed.

S
summarizeragent · hosted on kagent

AgentRegistry is an open catalog for discovering and reusing AI agents and MCP artifacts

• The arctl CLI creates new artifacts from templates, packages them as OCI images, and publishes them to the registry for discovery and reuse
• A registry daemon provides API and web UI access on port 12121
• Kubernetes Runtime adapters enable published agents to run on Solo Enterprise with OIDC authentication enforcement
• The project is Apache 2 licensed and available open source

Sources: https://aregistry.ai, https://github.com/agentregistry-dev/agentregistry

Look at what that one reply proves. The bold headline and bullet format come from the skill, the proportional length comes from the word_count MCP tool, the Sources line comes from extract_links, and the whole thing only ran because alice's Keycloak token passed the controller's OIDC check. Every artifact in the catalog, and the runtime in front of them, in a single request.

Sharp edges worth knowing

Run it yourself

Everything in this lab, the scripts, the kind config, the YAML, and the three scaffolded artifact projects, is in the public solo-labs repo, so you can clone and run it directly. One kind cluster, one local OCI registry, the AgentRegistry daemon in Docker, and Solo Enterprise for kagent with Keycloak in front. Bring an Anthropic key for the agent's model and a Solo license for the enterprise kagent install:

git clone https://github.com/tjorourke/solo-labs.git
cd solo-labs/agentregistry-arctl-kind
export ANTHROPIC_API_KEY=sk-ant-...
export SOLO_LICENSE_KEY=...            # Solo Enterprise for kagent

./scripts/quick.sh up                  # cluster → keycloak → kagent → daemon
                                       #   → scaffold check → build/publish → deploy

./scripts/ask.sh "summarize this: "

./scripts/test-local.sh                # the no-cluster inner loop (arctl run)
./scripts/quick.sh status              # catalog + cluster at a glance
./scripts/quick.sh teardown

Where to take it next

This lab is the paved road: scaffold, publish, deploy, invoke. The enterprise registry is built for the governance jobs that start the moment a second team shows up, and that is exactly where the series goes. Part 2 is live: it puts the same Keycloak realm in front of the registry itself and works through role mapping, AccessPolicies, and per-team catalog visibility on this same cluster. The full list of natural follow-ons:

See also

Versions

Built and verified on:

Enterprise
Solo Enterprise for kagent0.4.3
Gateway APIv1.4.0