SMR SDK + MCP Control Proposal (2026-02-19)
Goal
Add first-class Synth SDK and MCP support for controlling Synth Managed Research (SMR) projects and runs.
This proposal covers:
- a new
synth_ai.sdk.managed_research client,
- a new MCP server surface that wraps the SDK,
- and rollout sequencing to align backend/frontend/spec route contracts.
Current State (from code)
What is already implemented
- Backend SMR control-plane routes exist in
/Users/joshpurtell/Documents/Github/backend/app/api/v1/routes_smr.py.
- SMR data-plane MCP exists in
/Users/joshpurtell/Documents/Github/synth-managed-research/crates/smr-worker-host/src/main.rs and serves:
POST /mcp/orchestrator
POST /mcp/worker/:worker_id
- Orchestrator and worker MCP tool sets are defined in:
/Users/joshpurtell/Documents/Github/synth-managed-research/crates/smr-worker-host/src/smr_orchestrator_tools.rs
/Users/joshpurtell/Documents/Github/synth-managed-research/crates/smr-worker-host/src/smr_worker_tools.rs
Gaps relevant to SDK + MCP product surface
synth-ai currently has no dedicated SMR control client module under synth_ai/sdk/.
- Frontend and specs use project-scoped run routes (
/smr/projects/{project_id}/runs/...) while backend currently exposes canonical run routes mostly under /smr/runs/....
- Frontend SMR provider-key route currently prefers encrypted payloads (
encrypted_key_b64) while backend SMR provider-key schema expects plaintext api_key.
Proposed SDK Surface
Module layout
Add:
/Users/joshpurtell/Documents/Github/synth-ai/synth_ai/sdk/managed_research/__init__.py
/Users/joshpurtell/Documents/Github/synth-ai/synth_ai/sdk/managed_research/client.py
/Users/joshpurtell/Documents/Github/synth-ai/synth_ai/sdk/managed_research/models.py
/Users/joshpurtell/Documents/Github/synth-ai/synth_ai/sdk/managed_research/errors.py
Export from:
/Users/joshpurtell/Documents/Github/synth-ai/synth_ai/sdk/__init__.py
Primary class
ManagedResearchClient
Constructor pattern should match existing SDK clients:
api_key: Optional[str] = None (fallback SYNTH_API_KEY)
backend_base: Optional[str] = None (fallback Synth backend resolver)
timeout_seconds: float = 30.0
Method groups
Project lifecycle:
create_project(...)
list_projects(include_archived=False)
get_project(project_id)
patch_project(project_id, ...)
get_project_status(project_id)
pause_project(project_id)
resume_project(project_id)
archive_project(project_id)
unarchive_project(project_id)
Onboarding + keys:
onboarding_start(project_id)
onboarding_complete_step(project_id, step, status, detail=None)
onboarding_dry_run(project_id)
onboarding_status(project_id)
set_provider_key(project_id, provider, funding_source, api_key=None, encrypted_key_b64=None, encrypt_before_send=False)
provider_key_status(project_id, provider, funding_source)
Run controls:
trigger_run(project_id, timebox_seconds=None)
list_runs(project_id)
list_active_runs(project_id)
get_run(run_id, project_id=None)
pause_run(run_id)
resume_run(run_id)
stop_run(run_id)
Human-in-loop:
list_project_questions(project_id, status_filter="pending")
list_run_questions(run_id, project_id=None)
respond_question(run_id, question_id, response_text, project_id=None)
list_project_approvals(project_id, status_filter="pending")
list_run_approvals(run_id, project_id=None)
approve(run_id, approval_id, comment=None, project_id=None)
deny(run_id, approval_id, comment=None, project_id=None)
Artifacts + observability:
list_run_artifacts(run_id, project_id=None)
get_artifact(artifact_id)
get_artifact_content_response(artifact_id, disposition="inline", follow_redirects=False)
get_usage(project_id)
get_run_spend_entries(run_id) (admin spend-ledger rows)
get_run_economics(run_id) (admin run economics summary)
get_run_usage_by_actor(run_id, project_id=None, include_done_tasks=True)
get_ops_status(project_id, include_done_tasks=None)
search_victoria_logs(project_id, ...)
Compatibility rule
For all run-scoped read/write methods, use project-scoped route first and fallback to canonical /smr/runs/... if project-scoped returns 404.
This gives a stable SDK surface now while backend aliases are added.
Error model
Define ManagedResearchApiError including:
status_code
method
path
detail_snippet
This should mirror existing SDK error ergonomics (clear status + endpoint context).
Proposed MCP Surface (in synth-ai)
Why separate MCP
SMR already has internal orchestrator/worker MCP for data-plane runtime.
This proposal adds a customer/operator-facing MCP server that exposes control-plane actions via the new SDK client.
Transport
- Default:
stdio
- Optional: Streamable HTTP
Rationale: aligns with MCP architecture and transport guidance, where stdio is common local transport and Streamable HTTP is recommended for production deployments.
Tool namespace
Use explicit names to avoid collision with internal worker tools.
Project tools:
smr.project.create
smr.project.list
smr.project.get
smr.project.update
smr.project.status
smr.project.pause
smr.project.resume
smr.project.archive
Onboarding tools:
smr.onboarding.start
smr.onboarding.complete_step
smr.onboarding.dry_run
smr.onboarding.status
smr.provider_key.set
smr.provider_key.status
Run tools:
smr.run.trigger
smr.run.list
smr.run.list_active
smr.run.get
smr.run.pause
smr.run.resume
smr.run.stop
Approval/question tools:
smr.question.list_project
smr.question.list_run
smr.question.respond
smr.approval.list_project
smr.approval.list_run
smr.approval.approve
smr.approval.deny
Artifact/ops tools:
smr.artifact.list_run
smr.artifact.get
smr.artifact.content_link
smr.usage.get
smr.run.spend_entries.get (admin scope)
smr.run.usage_by_actor.get
smr.ops_status.get
smr.logs.search
Safety defaults
- No
/smr/internal/* tools in this MCP.
- Explicitly mark side-effecting tools in tool descriptions.
- Require required IDs (
project_id, run_id, etc.) and reject ambiguous calls.
- Return structured JSON only (no markdown blobs) for machine composability.
Packaging + CLI integration
Add optional dependency group in pyproject.toml:
Add CLI command group (flat style, per CLI AGENTS guidance):
/Users/joshpurtell/Documents/Github/synth-ai/synth_ai/cli/commands/mcp/__init__.py
/Users/joshpurtell/Documents/Github/synth-ai/synth_ai/cli/commands/mcp/smr.py
Suggested command:
synth-ai mcp smr --transport stdio
synth-ai mcp smr --transport streamable-http --host 0.0.0.0 --port 8765
Rollout Plan
- Backend contract alignment
- Add project-scoped run aliases in backend.
- Add
encrypted_key_b64 compatibility for SMR provider-key set.
- Keep canonical
/smr/runs/... routes for backward compatibility.
- SDK alpha (
synth_ai.sdk.managed_research)
- Implement methods listed above.
- Add fallback behavior and unit tests.
- Export from
synth_ai.sdk root.
- MCP alpha (
synth-ai mcp smr)
- Implement read-only tools first.
- Add side-effect tools after validation.
- Add tool contract tests (
tools/list, tools/call).
- GA hardening
- Add end-to-end tests against local backend.
- Publish docs and examples.
- Set stability tag policy in docs (Alpha -> Beta -> Stable).
Acceptance Criteria
- Users can control full SMR lifecycle from Python without manual REST wiring.
- MCP tools cover all core operator actions and return consistent typed JSON.
- SDK behavior is stable despite backend route transition (project-scoped + canonical).
- Key upload path is explicit and secure, with deterministic fallback behavior.
Addendum (2026-02-20): Granular usage + dollar-cost semantics
get_run_usage_by_actor(...) now defines two explicit output modes:
usage_mode="spend_entries": exact cost path sourced from /smr/admin/runs/{run_id}/spend.
usage_mode="logs_thread_totals": fallback path sourced from run logs token snapshots.
In exact-cost mode, summary and each orchestrator/worker/model/session row includes:
total_cost_cents, total_cost_usd
meter_quantities (input, cached input, output, reasoning, and non-token meters when present)
meter_cost_cents, meter_cost_usd
token_usage split (input_tokens, cached_input_tokens, output_tokens, reasoning_output_tokens, total_tokens)
token_cost_cents, token_cost_usd split by:
- input
- cached input
- output
- reasoning output
cost_data_available=true
In fallback mode (admin spend unavailable), output includes model attribution + split token quantities but marks:
cost_data_available=false
total_cost_cents=None / total_cost_usd=None
Fallback attempts run-level estimated dollars from project usage rollups:
summary.estimated_total_cost_cents / summary.estimated_total_cost_usd
summary.estimated_orchestrator_total_cost_cents / summary.estimated_worker_total_cost_cents
- per actor/model/session:
estimated_total_cost_cents
estimated_total_cost_usd
summary.estimated_cost_source="project_usage_per_run_token_share"
Allocation rule for estimates: divide run-level project usage cost across actors/models/sessions by token_usage.total_tokens share.
Primary external references
1---2name: 2921-smr-sdk-mcp-control-proposal-2026-02-19-d3b178503description: SMR SDK + MCP Control Proposal (2026-02-19)4---5# SMR SDK + MCP Control Proposal (2026-02-19)67## Goal89Add first-class Synth SDK and MCP support for controlling Synth Managed Research (SMR) projects and runs.1011This proposal covers:1213- a new `synth_ai.sdk.managed_research` client,14- a new MCP server surface that wraps the SDK,15- and rollout sequencing to align backend/frontend/spec route contracts.1617## Current State (from code)1819### What is already implemented2021- Backend SMR control-plane routes exist in `/Users/joshpurtell/Documents/Github/backend/app/api/v1/routes_smr.py`.22- SMR data-plane MCP exists in `/Users/joshpurtell/Documents/Github/synth-managed-research/crates/smr-worker-host/src/main.rs` and serves:23 - `POST /mcp/orchestrator`24 - `POST /mcp/worker/:worker_id`25- Orchestrator and worker MCP tool sets are defined in:26 - `/Users/joshpurtell/Documents/Github/synth-managed-research/crates/smr-worker-host/src/smr_orchestrator_tools.rs`27 - `/Users/joshpurtell/Documents/Github/synth-managed-research/crates/smr-worker-host/src/smr_worker_tools.rs`2829### Gaps relevant to SDK + MCP product surface3031- `synth-ai` currently has no dedicated SMR control client module under `synth_ai/sdk/`.32- Frontend and specs use project-scoped run routes (`/smr/projects/{project_id}/runs/...`) while backend currently exposes canonical run routes mostly under `/smr/runs/...`.33- Frontend SMR provider-key route currently prefers encrypted payloads (`encrypted_key_b64`) while backend SMR provider-key schema expects plaintext `api_key`.3435## Proposed SDK Surface3637## Module layout3839Add:4041- `/Users/joshpurtell/Documents/Github/synth-ai/synth_ai/sdk/managed_research/__init__.py`42- `/Users/joshpurtell/Documents/Github/synth-ai/synth_ai/sdk/managed_research/client.py`43- `/Users/joshpurtell/Documents/Github/synth-ai/synth_ai/sdk/managed_research/models.py`44- `/Users/joshpurtell/Documents/Github/synth-ai/synth_ai/sdk/managed_research/errors.py`4546Export from:4748- `/Users/joshpurtell/Documents/Github/synth-ai/synth_ai/sdk/__init__.py`4950### Primary class5152`ManagedResearchClient`5354Constructor pattern should match existing SDK clients:5556- `api_key: Optional[str] = None` (fallback `SYNTH_API_KEY`)57- `backend_base: Optional[str] = None` (fallback Synth backend resolver)58- `timeout_seconds: float = 30.0`5960### Method groups6162Project lifecycle:6364- `create_project(...)`65- `list_projects(include_archived=False)`66- `get_project(project_id)`67- `patch_project(project_id, ...)`68- `get_project_status(project_id)`69- `pause_project(project_id)`70- `resume_project(project_id)`71- `archive_project(project_id)`72- `unarchive_project(project_id)`7374Onboarding + keys:7576- `onboarding_start(project_id)`77- `onboarding_complete_step(project_id, step, status, detail=None)`78- `onboarding_dry_run(project_id)`79- `onboarding_status(project_id)`80- `set_provider_key(project_id, provider, funding_source, api_key=None, encrypted_key_b64=None, encrypt_before_send=False)`81- `provider_key_status(project_id, provider, funding_source)`8283Run controls:8485- `trigger_run(project_id, timebox_seconds=None)`86- `list_runs(project_id)`87- `list_active_runs(project_id)`88- `get_run(run_id, project_id=None)`89- `pause_run(run_id)`90- `resume_run(run_id)`91- `stop_run(run_id)`9293Human-in-loop:9495- `list_project_questions(project_id, status_filter="pending")`96- `list_run_questions(run_id, project_id=None)`97- `respond_question(run_id, question_id, response_text, project_id=None)`98- `list_project_approvals(project_id, status_filter="pending")`99- `list_run_approvals(run_id, project_id=None)`100- `approve(run_id, approval_id, comment=None, project_id=None)`101- `deny(run_id, approval_id, comment=None, project_id=None)`102103Artifacts + observability:104105- `list_run_artifacts(run_id, project_id=None)`106- `get_artifact(artifact_id)`107- `get_artifact_content_response(artifact_id, disposition="inline", follow_redirects=False)`108- `get_usage(project_id)`109- `get_run_spend_entries(run_id)` (admin spend-ledger rows)110- `get_run_economics(run_id)` (admin run economics summary)111- `get_run_usage_by_actor(run_id, project_id=None, include_done_tasks=True)`112- `get_ops_status(project_id, include_done_tasks=None)`113- `search_victoria_logs(project_id, ...)`114115### Compatibility rule116117For all run-scoped read/write methods, use project-scoped route first and fallback to canonical `/smr/runs/...` if project-scoped returns `404`.118119This gives a stable SDK surface now while backend aliases are added.120121### Error model122123Define `ManagedResearchApiError` including:124125- `status_code`126- `method`127- `path`128- `detail_snippet`129130This should mirror existing SDK error ergonomics (clear status + endpoint context).131132## Proposed MCP Surface (in `synth-ai`)133134## Why separate MCP135136SMR already has internal orchestrator/worker MCP for data-plane runtime.137This proposal adds a customer/operator-facing MCP server that exposes control-plane actions via the new SDK client.138139### Transport140141- Default: `stdio`142- Optional: Streamable HTTP143144Rationale: aligns with MCP architecture and transport guidance, where stdio is common local transport and Streamable HTTP is recommended for production deployments.145146### Tool namespace147148Use explicit names to avoid collision with internal worker tools.149150Project tools:151152- `smr.project.create`153- `smr.project.list`154- `smr.project.get`155- `smr.project.update`156- `smr.project.status`157- `smr.project.pause`158- `smr.project.resume`159- `smr.project.archive`160161Onboarding tools:162163- `smr.onboarding.start`164- `smr.onboarding.complete_step`165- `smr.onboarding.dry_run`166- `smr.onboarding.status`167- `smr.provider_key.set`168- `smr.provider_key.status`169170Run tools:171172- `smr.run.trigger`173- `smr.run.list`174- `smr.run.list_active`175- `smr.run.get`176- `smr.run.pause`177- `smr.run.resume`178- `smr.run.stop`179180Approval/question tools:181182- `smr.question.list_project`183- `smr.question.list_run`184- `smr.question.respond`185- `smr.approval.list_project`186- `smr.approval.list_run`187- `smr.approval.approve`188- `smr.approval.deny`189190Artifact/ops tools:191192- `smr.artifact.list_run`193- `smr.artifact.get`194- `smr.artifact.content_link`195- `smr.usage.get`196- `smr.run.spend_entries.get` (admin scope)197- `smr.run.usage_by_actor.get`198- `smr.ops_status.get`199- `smr.logs.search`200201### Safety defaults202203- No `/smr/internal/*` tools in this MCP.204- Explicitly mark side-effecting tools in tool descriptions.205- Require required IDs (`project_id`, `run_id`, etc.) and reject ambiguous calls.206- Return structured JSON only (no markdown blobs) for machine composability.207208## Packaging + CLI integration209210Add optional dependency group in `pyproject.toml`:211212- `mcp>=1.0.0`213214Add CLI command group (flat style, per CLI AGENTS guidance):215216- `/Users/joshpurtell/Documents/Github/synth-ai/synth_ai/cli/commands/mcp/__init__.py`217- `/Users/joshpurtell/Documents/Github/synth-ai/synth_ai/cli/commands/mcp/smr.py`218219Suggested command:220221- `synth-ai mcp smr --transport stdio`222- `synth-ai mcp smr --transport streamable-http --host 0.0.0.0 --port 8765`223224## Rollout Plan2252261. Backend contract alignment227228- Add project-scoped run aliases in backend.229- Add `encrypted_key_b64` compatibility for SMR provider-key set.230- Keep canonical `/smr/runs/...` routes for backward compatibility.2312322. SDK alpha (`synth_ai.sdk.managed_research`)233234- Implement methods listed above.235- Add fallback behavior and unit tests.236- Export from `synth_ai.sdk` root.2372383. MCP alpha (`synth-ai mcp smr`)239240- Implement read-only tools first.241- Add side-effect tools after validation.242- Add tool contract tests (`tools/list`, `tools/call`).2432444. GA hardening245246- Add end-to-end tests against local backend.247- Publish docs and examples.248- Set stability tag policy in docs (Alpha -> Beta -> Stable).249250## Acceptance Criteria251252- Users can control full SMR lifecycle from Python without manual REST wiring.253- MCP tools cover all core operator actions and return consistent typed JSON.254- SDK behavior is stable despite backend route transition (project-scoped + canonical).255- Key upload path is explicit and secure, with deterministic fallback behavior.256257## Addendum (2026-02-20): Granular usage + dollar-cost semantics258259`get_run_usage_by_actor(...)` now defines two explicit output modes:260261- `usage_mode="spend_entries"`: exact cost path sourced from `/smr/admin/runs/{run_id}/spend`.262- `usage_mode="logs_thread_totals"`: fallback path sourced from run logs token snapshots.263264In exact-cost mode, `summary` and each orchestrator/worker/model/session row includes:265266- `total_cost_cents`, `total_cost_usd`267- `meter_quantities` (input, cached input, output, reasoning, and non-token meters when present)268- `meter_cost_cents`, `meter_cost_usd`269- `token_usage` split (`input_tokens`, `cached_input_tokens`, `output_tokens`, `reasoning_output_tokens`, `total_tokens`)270- `token_cost_cents`, `token_cost_usd` split by:271 - input272 - cached input273 - output274 - reasoning output275- `cost_data_available=true`276277In fallback mode (admin spend unavailable), output includes model attribution + split token quantities but marks:278279- `cost_data_available=false`280- `total_cost_cents=None` / `total_cost_usd=None`281282Fallback attempts run-level estimated dollars from project usage rollups:283284- `summary.estimated_total_cost_cents` / `summary.estimated_total_cost_usd`285- `summary.estimated_orchestrator_total_cost_cents` / `summary.estimated_worker_total_cost_cents`286- per actor/model/session:287 - `estimated_total_cost_cents`288 - `estimated_total_cost_usd`289- `summary.estimated_cost_source="project_usage_per_run_token_share"`290291Allocation rule for estimates: divide run-level project usage cost across actors/models/sessions by `token_usage.total_tokens` share.292293## Primary external references294295- MCP architecture: [https://modelcontextprotocol.io/docs/learn/architecture](https://modelcontextprotocol.io/docs/learn/architecture)296- MCP transports: [https://modelcontextprotocol.io/docs/concepts/transports](https://modelcontextprotocol.io/docs/concepts/transports)297- MCP tools: [https://modelcontextprotocol.io/docs/concepts/tools](https://modelcontextprotocol.io/docs/concepts/tools)298- MCP Python SDK: [https://github.com/modelcontextprotocol/python-sdk](https://github.com/modelcontextprotocol/python-sdk)299- Synth prompt optimization docs: [https://docs.usesynth.ai/prompt-optimization/gepa](https://docs.usesynth.ai/prompt-optimization/gepa)