Skip to main content
Glama

aipod — one binary, two modes

Purpose. aipod is a single program you can start as an MCP server or as an agent, chosen by a subcommand:

Command

Mode

What it is

Publishes

aipod server

MCP server

a reference implementation of every MCP feature, so client / gateway / runtime authors have one endpoint to test against

a service contract (GET /contract.json)

aipod agent

agent

a pydantic-ai agent that connects to an aipod server over MCP and exposes its tools to a model

an agent card (GET /.well-known/agent-card.json)

Built on FastMCP (server mode) and pydantic-ai's MCP client (agent mode). Packaged as a single FROM scratch container; the same image runs either mode.

Repo: https://github.com/bigg01/aipod  ·  Latest release: v0.1.7  ·  Image: ghcr.io/bigg01/aipod  ·  Chart: oci://ghcr.io/bigg01/charts/aipod

Live instance for remote MCP testing: a public aipod server runs at https://aipod.guggenbuehl.net/ — MCP endpoint https://aipod.guggenbuehl.net/mcp, contract at /contract.json. Open by default; point any MCP client at it without installing anything. Shared and best-effort — treat state (incidents, deployments) as scratch.

aipod architecture: MCP clients and agent platforms talk to aipod server; aipod agent talks to aipod server and to a model

Wondering why a reference MCP server and agent are worth having around? See docs/blog/contracts-and-agent-cards.md.

Server mode — feature surface

Area

Details

Tools

echo, add, get_tiny_image, get_annotated_message, get_structured_weather, get_resource_reference, get_resource_links, trigger_long_running_operation, toggle_simulated_logging, toggle_subscriber_updates

Marvel roster

list_heroes, get_hero, find_heroes_by_power, assemble_team — typed Hero / HeroRoster / MissionTeam output over a small fixed dataset

SRE / IT-application

list_services, get_service, check_service_health, error_budget, search_logs, list_incidents, open_incident, update_incident, list_deployments, rollback_deployment, get_oncall, get_runbook — a toy service estate with mutable incident / deployment state and deterministic synthetic metrics & logs

pydantic-ai tools

poet, summarize (structured output), weather_report, hero_bio, incident_postmortem — model supplied by the client via MCP sampling (no server-side key)

Structured output

get_structured_weather / summarize / the roster & SRE tools return typed Pydantic models → output schema + structuredContent

Side effects

open_incident, update_incident, rollback_deployment (+ the toggle_* / trigger_* demo tools) are flagged sideEffects: true in the contract

Content blocks

text, image, embedded resource, resource links, priority / audience annotations

Resources

static docs + templated demo://resource/dynamic/{text,blob}/{resource_id}, hero://roster/{codename}, service://catalog/{name}, runbook://{service}

Prompts

simple_prompt, args_prompt, completable_prompt, resource_prompt

Auth

open by default; add a key and /mcp becomes an OAuth 2.1 protected resource (bearer token + /.well-known/oauth-protected-resource)

Metrics

on by default (Prometheus GET /metrics); OpenTelemetry per-MCP-method + per-tool counters/histograms, sampling count, inventory gauges. AIPOD_METRICS=otlp|console|none to change or disable

Also

argument completion, resource subscriptions, progress, logging/setLevel

HTTP routes: GET / (landing), GET /health, GET|POST /mcp, GET /contract.json, GET /metrics (Prometheus, on by default), and — when auth is enabled — GET /.well-known/oauth-protected-resource.

GET / itself is a plain landing page listing every tool, resource, and prompt above — open it in a browser once the server is running:

aipod server's landing page at GET /, listing every tool, resource, and prompt

Related MCP server: Echo MCP Server

Agent mode

        HTTP + JSON                    MCP (Streamable HTTP)
client ─────────────▶ aipod agent ────────────────────────▶ aipod server
                      pydantic-ai Agent + model provider    tools / resources / prompts

HTTP routes: GET /, GET /health, GET /.well-known/agent-card.json, POST /ask (legacy {"prompt":"..."} → {"output":"..."} and Responses-style {"input":"..."} / {"input":[...messages...]} → {"object":"response", ...}).

Agent mode needs a model — AIPOD_MODEL (e.g. anthropic:claude-haiku-4-5) plus the provider key. Without one it still serves the card and /health; /ask returns 503.

