Skip to main content

MCP Server

O.D.I.N. speaks the Model Context Protocol. Any MCP-compatible client — Claude Desktop, Claude Code, Cursor, LM Studio, Ollama frontends, OpenClaw skills — can drive the farm through the same stable tool surface.

The v2 surface (shipped in v1.8.9 / odin-print-farm-mcp@2) is designed for agent-driven operation: safe retries, previewable writes, structured errors, and a CMMC / ITAR-compatible deployment path.


Quick Start

1. Install

npx -y odin-print-farm-mcp@2

Or install globally:

npm i -g odin-print-farm-mcp

2. Mint a scoped token in O.D.I.N.

Go to Settings → API Tokens → New Token. Choose a scope matching the agent's job:

ScopeWhat the agent can do
agent:readDashboard + queue visibility. No state changes.
agent:writeEverything in agent:read + queue/cancel/approve/reject jobs, pause/resume printers, manage spools & alerts, log maintenance.
adminFull API. Not recommended for agents — grant the narrowest scope that works.

The admin / license / backup / user-CRUD / SMTP surface is humans-only regardless of scope. Agents cannot modify RBAC, rotate keys, accept license terms, or trigger backups.

3. Wire the client

Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json):

{
"mcpServers": {
"odin": {
"command": "npx",
"args": ["-y", "odin-print-farm-mcp@2"],
"env": {
"ODIN_BASE_URL": "http://192.168.1.100:8000",
"ODIN_API_KEY": "odin_xxxxxxxxxxxxxxxxxxxx"
}
}
}
}

Claude Code (via ~/.claude/claude.json or a project-local .mcp.json):

{
"mcpServers": {
"odin": {
"command": "npx",
"args": ["-y", "odin-print-farm-mcp@2"],
"env": {
"ODIN_BASE_URL": "http://192.168.1.100:8000",
"ODIN_API_KEY": "odin_xxxxxxxxxxxxxxxxxxxx"
}
}
}
}

OpenClaw / generic MCP host: the package binary is odin-mcp after npm i -g. Stdio transport, no network listener — the MCP client launches it as a subprocess.


Tool Surface

26 tools total — 22 live (drive an actual O.D.I.N. instance) plus 4 reference (standalone calculators that work without a backend).

Live read tools (scope: agent:read or higher)

ToolPurpose
farm_summaryOne-shot dashboard — printer counts by state, queue depth, active jobs, unread alerts. Call this first.
list_printersFleet list, optional status filter (idle / printing / paused / offline / error).
get_printerFull status + telemetry + active job + AMS slots for one printer.
list_jobsJobs by status (pending / submitted / printing / completed / failed / cancelled).
get_jobFull job record.
list_queuePending/queued jobs in priority order.
list_alertsAlerts by severity + read state.
list_spoolsSpool inventory. Filter by filament type or "available only".
list_filamentsFilament library types.
list_maintenance_tasksScheduled maintenance, optional overdue-only filter.
list_ordersCustomer orders by status.

Live write tools (scope: agent:write or admin)

Every write tool accepts optional dry_run and idempotency_key inputs.

ToolPurpose
queue_jobAdd a job. Optionally pin to a specific printer.
cancel_jobCancel a pending/queued/printing job.
approve_jobApprove a job awaiting review.
reject_jobReject with a required reason (shown to submitter).
pause_printerPause the active print.
resume_printerResume a paused printer.
mark_alert_readMark an alert read.
dismiss_alertHide an alert from the dashboard.
assign_spoolBind a spool to a printer AMS slot.
consume_spoolLog grams used against a spool.
complete_maintenanceLog a maintenance-task completion.

Reference tools (no backend required)

ToolPurpose
calculate_print_costMaterial + electricity + depreciation + failure-rate math.
recommend_printer_for_farmMatch printers to farm constraints.
estimate_farm_capacityThroughput forecasting.
compare_farm_softwareFeature matrix vs OctoFarm / Mainsail / Duet etc.

Useful for pre-sales or agents answering "what should I buy?" without an ODIN deployment.


Agent Primitives

The v2 surface inherits four backend primitives that make agent retries and previews safe.

Idempotency-Key (safe retries)

Every write tool auto-generates a UUID Idempotency-Key per invocation. Pass a stable key across retries to deduplicate:

queue_job(model_id=42, printer_id=1, idempotency_key="11111111-2222-3333-4444-555555555555")

Backend behavior:

  • Same key, same body → replays the cached response. _idempotent_replay: true marker in the output.
  • Same key, different body409 idempotency_conflict. Use a fresh key.
  • Concurrent retry in-flight409 idempotency_in_progress (retriable).
  • Authz changed since the original (role/scope/org demotion) → 409 idempotency_authz_changed. Mint a fresh key.
  • Original response couldn't be cached (cookies, oversized) → 409 idempotency_uncacheable_success. Mint a fresh key.

