Add a new generation tool worker
A tool is how the platform generates something — a song, stems, album art, a
conversion. Under the hood a tool is a long-running queue worker, not a web service:
no HTTP, no ingress. It polls the task queue, runs your model, uploads results, and drains
on SIGTERM. You write three things — an input model, an output model, and a
process() method — and the WorkerFleet in soundverse-py
does registration, task-claiming, heartbeats, uploads, usage reporting, and shutdown.
What you’ll touch
Section titled “What you’ll touch”Directorycore-tool-name/
Directoryapp/
Directorytools/
- name.py your input model + output model + tool class
- __init__.py register your tool in
ALL_TOOLS
- main.py set
SERVICE_NAME
- pyproject.toml set the package
name Directory.github/workflows/
- deploy-aca-staging.yml set
service_name - deploy-aca-prod.yml set
service_name
- deploy-aca-staging.yml set
-
Scaffold. Copy
template-core-toolinto a new repocore-tool-<name>. -
Write
app/tools/<name>.py— the three parts:app/tools/my_tool.py from pydantic import BaseModel, Fieldfrom soundverse.tools.base import BaseTool, ToolOutputfrom soundverse.tools.context import TaskContext# 1) INPUT model — a plain pydantic model. Field(description=...) and# constraints (ge/le/min_length…) become the MCP input JSON schema the# agent sees, and are validated before process() runs.class MyInput(BaseModel):prompt: str = Field(description="What to generate")seconds: int = Field(default=30, ge=5, le=120, description="Clip length")# 2) OUTPUT model — subclass ToolOutput. Field naming is a contract:# a field named `text` → the primary result; OutputFile/AudioFile/# ImageFile/VideoFile (or lists) → auto-uploaded, deduped assets;# everything else → metadata_json.class MyOutput(ToolOutput):text: str# 3) The tool class. process() is SYNCHRONOUS (runs in a thread pool).class MyTool(BaseTool[MyInput, MyOutput]):def process(self, input: MyInput, ctx: TaskContext) -> MyOutput:ctx.emit_progress(10, "starting")# … call your model …ctx.report_usage(input.seconds) # raw usage; the worker NEVER billsreturn MyOutput(text="done")Use the
ctx(TaskContext) for everything that reaches the platform:resolve_credentials(),set_streaming_url(),emit_progress(),emit_partial(),upload_file(),report_usage(),live_audio_ingest(), and theuser_id/workspace_id/project_idscope. -
Declare platform config so the tool gets priced. Pricing is the step people forget — see the dedicated guide. Declare it as class attributes; the fleet upserts it to the registry on boot.
app/tools/my_tool.py (class attributes) from soundverse.tools.config import LicensePricingfrom soundverse_proto.generation.tool.v1 import tool_pb2class MyTool(BaseTool[MyInput, MyOutput]):model = "my-model"operation = "generate"cost_unit = "second"usage_field = "seconds" # makes the reserve-time hold quantity-awarepricing = (LicensePricing(license=tool_pb2.LICENSE_STANDARD,cost_mode=tool_pb2.COST_MODE_DYNAMIC,cost_base=10, # reserve-time hold / floorcost_increment=1, # per reported usage unit),# …one entry per license you accept…) -
Register it. Import your class in
app/tools/__init__.pyand add an instance toALL_TOOLS(one worker can serve several tools).app/main.pyonly readsALL_TOOLS. -
Rename the service in four places —
nameinpyproject.toml,SERVICE_NAMEinapp/main.py, andservice_namein bothdeploy-aca-staging.ymlanddeploy-aca-prod.yml. Delete the example (rm app/tools/example_tool.py tests/test_example_tool.py), then runmake check(ruff + pyright strict + pytest). -
Registration is automatic on boot.
WorkerFleetcallsRegisterTool(withinput_schema_json+output_schema_jsonderived from your models), then applies the idempotentUpsertToolConfig/UpsertToolLicensePricing/UpsertToolRateLimits/UpsertToolCredentials. Your code is the source of truth — edit a value, restart the worker, it re-applies. Restart core-mcp. easy to miss core-mcp caches tool input/output schemas in memory. Redeploying the worker updates the DB registry row, but a changed schema is not picked up until core-mcp is restarted. See the core-mcp field guide.
-
Deploy. Ensure the org secrets/vars exist (prefixed
STAGING_/PROD_; the deploy engine strips the prefix and injects them):INTERNAL_RPC_SECRET,REDIS_ADDR,CORE_DATABASE_GRPC,CORE_STORAGE_GRPC, plus any provider key. Push to thestaging/prodbranch to deploy. See Deploy a service.
The traps that will actually bite you
Section titled “The traps that will actually bite you”What you get for free
Section titled “What you get for free”Because the panel is registry-only and schema-driven, your Pydantic input model is
the UI — the SaaS AI Tools panel renders a form from your
input_schema_json. No frontend code. You also get task claiming, heartbeats/leases,
progress + partial events, deduped uploads, usage reporting, and graceful drain — all from
the fleet.
Related
Section titled “Related”- Add pricing to a tool — the details behind step 3
- The soundverse-py SDK —
WorkerFleet+TaskContext - TaskContext API reference — every
ctx.*method - core-mcp — why the schema-cache restart is needed
- Deploy a service — pushing to staging/prod