Requirements

  • Python ≥ 3.11, uv

  • Docker + a Kubernetes cluster (optional)

Run locally

uv sync

# server mode
uv run aipod server                            # http://127.0.0.1:8000  (MCP at /mcp)
uv run aipod server --transport stdio          # for subprocess clients (Claude Desktop, editors)
uv run aipod server --print contract           # emit the service contract as JSON
uv run aipod server --auth-token s3cret        # require 'Authorization: Bearer s3cret' on /mcp

# agent mode (needs a running server + a model)
export AIPOD_MCP_URL=http://127.0.0.1:8000/mcp
export AIPOD_MODEL=anthropic:claude-haiku-4-5
export ANTHROPIC_API_KEY=...
uv run aipod agent                             # http://127.0.0.1:8080
uv run aipod agent --ask "Write a poem about sockets, then summarise it."
uv run aipod agent --print agent-card          # emit the agent card as JSON

stdio vs. HTTP (server mode)

  • Streamable HTTP (default) — a long-running network service; clients connect to /mcp, responses and notifications stream back as SSE. Use for anything shared or deployed.

  • stdio — no network listener. The client launches aipod server as a child process and talks to it over that process's stdin/stdout. "Subprocess clients" are desktop / editor MCP hosts (Claude Desktop, Cursor, the VS Code MCP extension) that work this way; you never start the server yourself.

Authentication (optional)

The server runs open by default. Give it a key and the Streamable HTTP /mcp route becomes an OAuth 2.1 protected resource:

uv run aipod server --auth-token s3cret          # or: AIPOD_API_KEY=s3cret
export AIPOD_API_KEYS="key-a,key-b"              # multiple keys (rotation / per-client)

Env var

Effect

AIPOD_API_KEY / AIPOD_API_KEYS

keys the server accepts (--auth-token wins)

AIPOD_AUTH_SCOPES

CSV of scopes a caller must hold (default: none)

AIPOD_AUTH_ISSUER

authorization-server URL advertised in metadata (default: this server)

AIPOD_PUBLIC_URL

externally reachable base URL when behind a proxy / ingress

With auth on:

  • requests to /mcp without Authorization: Bearer <key> get 401 + a WWW-Authenticate header pointing at GET /.well-known/oauth-protected-resource (RFC 9728);

  • that metadata document lists the authorization server(s) and scopes;

  • contract.json → security switches from {"scheme":"none"} to a bearer block, and clientRequirements.authentication.required becomes true.

Tokens are checked against the static key list — the resource-server half of the spec without an identity provider. For full OAuth 2.1, point AIPOD_AUTH_ISSUER at a real authorization server and replace StaticTokenVerifier in src/aipod/server/auth.py with a JWT-validating one.

aipod agent reaches a protected server by setting AIPOD_MCP_TOKEN.

curl -s http://127.0.0.1:8000/mcp -X POST ... -H 'Authorization: Bearer s3cret'
curl -s http://127.0.0.1:8000/.well-known/oauth-protected-resource | jq

Full walkthrough in docs/testing-mcp.md.

Observability (OpenTelemetry)

Both modes emit OpenTelemetry metrics, on by default with the Prometheus exporter — aipod server serves GET /metrics with no configuration. The server instruments its own MCP internals, not just the Python process:

Instrument

Type

Attributes

mcp.server.requests

counter

mcp.method (tools/call, resources/read, prompts/get, completion/complete, resources/subscribe, logging/setLevel, …), outcome

mcp.server.request.duration

histogram (s)

same

mcp.server.tool.calls

counter

mcp.tool.name, outcome (ok/error), mcp.tool.sampling

mcp.server.tool.duration

histogram (s)

per tool name

mcp.server.sampling.requests

counter

server → client sampling round-trips

mcp.server.tools / .resources / .resource_templates / .prompts

gauges

the registered inventory

mcp.server.resource_subscriptions.active / mcp.server.background_tasks.active

gauges

live per-connection state

aipod.agent.ask.calls / aipod.agent.ask.duration

counter / histogram

outcome (agent mode)

# default: Prometheus scrape endpoint on the mode's HTTP port
uv run aipod server
curl -s localhost:8000/metrics | grep mcp_server_

