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:
| Scope | What the agent can do |
|---|---|
agent:read | Dashboard + queue visibility. No state changes. |
agent:write | Everything in agent:read + queue/cancel/approve/reject jobs, pause/resume printers, manage spools & alerts, log maintenance. |
admin | Full 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)
| Tool | Purpose |
|---|---|
farm_summary | One-shot dashboard — printer counts by state, queue depth, active jobs, unread alerts. Call this first. |
list_printers | Fleet list, optional status filter (idle / printing / paused / offline / error). |
get_printer | Full status + telemetry + active job + AMS slots for one printer. |
list_jobs | Jobs by status (pending / submitted / printing / completed / failed / cancelled). |
get_job | Full job record. |
list_queue | Pending/queued jobs in priority order. |
list_alerts | Alerts by severity + read state. |
list_spools | Spool inventory. Filter by filament type or "available only". |
list_filaments | Filament library types. |
list_maintenance_tasks | Scheduled maintenance, optional overdue-only filter. |
list_orders | Customer orders by status. |
Live write tools (scope: agent:write or admin)
Every write tool accepts optional dry_run and idempotency_key inputs.
| Tool | Purpose |
|---|---|
queue_job | Add a job. Optionally pin to a specific printer. |
cancel_job | Cancel a pending/queued/printing job. |
approve_job | Approve a job awaiting review. |
reject_job | Reject with a required reason (shown to submitter). |
pause_printer | Pause the active print. |
resume_printer | Resume a paused printer. |
mark_alert_read | Mark an alert read. |
dismiss_alert | Hide an alert from the dashboard. |
assign_spool | Bind a spool to a printer AMS slot. |
consume_spool | Log grams used against a spool. |
complete_maintenance | Log a maintenance-task completion. |
Reference tools (no backend required)
| Tool | Purpose |
|---|---|
calculate_print_cost | Material + electricity + depreciation + failure-rate math. |
recommend_printer_for_farm | Match printers to farm constraints. |
estimate_farm_capacity | Throughput forecasting. |
compare_farm_software | Feature 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: truemarker in the output. - Same key, different body →
409 idempotency_conflict. Use a fresh key. - Concurrent retry in-flight →
409 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_jobeven 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:writeminted by an operator works; the mint endpoint refuses to issueagent:writeto 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 (whichsafe_postvalidates). -
Proxy bypass closed — every ITAR-protected httpx client passes
trust_env=Falsewhen ITAR is active, soHTTP(S)_PROXYenv 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
- API Tokens — creating scoped tokens
- API Overview — REST API surface
- Environment Variables —
ODIN_ITAR_MODE,API_KEY, etc.