The 24h TTL keeps the cache bounded; an hourly pruner cleans up.

X-Dry-Run (preview writes)

Pass dry_run: true on any write tool to ask the backend what would happen:

cancel_job(job_id=42, reason="bad slice", dry_run=true)
# => {"dry_run": true, "would_execute": {"action": "cancel", "job_id": 42, ...}}

No DB commit, no event emit, no external call.

Deny-by-default safety gate (ODIN 1.9.0+). Sending X-Dry-Run: true to a route that hasn't opted into the preview branch returns HTTP 501 with error.code = "dry_run_unsupported" instead of silently executing the real mutation. Every route in the MCP catalog above IS opted in — the gate exists so future routes are safe-by-default during their retrofit window.

MCP client version-sniff (odin-mcp 2.1.0+). The MCP client checks GET /api/v1/version on first use and refuses to forward dry_run: true to ODIN backends older than 1.9.0, which would silently execute despite the flag. Refusal is a client-side OdinApiError("dry_run_unsupported_backend") with retriable: false. Upgrade ODIN or drop dry_run from the call.

Authorization — agent scopes vs JWT (v1.9.0)

The MCP tool surface uses stacked authorization: every agent route requires BOTH a role floor (enforced for JWT sessions) AND a token scope (enforced for per-user scoped tokens).

  • JWT sessions (portal UI, admin panel): pass the role check at the route level; the scope check is a no-op for non-token auth. Example: a viewer-role user cannot call cancel_job even through the MCP, because the route requires operator role. Scoped tokens do not grant a role escalation path.
  • Scoped tokens (MCP clients, service accounts): pass the scope check at the route level; the role check enforces the token owner's role floor. Example: a token with agent:write minted by an operator works; the mint endpoint refuses to issue agent:write to a viewer-role user.

This is why dropping either dep would create a hole — contract test tests/test_contracts/test_agent_routes_stack_both_gates.py walks every agent route and asserts both deps are present.

Minting a scoped token

curl -sS -X POST "$ODIN/api/v1/tokens" \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{"name": "claude-mcp", "scopes": ["agent:write"], "expires_days": 90}'
  • agent:read — mintable by any authenticated user (viewer+).
  • agent:write — mintable by operator and admin only.
  • admin — mintable by admin only.

Error envelope (machine-readable)

Every error returns both a legacy detail string (backward compat with existing UI) AND a structured error object:

{
"detail": "Printer 42 not found",
"error": {
"code": "printer_not_found",
"detail": "Printer 42 not found",
"retriable": false
}
}

Stable error.code values agents can branch on: printer_not_found, job_not_found, quota_exceeded, scope_denied, idempotency_conflict, itar_outbound_blocked, rate_limited, internal_error, and more.

next_actions hints

Write responses include next_actions: [...] — suggested follow-ups so a weak local model doesn't have to guess:

{
"job_id": 42,
"status": "queued",
"next_actions": [
{"tool": "get_job", "args": {"id": 42}, "reason": "check status"},
{"tool": "list_queue", "reason": "see queue depth"}
]
}

Pure hint — no enforcement. Designed for 7B–32B local models.


ITAR / CMMC Deployment

Set ODIN_ITAR_MODE=1 in the environment and ODIN runs in fail-closed air-gap mode:

  • Boot audit — if any configured outbound destination (webhook, license server, SMTP, MQTT republish, OIDC discovery, Spoolman URL, org webhook in groups.settings_json) resolves to a public address, the container refuses to start with a clear operator message.

  • Runtime guards — every outbound HTTP client (safe_post, license server calls, OIDC, Spoolman GETs, printer adapters Moonraker/PrusaLink/smart plug) validates the destination and DNS-pins the connection before the socket connect. Split-horizon / resolver-drift / cache-poison scenarios cannot leak.

  • Hard-disabled subsystems — APNs (Apple push), Web Push (browser vendor endpoints), and other fundamentally-public transports refuse to send under ITAR_MODE=1. Operators who need push wire it through an internal gateway + user-configured webhooks (which safe_post validates).

  • Proxy bypass closed — every ITAR-protected httpx client passes trust_env=False when ITAR is active, so HTTP(S)_PROXY env vars cannot route around the DNS pin.

The agent surface is designed to run fully on-prem under ITAR: local Ollama + Qwen2.5-32B-Instruct (or similar) + odin-print-farm-mcp + ODIN, zero outbound. CMMC-clean local-LLM recipes ship as deploy/agent-runtime/ in subsequent releases.


Authentication

Tokens use the odin_ prefix and pass in the X-API-Key header (the package handles this automatically from ODIN_API_KEY). Revoke a token at any time from Settings → API Tokens; a revoked token is blacklisted immediately and cached idempotency replays refuse to serve to it.


See Also