# push to an OTLP/HTTP collector instead
AIPOD_METRICS=otlp OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 uv run aipod agent

# stdout, for a quick look
AIPOD_METRICS=console uv run aipod server

# turn it off
AIPOD_METRICS=none uv run aipod server

AIPOD_METRICS = prometheus (default) | otlp | console | none; OTEL_METRICS_EXPORTER=none or OTEL_SDK_DISABLED=true also disable it; a bare OTEL_EXPORTER_OTLP_ENDPOINT selects otlp. OTEL_SERVICE_NAME / OTEL_RESOURCE_ATTRIBUTES set the resource. In k8s: metrics.exporter in the Helm values, or AIPOD_METRICS in k8s/configmap.yaml.

Grafana dashboard — dashboards/aipod.json (import it directly) covers the inventory gauges, per-method + per-tool rate / errors / latency, sampling, and the agent /ask. On a kube-prometheus-stack cluster, kubectl apply -k dashboards ships it as a sidecar-loaded ConfigMap.

The running version shows on the landing page (GET /) and in GET /health ({"status":"ok","version":"…"}).

Test

uv run pytest

Hermetic — server tests drive an in-memory MCP session with a stubbed sampling callback; agent tests need no running server and no API key.

Exercise the server with the MCP Inspector

@modelcontextprotocol/inspector is the reference MCP client — it speaks the raw protocol, so no model or API key is needed (except for the sampling-backed tools).

uv run aipod server                      # start a server (MCP at :8000/mcp)

# interactive UI (http://127.0.0.1:6274)
npx -y @modelcontextprotocol/inspector                          # or: make inspect

# scripted / CI — one request per call (transport auto-detected from /mcp)
npx -y @modelcontextprotocol/inspector --cli \
  http://127.0.0.1:8000/mcp --method tools/list                 # or: make inspect-cli
npx -y @modelcontextprotocol/inspector --cli \
  http://127.0.0.1:8000/mcp --method tools/call --tool-name add --tool-arg a=2 --tool-arg b=3

# or skip the local server and hit the live instance
npx -y @modelcontextprotocol/inspector --cli \
  https://aipod.guggenbuehl.net/mcp --method tools/list

Full walkthrough — every feature (structured output, resource templates, completion, subscriptions, logging, progress, sampling), the --cli vs UI split, stdio via an mcp.json, and a CI gate example — in docs/testing-mcp.md.

Container (FROM scratch)

One image, either mode. PyInstaller bundles the app, staticx folds in libc, the final image is FROM scratch (binary + /tmp + CA certs + /etc/passwd), ~34 MB.

Every release publishes it to the GitHub Container Registry (public, no login):

docker pull ghcr.io/bigg01/aipod:0.1.1        # or :latest, :0.1, :sha-<commit>

docker run --rm -p 8000:8000 ghcr.io/bigg01/aipod:latest       # server (default CMD)
docker run --rm -p 8080:8080 \
  -e AIPOD_MCP_URL=http://host.docker.internal:8000/mcp \
  -e AIPOD_MODEL=anthropic:claude-haiku-4-5 -e ANTHROPIC_API_KEY=... \
  ghcr.io/bigg01/aipod:latest agent --host 0.0.0.0 --port 8080  # agent

Or build it yourself: docker build -t aipod:latest . (same result, make docker).

The binary self-extracts into TMPDIR (/tmp) on start, so the runtime needs a writable /tmp even with a read-only root filesystem.

Kubernetes

Both modes deploy from the one image, two ways:

Kustomize — k8s/

kubectl apply -k k8s:

  • Deployment/aipod-server (+ Service/aipod-server) — replicas: 1 (per-session state + background tasks live in memory)

  • Deployment/aipod-agent (+ Service/aipod-agent, Ingress) — replicas: 2, stateless; AIPOD_MCP_URL points at the server Service

  • ConfigMap/aipod-config — governance labels + AIPOD_MODEL; provider key from a Secret you create (kubectl create secret generic aipod-model --from-literal=ANTHROPIC_API_KEY=...)

  • Secret/aipod-auth (optional) — AIPOD_API_KEY turns on bearer auth for the server and is reused by the agent as AIPOD_MCP_TOKEN (kubectl create secret generic aipod-auth --from-literal=AIPOD_API_KEY=$(openssl rand -hex 16))

