aipod
Summary: aipod is a reference MCP server exposing ~30 tools across demos, a Marvel roster, an SRE/IT-application estate, and sampling-backed pydantic-ai tools — most returning typed structured output.
Basic demos:
echoa message,addtwo numbers, fetch a tiny image, get annotated content blocks (priority/audience), and return structured weather for Zurich or Savognin.MCP feature demos: return embedded resource references, resource links, run a long-running job with progress notifications, and toggle simulated logging or subscriber updates.
Marvel roster: list/filter heroes, look up a hero by codename, find heroes by power, and deterministically assemble a team for a threat.
SRE / service estate: list and inspect services (tier, team, deps, SLOs), check health, compute error budgets, search synthetic logs, and fetch runbooks and on-call info.
Mutable state: open/update incidents and roll back deployments (side-effecting).
Sampling-backed LLM tools:
poet,summarize,weather_report,hero_bio,incident_postmortem— the model is supplied by the client via MCP sampling, no server key needed.Structured output: most tools return typed Pydantic models with output schemas and
structuredContent.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@aipod Fetch the service contract and list the available tools."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 |
| MCP server | a reference implementation of every MCP feature, so client / gateway / runtime authors have one endpoint to test against | a service contract ( |
| agent | a pydantic-ai agent that connects to an | an agent card ( |
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.
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 |
|
Marvel roster |
|
SRE / IT-application |
|
pydantic-ai tools |
|
Structured output |
|
Side effects |
|
Content blocks | text, image, embedded resource, resource links, priority / audience annotations |
Resources | static docs + templated |
Prompts |
|
Auth | open by default; add a key and |
Metrics | on by default (Prometheus |
Also | argument completion, resource subscriptions, progress, |
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:

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 / promptsHTTP 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 JSONstdio 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 serveras 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 |
| keys the server accepts ( |
| CSV of scopes a caller must hold (default: none) |
| authorization-server URL advertised in metadata (default: this server) |
| externally reachable base URL when behind a proxy / ingress |
With auth on:
requests to
/mcpwithoutAuthorization: Bearer <key>get401+ aWWW-Authenticateheader pointing atGET /.well-known/oauth-protected-resource(RFC 9728);that metadata document lists the authorization server(s) and scopes;
contract.json→securityswitches from{"scheme":"none"}to abearerblock, andclientRequirements.authentication.requiredbecomestrue.
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 | jqFull 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 |
| counter |
|
| histogram (s) | same |
| counter |
|
| histogram (s) | per tool name |
| counter | server → client sampling round-trips |
| gauges | the registered inventory |
| gauges | live per-connection state |
| counter / histogram |
|
# 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 serverAIPOD_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 pytestHermetic — 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/listFull 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 # agentOr 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_URLpoints at the server ServiceConfigMap/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_KEYturns on bearer auth for the server and is reused by the agent asAIPOD_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.yamlSame 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
McpServerCRD — OAuth, per-tool allow-lists, and sandbox selectors included — seeexamples/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/aipodtaggedX.Y.Z+X.Y+latest+sha-<commit>, with an SBOM and build provenance attestation. Public —docker pullneeds 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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.pyAvailable Tools
31 toolsaddAddB
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_linksGet resource linksB
Return between 1 and 10 resource_link blocks pointing at dynamic resources.
Inputs: count.
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
Scored across 31 tools
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.
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.
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.
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
Related MCP Connectors
MCP compatibility and transport verification server.
MCP compatibility and transport verification server.
MCP compatibility and transport verification server.
MCP compatibility and transport verification server.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceA server implementation of the Model Context Protocol (MCP) that provides REST API endpoints for managing and interacting with MCP resources.-
- FlicenseNot gradedqualityDmaintenanceA 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-
- AlicenseNot gradedqualityDmaintenanceA comprehensive reference implementation demonstrating all features of the Model Context Protocol (MCP) specification, serving as documentation, learning resource, and testing tool for MCP implementations.1MIT
- AlicenseNot gradedqualityDmaintenanceA test MCP server that exercises all MCP protocol features, including prompts, tools, resources, and sampling, for client builders.443,034 npmMIT