-
Notifications
You must be signed in to change notification settings - Fork 0
service authoring
A shared service is a heavy or stateful sidecar (hindsight = postgres+MCP, openbrain, a custom
MCP tool) that runs as its own image/container/volume on the shared network, with a lifecycle
independent of any instance. Multiple instances attach to one running service concurrently;
claude+hindsight and omp+hindsight read and write one memory (design §3, §9).
For the why (why services are service-scoped, why they outlive instances, why they're separate
images), read docs/harnessed-design.md §3 & §9. This guide shows the how
with a worked example from catalog/services/ping/.
A service lives at catalog/services/<name>/ and ships three things:
| File | Role |
|---|---|
catalog/services/<name>/service.yaml |
the manifest: name, image, port, volume, healthcheck
|
catalog/services/<name>/Dockerfile |
the service's own image lineage (independent of the harness images) |
the server itself (e.g. server.py) |
the actual MCP server (Streamable HTTP) |
You manage services by name with harnessed svc up|down|list (implemented in src/harnessed/launcher.py), and a stack references one by listing it under services:.
The typed model lives in src/harnessed/schema.py (ServiceDef).
Flat scalars:
name: <service> # required
image: <name>:<tag> # required — the service's own image
volume: <volume-name> # optional — service-scoped named volume (survives svc down)
port: <port> # required — the port the server listens on
healthcheck: "<cmd>" # optional — readiness probe for `svc up` to pollA recipe references a service via mcp.servers[].service: <name>; the assembler resolves that to a
hatago URL-proxy entry pointing at http://<name>:<port>/mcp (see Attaching from a recipe below).
ping is the smallest shared-service sidecar — one ping MCP tool over Streamable HTTP, no
external state. All three files:
name: ping
image: harnessed-ping:latest
volume: ping-data
port: 8080
healthcheck: "curl -sf http://localhost:8080/health || exit 1"-
image: harnessed-ping:latest—svc upbuilds this from the service's ownDockerfileon first use (and the build-time BLD-02 image scan gates it). -
volume: ping-data— service-scoped; it survivessvc downby default (that's the value — one memory across instances).--purgeis the explicit destroy. -
healthcheck— whatsvc uppolls to confirm readiness before returning.
FROM python:3.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir "mcp[cli]"
WORKDIR /app
COPY server.py /app/server.py
EXPOSE 8080
HEALTHCHECK --interval=5s --timeout=3s --start-period=3s --retries=3 \
CMD curl -sf http://localhost:8080/health || exit 1
CMD ["python", "/app/server.py"]The service has its own image lineage (FROM python:3.12-slim) — it is not built FROM any
harness image. The HEALTHCHECK mirrors the manifest's healthcheck so podman and svc up agree
on readiness.
A FastMCP server over Streamable HTTP, with a /health route alongside the MCP endpoint:
from mcp.server.fastmcp import FastMCP
from mcp.server.transport_security import TransportSecuritySettings
from starlette.responses import PlainTextResponse
from starlette.routing import Route
mcp = FastMCP("ping")
# FastMCP's DNS-rebinding protection rejects the podman host-gateway Host header by default,
# so allow it alongside the localhost defaults (the service is proxied via host.containers.internal).
mcp.settings.transport_security = TransportSecuritySettings(
enable_dns_rebinding_protection=True,
allowed_hosts=["127.0.0.1:*", "localhost:*", "[::1]:*", "host.containers.internal:*"],
)
@mcp.tool()
def ping() -> str:
"""Return pong."""
return "pong"
async def _health(_request):
return PlainTextResponse("ok")
# FastMCP.streamable_http_app() serves the MCP endpoint at /mcp; add /health on the same port.
app = mcp.streamable_http_app()
app.router.routes.insert(0, Route("/health", _health, methods=["GET"]))
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8080)-
Streamable HTTP — one endpoint (
/mcp),POST+ optionalGET/SSE stream. SSE is deprecated in the current MCP spec (2025-06-18) and in Claude Code; do not author new SSE servers (see the "What NOT to Use" table inCLAUDE.md). - A separate
/healthroute lets the containerHEALTHCHECK(andsvc up) probe readiness without speaking MCP. - The
allowed_hostsentry forhost.containers.internalmatters on the default rootless networking model (see below).
harnessed svc up|down|list manages shared services by name — independent of any instance
(implemented in src/harnessed/launcher.py):
harnessed svc up ping # build image (first use) + create volume + run -d + wait for healthcheck
harnessed svc list # enumerate running harnessed-managed services
harnessed svc down ping # stop + remove the container (volume KEPT)
harnessed svc down ping --purge # stop + remove the container AND the volumeThe service is labelled harnessed-service=<name> and runs on the shared network; it is not a
pod member. A stack that declares the service auto-starts it on launch:
# catalog/stacks/ping-time/stack.yaml
name: ping-time
config: isolated
harness: claude
recipes: [time, ping]
services: [ping] # ← the isolated launcher runs ensure_service_up(ping) on launchThe service outlives the instance — stop the stack and the sidecar keeps running, so the next instance (or another stack) attaches to the same state.
A recipe references a service via mcp.servers[].service. The assembler resolves the name to a
hatago URL-proxy entry, so hatago proxies the network-native server:
# catalog/recipes/ping/recipe.yaml
mcp:
servers:
- name: ping
service: ping # ← resolved to http://ping:8080/mcp
transport: httpNetworking note: by default stacks use rootless (pasta) networking, so pod members reach
a shared service via the host gateway host.containers.internal:<port>. That is why server.py
adds host.containers.internal to FastMCP's allowed hosts. On hosts that support rootless bridges,
set HARNESSED_NET=<name> and members resolve the service by DNS name instead (http://<name>:<port>).
- docs/harnessed-design.md §3 & §9 — the why (runtime pod, service-scoped state & lifecycle).
-
Recipe-authoring guide — the service-ref MCP shape (
service:/transport: http). -
Stacks guide — declaring
services:in a stack manifest. -
catalog/services/ping/— the worked example (manifest + image + server). -
src/harnessed/schema.py— the typedServiceDefmodel.
Start Here
Guides
- Recipe authoring
- Service authoring
- Stacks
- Extending stacks (proposed)
- Recipe catalog
- System prompt & rules (proposed)
- Secrets
- AWS SSO
- Pulumi (host login forwarding)
- Egress & exposing services
- Container filesystem
- Git hooks
- Troubleshooting
- Pin management (harnessed update)
Codebase Map
Planning & Roadmap
- open work: GitHub Issues
Research & Prompts
- research/ (home-folder requirements per harness, browse in-repo)
- prompts/ (reusable prompt templates, browse in-repo)