Helm — charts/aipod/

# from a checkout
helm install aipod ./charts/aipod

# or the published OCI chart
helm install aipod oci://ghcr.io/bigg01/charts/aipod --version 0.1.0 \
  -f examples/helm-values.yaml

Same objects, parameterised: server.enabled / agent.enabled, *.replicas, *.ingress.*, *.resources, the config map, and auth / model (inline key ⇒ the chart makes the Secret, or point at *.existingSecret). Full list in charts/aipod/values.yaml; examples/helm-values.yaml is a TLS-ingress + bearer-auth override. make helm-lint / helm-template / helm-install.

Both pods run non-root, no capabilities, read-only rootfs, RuntimeDefault seccomp, with an emptyDir at /tmp.

On Azure Kubernetes Service (AKS)

Same manifests, no Azure-specific changes needed beyond getting the image into a registry AKS can pull from:

az acr create -g my-rg -n myacr --sku Basic
az acr build -r myacr -t aipod:latest .          # builds in ACR, no local push needed

az aks create -g my-rg -n my-aks --attach-acr myacr
az aks get-credentials -g my-rg -n my-aks

# point k8s/kustomization.yaml's `images:` entry at myacr.azurecr.io/aipod, then:
kubectl apply -k k8s/

--attach-acr wires AKS's kubelet identity to pull from that registry without a separate imagePullSecret.

Agent platforms

Same server, same /mcp endpoint — different runtimes just point at it differently.

  • kagent registers a remote MCP server as its own CRD — see examples/kagent-remotemcpserver.yaml. Apply it and kagent discovers every tool the same way it discovers its own built-in tool server (kubectl get remotemcpserver aipod -o yaml → status.discoveredTools).

  • kars (Microsoft's Kubernetes-native agent runtime) has its own McpServer CRD — OAuth, per-tool allow-lists, and sandbox selectors included — see examples/kars-mcpserver.yaml.

  • Azure AI Foundry (and anything else using the same Responses-API-shaped MCP tool) takes the endpoint straight in the agent/tool definition, no separate resource — see examples/azure-ai-foundry-mcp-tool.json.

CI / releases

For Microsoft Foundry Hosted Agents, see the Invocations runtime and deployment guide.

.github/workflows/ci.yml runs on every push / PR: pytest on Python 3.11–3.13, uv build, uv lock --check, a check that examples/ is in sync, helm lint + kubeconform on the rendered manifests, and a FROM scratch image build with a /health + /contract.json smoke test.

.github/workflows/release.yml runs on a vX.Y.Z tag (which must match the pyproject.toml version) and produces, for that version:

  • Container — ghcr.io/bigg01/aipod tagged X.Y.Z + X.Y + latest + sha-<commit>, with an SBOM and build provenance attestation. Public — docker pull needs no login.

  • Helm chart — oci://ghcr.io/bigg01/charts/aipod, version pinned to the tag.

  • Binary — the static aipod-linux-x86_64.

  • GitHub Release — notes plus the binary and chart tarball attached.

Governance

Both modes carry the same labels from AIPOD_* env vars — a governance block in the server contract, an x-governance block (plus a dependencies link to the server's contract) in the agent card:

Env var

Field

AIPOD_OWNER

owner / provider.organization

AIPOD_DOMAIN

domain

AIPOD_DATA_CLASSIFICATION

dataClassification (PUBLIC…RESTRICTED)

AIPOD_DATA_RESIDENCY

dataResidency

AIPOD_REGULATORY_SCOPE

regulatoryScope (CSV)

AIPOD_AUTH_SCHEMES

authenticationSchemes (CSV)

AIPOD_CONTAINS_PII

containsPII

Per-tool the contract also has requiresSampling, sideEffects, and dataEgress so a router / gateway can gate calls on data movement and state changes rather than on tool names.

Layout

src/aipod/
  __main__.py          CLI - `aipod server` | `aipod agent`
  governance.py        shared AIPOD_* governance labels
  telemetry.py         OpenTelemetry metrics (both modes)
  server/
    build.py           every MCP feature on one FastMCP instance
    sampling_tools.py   pydantic-ai tools (model via MCP sampling)
    heroes.py          Marvel roster data + models for the roster tools
    sre.py            IT-application / SRE estate: catalogue, incidents, deploys, metrics
    auth.py            optional bearer-token / OAuth 2.1 protected-resource auth
    contract.py         service contract builder
    data.py, landing.py
  agent/
    runtime.py          pydantic-ai Agent + MCP toolset -> the server
    card.py             agent card builder
    http.py             Starlette app: card, /health, /ask, /metrics
    config.py           AIPOD_MCP_URL, AIPOD_MODEL, ...
packaging/  PyInstaller entry + spec
examples/   generated contract.json + agent-card.json + helm-values.yaml
k8s/        both Deployments, Services, Ingress, ConfigMap, kustomization
charts/aipod/  Helm chart (same objects, parameterised)
.github/workflows/  ci.yml (test + build) + release.yml (image + chart + binary)
docs/       testing-mcp.md (Inspector walkthrough) + blog/ (contracts & agent cards)
tests/      test_server.py + test_agent.py

Available Tools

31 tools
addAddB

Add two numbers and describe the result.

Inputs: a (required), b (required). Returns: result

assemble_teamAssemble teamA

Pick the heroes whose powers best fit a described threat (deterministic).

Inputs: threat (required), size. Returns: The output of ``assemble_team``.

check_service_healthCheck service healthA

Roll up synthetic metrics, SLOs, and open incidents into a healthy/degraded/down verdict.

Inputs: name (required). Returns: status, metrics, reasons, service, +3 more

echoEchoA

Echo the input message back to the caller.

Inputs: message (required). Returns: result

error_budgetError budgetB

Compute the remaining SLO error budget and current burn rate for a service.

Inputs: name (required), window_days. Returns: service, burn_rate, window_days, remaining_pct, +3 more

find_heroes_by_powerFind heroes by powerA

Return every hero whose power list matches the given substring (e.g. 'flight').

Inputs: power (required). Returns: A list of heroes plus its length, returned by the listing / search tools.

get_annotated_messageGet annotated messageC

Return content blocks carrying priority and audience annotations.

Inputs: message_type (required), include_image.

get_heroGet heroA

Return the full record for one hero by codename (e.g. 'spider-man').

Inputs: codename (required). Returns: One character in the roster.

get_oncallGet on-callA

Who is on call for a team or for the team that owns a service, plus the escalation order.

Inputs: team_or_service (required). Returns: team, engineer, escalation

get_resource_referenceGet resource referenceC

Return an embedded resource content block for a dynamic resource.

Inputs: kind, resource_id.

get_runbookGet runbookA

Return the runbook entries for a service, optionally narrowed to a symptom.

Inputs: service (required), symptom. Returns: entries, service

get_serviceGet serviceA

Return one service's catalogue entry: tier, team, dependencies, SLOs, repo.

Inputs: name (required). Returns: One application / service in the catalogue.

get_structured_weatherGet structured weatherA

Return a typed Weather object so the client can validate it against the output schema.

Inputs: location. Returns: Structured weather payload returned by the weather tools.

get_tiny_imageGet tiny imageC

Return text interleaved with an image content block.

Inputs: no named parameters.

hero_bioHero bio (pydantic-ai + sampling)B

A pydantic-ai agent writes a short in-universe bio from the hero's facts; the LLM comes from the client via MCP sampling.

Inputs: codename (required). Returns: result

incident_postmortemIncident postmortem (pydantic-ai + sampling)A

A pydantic-ai agent drafts a short blameless postmortem from an incident's facts; the LLM comes from the client via MCP sampling.

Inputs: incident_id (required). Returns: result

list_deploymentsList deploymentsB

List recent deployments, optionally filtered by service or status.

Inputs: status, service. Returns: count, deployments

list_heroesList heroesB

List the Marvel heroes in the roster, optionally filtered by team.

Inputs: team. Returns: A list of heroes plus its length, returned by the listing / search tools.

list_incidentsList incidentsA

List incidents, optionally filtered by status, severity, or service.

Inputs: status, service, severity. Returns: count, incidents

list_servicesList servicesA

List the application/service catalogue, optionally filtered by tier or environment.

Inputs: tier, environment. Returns: count, services

open_incidentOpen incidentB

Open a new incident against a service. Mutates server-side state.

Inputs: service (required), summary (required), severity (required). Returns: id, notes, status, service, +4 more

poetPoet (pydantic-ai + sampling)A

A pydantic-ai agent writes a short rhyming poem; the LLM comes from the client via MCP sampling.

Inputs: theme (required). Returns: result

rollback_deploymentRoll back deploymentA

Roll back a deployment by id (simulated). Mutates server-side state.

Inputs: deployment_id (required). Returns: id, status, service, version, +2 more

search_logsSearch logsA

Search a service's (synthetic, deterministic) log stream for a substring, optionally by level.

Inputs: service (required), level, limit, query. Returns: count, lines, query, service

summarizeSummarize (pydantic-ai structured output + sampling)C

A pydantic-ai agent returns a structured Summary; the LLM comes from the client via MCP sampling.

Inputs: text (required). Returns: Structured result produced by the `summarize` tool.

toggle_simulated_loggingToggle simulated loggingA

Start or stop a background task that emits a random-level log message every 5s.

Inputs: no named parameters. Returns: result

toggle_subscriber_updatesToggle subscriber updatesB

Start or stop a background task that emits resources/updated for every subscribed resource.

Inputs: no named parameters. Returns: result

trigger_long_running_operationTrigger long running operationA

Run a fake multi-step job, emitting a progress notification per step.

Inputs: steps, step_seconds. Returns: result

update_incidentUpdate incidentA

Move an incident to a new status (acknowledged/mitigated/resolved) with an optional note. Mutates state.

Inputs: status (required), incident_id (required), note. Returns: id, notes, status, service, +4 more

weather_reportWeather report (pydantic-ai + sampling)B

A pydantic-ai agent turns the structured readings for a city into a short spoken-style forecast.

Inputs: location. Returns: result

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    B3.2/5.0

    Scored across 31 tools

    Disambiguation4/5

    Most tools have clearly distinct purposes, especially in the service/incident cluster (list_services, get_service, check_service_health, error_budget, search_logs). A few borderline overlaps exist between the LLM-writing tools (poet, summarize, weather_report, hero_bio, incident_postmortem) and between find_heroes_by_power and assemble_team, but the descriptions are specific enough to prevent major confusion.

    Naming Consistency3/5

    The majority of tools follow a readable verb_noun snake_case pattern (list_heroes, get_service, update_incident, rollback_deployment). However, there is a noticeable mix of bare nouns (poet, weather_report, hero_bio, error_budget, incident_postmortem) and bare verbs (echo, add, sumarize), so the naming convention is consistent in style but not in grammatical structure.

    Tool Count2/5

    With 31 tools spanning math demos, protocol feature demos, hero management, and incident response, the server has too large a surface for an agent to navigate as a coherent tool set. Even if each tool is individually useful in a demo context, the count exceeds the threshold for a focused server and creates a sprawling, kitchen-sink feel.

    Completeness4/5

    The incident-response domain is well covered with services, health checks, incidents, oncall, deployments, runbooks, and postmortems, and the hero domain covers query/analysis workflows well with list, get, find, assemble, and bio tools. Minor gaps exist—there is no way to create or update services or heroes, and incident retrieval is only available via list_incidents—but these are workable for the apparent use cases.

    Maintenance

    ActivityNo data
    ResponsivenessOutdated data

    Related MCP Connectors

    Related MCP Servers

    • F
      license
      Not graded
      quality
      D
      maintenance
      A server implementation of the Model Context Protocol (MCP) that provides REST API endpoints for managing and interacting with MCP resources.
      -
    • F
      license
      Not graded
      quality
      D
      maintenance
      A simple demonstration MCP server that provides an echo tool and resource for learning how to build MCP servers. Serves as a starting point and template for creating custom MCP server implementations.
      1
      -
    • A
      license
      Not graded
      quality
      D
      maintenance
      A comprehensive reference implementation demonstrating all features of the Model Context Protocol (MCP) specification, serving as documentation, learning resource, and testing tool for MCP implementations.
      1
      MIT