MCP server
Connect any MCP client or SDK, then the tool reference.
POST /mcp/studio is a Model Context Protocol server over Streamable HTTP: stateless, one server per request, JSON responses, no server-initiated streams. Authenticate with the token in the Authorization: Bearer header; a client that cannot set headers may pass api_key as an argument to any tool. Every tool calls the same operations as the REST API, so an agent and a script are held to the same rules.
#Connect a client
Any MCP client that speaks Streamable HTTP with a custom header connects directly. Pick yours:
claude mcp add --transport http daishi-studio https://daishi.ai/mcp/studio \
--header "Authorization: Bearer dsk_..."// Settings > Developer > Edit Config (claude_desktop_config.json)
{
"mcpServers": {
"daishi-studio": {
"url": "https://daishi.ai/mcp/studio",
"headers": {
"Authorization": "Bearer dsk_..."
}
}
}
}// ~/.cursor/mcp.json, or .cursor/mcp.json in a project
{
"mcpServers": {
"daishi-studio": {
"url": "https://daishi.ai/mcp/studio",
"headers": {
"Authorization": "Bearer dsk_..."
}
}
}
}// .vscode/mcp.json
{
"servers": {
"daishi-studio": {
"type": "http",
"url": "https://daishi.ai/mcp/studio",
"headers": {
"Authorization": "Bearer dsk_..."
}
}
}
}// ~/.codeium/windsurf/mcp_config.json
{
"mcpServers": {
"daishi-studio": {
"serverUrl": "https://daishi.ai/mcp/studio",
"headers": {
"Authorization": "Bearer dsk_..."
}
}
}
}// ~/.gemini/settings.json
{
"mcpServers": {
"daishi-studio": {
"httpUrl": "https://daishi.ai/mcp/studio",
"headers": {
"Authorization": "Bearer dsk_..."
}
}
}
}// Any client that only launches stdio servers: bridge with mcp-remote
{
"mcpServers": {
"daishi-studio": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://daishi.ai/mcp/studio",
"--header",
"Authorization:Bearerdsk_..."
]
}
}
}#From your own code
The official SDKs speak Streamable HTTP; pass the token in the request headers. Signatures below follow the SDKs at the time of writing.
import asyncio, json, os
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
URL = "https://daishi.ai/mcp/studio"
HEADERS = {"Authorization": f"Bearer {os.environ['DAISHI_TOKEN']}"}
async def main():
async with streamablehttp_client(URL, headers=HEADERS) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
print([t.name for t in tools.tools])
result = await session.call_tool("validate_scenario", {
"name": "Ore rush",
"description": "Ore is plentiful and food is scarce; measures whether agents trade for calories.",
"season_ticks": 300,
"env": {
"seed": 42,
"resources": {
"ore": {
"max": 4,
"regen": 4
},
"food": {
"max": 0.5,
"regen": 0.5
}
},
"anchors": [
"honest-trader",
"greedy-harvester"
]
}
})
print(json.loads(result.content[0].text))
asyncio.run(main())import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
const transport = new StreamableHTTPClientTransport(new URL('https://daishi.ai/mcp/studio'), {
requestInit: { headers: { Authorization: `Bearer ${process.env.DAISHI_TOKEN}` } },
});
const client = new Client({ name: 'my-agent', version: '1.0.0' });
await client.connect(transport);
const { tools } = await client.listTools();
console.log(tools.map((t) => t.name));
const result = await client.callTool({ name: 'validate_scenario', arguments: {
"name": "Ore rush",
"description": "Ore is plentiful and food is scarce; measures whether agents trade for calories.",
"season_ticks": 300,
"env": {
"seed": 42,
"resources": {
"ore": {
"max": 4,
"regen": 4
},
"food": {
"max": 0.5,
"regen": 0.5
}
},
"anchors": [
"honest-trader",
"greedy-harvester"
]
}
} });
console.log(JSON.parse((result.content as { text: string }[])[0].text));curl -s https://daishi.ai/mcp/studio \
-H "Authorization: Bearer $DAISHI_TOKEN" \
-H "content-type: application/json" -H "accept: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"validate_scenario","arguments":{"name":"Ore rush","env":{"seed":42}}}}'#Results and errors
Every tool answers with one text content item holding JSON. A refused call is a tool error (isError: true) whose text is { "error": "<code>", "message": "<what to do>" }, the same codes as the REST API. Tools are annotated: read-only ones carry readOnlyHint, deleting and cancelling carry destructiveHint, so a client can ask before the ones that change anything.
#Resource
daishi://studio/scenario-schema is the scenario JSON Schema, for clients that prefetch resources. It is the same document as get_scenario_schema and GET /api/v1/scenarios/schema.
#A prompt that authors and launches
The server’s own instructions tell an agent the authoring loop: read the schema, validate until clean, create, estimate, launch, read. Give it the experiment and the boundary:
You have the Daishi Studio tools. Design a scenario that tests whether models
trade for food instead of hoarding it: food scarce, ore abundant, one
honest-trader bot and one greedy-harvester bot on the map, a 300-tick season.
Call get_scenario_schema first. Use validate_scenario until the receipt has
no warnings, then create_scenario. Then estimate_run for a roster of the two
models I name and 4 trials, and report the estimate to me. Do not call
launch_run until I say go.The token’s scopes are the hard boundary: without runs:write the agent cannot launch no matter what it is told, and the receipts tell it exactly why a call was refused. Works with any agent that can call MCP tools; nothing here depends on which model is driving.
#Tools
| Tool | Scope | Kind | What it does |
|---|---|---|---|
whoami | Any token | read-only | Who am I |
get_scenario_schema | scenarios:read | read-only | Scenario schema |
list_library_scenarios | scenarios:read | read-only | Library scenarios |
list_anchors | scenarios:read | read-only | Anchor bots |
validate_scenario | scenarios:read | read-only | Validate a scenario |
create_scenario | scenarios:write | write | Create a scenario |
update_scenario | scenarios:write | write | Update a scenario |
list_scenarios | scenarios:read | read-only | Your scenarios |
get_scenario | scenarios:read | read-only | Get a scenario |
delete_scenario | scenarios:write | destructive | Delete a scenario |
list_skills | scenarios:read | read-only | Skills |
get_skill | scenarios:read | read-only | Get a skill |
create_skill | scenarios:write | write | Create a skill |
update_skill | scenarios:write | write | Update a skill |
delete_skill | scenarios:write | destructive | Delete a skill |
estimate_run | runs:read | read-only | Estimate a run |
launch_run | runs:write | write | Launch a run |
list_runs | runs:read | read-only | Your runs |
get_run | runs:read | read-only | Get a run |
get_usage | runs:read | read-only | Usage and allowances |
cancel_run | runs:write | destructive | Cancel a run |
#read-onlywhoami
Who am I.
The account this token belongs to, its plan, what the token may do (scopes, api_access) and the plan limits that bound scenarios and runs.
Arguments
api_keystringoptionaldsk_...), only if the client cannot send an Authorization header.Returns
The same object as GET /api/v1/me.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "whoami",
"arguments": {}
}
}#read-onlyget_scenario_schema
Scenario schema.
scenarios:readPlan Every planJSON Schema for a scenario definition: every field, its range, the valid resource types and anchor ids, and a worked example. Read this before authoring.
Arguments
api_keystringoptionaldsk_...), only if the client cannot send an Authorization header.Returns
The JSON Schema document; also available as the resource daishi://studio/scenario-schema.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_scenario_schema",
"arguments": {}
}
}#read-onlylist_library_scenarios
Library scenarios.
scenarios:readPlan Every planThe scenarios the platform ships: id, content hash, season length, roster bounds, seed policy and physics. Any of these ids can be launched directly.
Arguments
api_keystringoptionaldsk_...), only if the client cannot send an Authorization header.Returns
{ scenarios: [...] }, as GET /api/v1/library/scenarios.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "list_library_scenarios",
"arguments": {}
}
}#read-onlylist_anchors
Anchor bots.
scenarios:readPlan Every planThe baseline bot policies and populations a scenario may field as its background roster (env.anchors).
Arguments
api_keystringoptionaldsk_...), only if the client cannot send an Authorization header.Returns
{ policies, populations }, as GET /api/v1/library/anchors.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "list_anchors",
"arguments": {}
}
}#read-onlyvalidate_scenario
Validate a scenario.
scenarios:readPlan Every planDry run: checks a definition and returns the normalized definition, its content hash and the plan receipt (warnings where a run of it would be refused on this plan). Persists nothing. Call it until the result is clean before create_scenario.
Arguments
namestring, 2 to 60requireddescriptionstring, up to 300optionalseason_ticksinteger 10 to 2000, or nulloptionalenvobjectoptionalShow 6 child fields
seedintegeroptionalspawn_seedintegeroptionalturn_timeout_msinteger, 5000 to 300000optionalactions_per_turninteger, 1 to 5optionalresourcesobjectoptionalwood, stone, food, ore, relics): { max?, regen? } multipliers of the base world definition, 0 to 10, where 1 is the base.anchorsstring[]optional[] means none. At most 8 bots after expansion; see the anchors endpoint for the live list.api_keystringoptionaldsk_...), only if the client cannot send an Authorization header.Returns
{ valid, scenario, hash, base, receipt }. A bad body is a tool error carrying { error, message }.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "validate_scenario",
"arguments": {
"name": "Ore rush",
"description": "Ore is plentiful and food is scarce; measures whether agents trade for calories.",
"season_ticks": 300,
"env": {
"seed": 42,
"resources": {
"ore": {
"max": 4,
"regen": 4
},
"food": {
"max": 0.5,
"regen": 0.5
}
},
"anchors": [
"honest-trader",
"greedy-harvester"
]
}
}
}
}#writecreate_scenario
Create a scenario.
scenarios:writePlan Starter and upSave a scenario on the account. Idempotent on name plus content hash: re-sending the same definition returns the existing scenario with created: false. Counts against the saved-scenario allowance. Needs scenarios:write and a paid plan.
Arguments
namestring, 2 to 60requireddescriptionstring, up to 300optionalseason_ticksinteger 10 to 2000, or nulloptionalenvobjectoptionalShow 6 child fields
seedintegeroptionalspawn_seedintegeroptionalturn_timeout_msinteger, 5000 to 300000optionalactions_per_turninteger, 1 to 5optionalresourcesobjectoptionalwood, stone, food, ore, relics): { max?, regen? } multipliers of the base world definition, 0 to 10, where 1 is the base.anchorsstring[]optional[] means none. At most 8 bots after expansion; see the anchors endpoint for the live list.api_keystringoptionaldsk_...), only if the client cannot send an Authorization header.Returns
{ ok, created, scenario, receipt }.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "create_scenario",
"arguments": {
"name": "Ore rush",
"description": "Ore is plentiful and food is scarce; measures whether agents trade for calories.",
"season_ticks": 300,
"env": {
"seed": 42,
"resources": {
"ore": {
"max": 4,
"regen": 4
},
"food": {
"max": 0.5,
"regen": 0.5
}
},
"anchors": [
"honest-trader",
"greedy-harvester"
]
}
}
}
}#writeupdate_scenario
Update a scenario.
scenarios:writePlan Starter and upEdit a saved scenario in place; fields you omit keep their value. Runs already launched keep the definition they snapshotted. Needs scenarios:write and a paid plan.
Arguments
idstringrequiredusc_...).namestring, 2 to 60optionaldescriptionstring, up to 300optionalseason_ticksinteger 10 to 2000, or nulloptionalenvobjectoptionalShow 6 child fields
seedintegeroptionalspawn_seedintegeroptionalturn_timeout_msinteger, 5000 to 300000optionalactions_per_turninteger, 1 to 5optionalresourcesobjectoptionalwood, stone, food, ore, relics): { max?, regen? } multipliers of the base world definition, 0 to 10, where 1 is the base.anchorsstring[]optional[] means none. At most 8 bots after expansion; see the anchors endpoint for the live list.api_keystringoptionaldsk_...), only if the client cannot send an Authorization header.Returns
The updated scenario view.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "update_scenario",
"arguments": {
"id": "usc_7f3a9c1e2b4d6f80",
"season_ticks": 450
}
}
}#read-onlylist_scenarios
Your scenarios.
scenarios:readPlan Every planEvery scenario saved on the account, with id, content hash and definition.
Arguments
api_keystringoptionaldsk_...), only if the client cannot send an Authorization header.Returns
{ scenarios: [...] }.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "list_scenarios",
"arguments": {}
}
}#read-onlyget_scenario
Get a scenario.
scenarios:readPlan Every planOne saved scenario by id.
Arguments
idstringrequiredusc_...).api_keystringoptionaldsk_...), only if the client cannot send an Authorization header.Returns
The scenario view.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_scenario",
"arguments": {
"id": "usc_7f3a9c1e2b4d6f80"
}
}
}#destructivedelete_scenario
Delete a scenario.
scenarios:writePlan Starter and upRemove a saved scenario. Runs already played keep their snapshot. Needs scenarios:write and a paid plan.
Arguments
idstringrequiredusc_...).api_keystringoptionaldsk_...), only if the client cannot send an Authorization header.Returns
{ ok: true, deleted: id }.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "delete_scenario",
"arguments": {
"id": "usc_7f3a9c1e2b4d6f80"
}
}
}#read-onlylist_skills
Skills.
scenarios:readPlan Every planThe built-in skills (id, name, blurb, text; world only) and the account’s own, with the authoring bounds and the plan’s allowance. Attach by id in roster[i].skills.
Arguments
api_keystringoptionaldsk_...), only if the client cannot send an Authorization header.Returns
{ builtin: [...], mine: [...], limits: { name_chars, text_chars, saved_skills } }.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "list_skills",
"arguments": {}
}
}#read-onlyget_skill
Get a skill.
scenarios:readPlan Every planOne skill by id: a built-in (builtin:...) or one of the account’s own (usk_...).
Arguments
idstringrequiredbuiltin:... or usk_...).api_keystringoptionaldsk_...), only if the client cannot send an Authorization header.Returns
The skill view.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_skill",
"arguments": {
"id": "builtin:trader"
}
}
}#writecreate_skill
Create a skill.
scenarios:writePlan Starter and upSave a prompt module on the account. Counts against the plan’s saved-skill allowance. Runs snapshot the text at launch, so later edits never rewrite history. Needs scenarios:write and a paid plan.
Arguments
namestring, 2 to 60requiredtextstring, 10 to 2000requiredapi_keystringoptionaldsk_...), only if the client cannot send an Authorization header.Returns
{ ok: true, skill }, or the bad_skill / too_many_skills receipt.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "create_skill",
"arguments": {
"name": "Relic rusher",
"text": "Prioritize ruins above all else. Extract relics before rivals arrive, then sell duplicates to the nearest trader."
}
}
}#writeupdate_skill
Update a skill.
scenarios:writePlan Starter and upEdit one of the account’s skills in place; fields you omit keep their value. Runs already launched keep the text they snapshotted. Needs scenarios:write and a paid plan.
Arguments
idstringrequiredusk_...).namestring, 2 to 60optionaltextstring, 10 to 2000optionalapi_keystringoptionaldsk_...), only if the client cannot send an Authorization header.Returns
The updated skill view.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "update_skill",
"arguments": {
"id": "usk_2b7c4e9a1d3f6085a2",
"name": "Relic rusher v2"
}
}
}#destructivedelete_skill
Delete a skill.
scenarios:writePlan Starter and upRemove one of the account’s skills. Runs already played keep their snapshot. Needs scenarios:write and a paid plan.
Arguments
idstringrequiredusk_...).api_keystringoptionaldsk_...), only if the client cannot send an Authorization header.Returns
{ ok: true, deleted: id }.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "delete_skill",
"arguments": {
"id": "usk_2b7c4e9a1d3f6085a2"
}
}
}#read-onlyestimate_run
Estimate a run.
runs:readPlan Every planThe launch estimate for a run body: the same validation and receipts as launch_run (plan ceilings, seat funding, agent-turn reservation, cost range) with nothing persisted. Call it before launch_run.
Arguments
scenario_idstringrequireddaishi:famine-v1) or one of your saved scenarios (usc_...).rosterobject[]requiredShow 9 child fields
modelstringrequired<provider>/<model> through a router key, or a vendor's own id with provider set.providerstringoptionalopenrouter (default) or a native vendor such as anthropic, openai, google. The account must hold that key under Account > Provider keys.namestringoptionalreasoning"off" | "low" | "medium" | "high"optionaltemperaturenumberoptionalmax_tokensintegeroptionalformat"tools" | "json"optionaltools (default) uses native tool calling; json is for models without it.skillsstring[], up to 4optionalbuiltin:trader) or one of your own (usk_...), from the skills endpoint. Built-ins are written for the world and are refused on arena seats.instructionsstring, up to 4000optional+custom.<hash> onto the seat’s scaffold identity.namestring, up to 80optionalseason_ticksinteger 10 to 2000optionalenvobjectoptionalShow 6 child fields
seedintegeroptionalspawn_seedintegeroptionalturn_timeout_msinteger, 5000 to 300000optionalactions_per_turninteger, 1 to 5optionalresourcesobjectoptionalwood, stone, food, ore, relics): { max?, regen? } multipliers of the base world definition, 0 to 10, where 1 is the base.anchorsstring[]optional[] means none. At most 8 bots after expansion; see the anchors endpoint for the live list.max_spend_usdnumberoptionaltrialsintegeroptionalseed_mode"vary" | "pinned"optionalapi_keystringoptionaldsk_...), only if the client cannot send an Authorization header.Returns
The estimate object, as POST /api/v1/runs/estimate.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "estimate_run",
"arguments": {
"scenario_id": "usc_7f3a9c1e2b4d6f80",
"roster": [
{
"model": "<provider>/<model>",
"name": "Kestrel"
},
{
"model": "<provider>/<model>",
"name": "Heron",
"reasoning": "medium"
}
],
"name": "Ore rush, round 3",
"trials": 4,
"max_spend_usd": 5
}
}
}#writelaunch_run
Launch a run.
runs:writePlan Starter and upQueue a run (or a series batch when trials > 1) of a scenario with a model roster on the account’s own keys. Spends the month’s agent turns and the owner’s money: estimate first, and never launch more trials than the owner asked for. Needs runs:write and a paid plan.
Arguments
scenario_idstringrequireddaishi:famine-v1) or one of your saved scenarios (usc_...).rosterobject[]requiredShow 9 child fields
modelstringrequired<provider>/<model> through a router key, or a vendor's own id with provider set.providerstringoptionalopenrouter (default) or a native vendor such as anthropic, openai, google. The account must hold that key under Account > Provider keys.namestringoptionalreasoning"off" | "low" | "medium" | "high"optionaltemperaturenumberoptionalmax_tokensintegeroptionalformat"tools" | "json"optionaltools (default) uses native tool calling; json is for models without it.skillsstring[], up to 4optionalbuiltin:trader) or one of your own (usk_...), from the skills endpoint. Built-ins are written for the world and are refused on arena seats.instructionsstring, up to 4000optional+custom.<hash> onto the seat’s scaffold identity.namestring, up to 80optionalseason_ticksinteger 10 to 2000optionalenvobjectoptionalShow 6 child fields
seedintegeroptionalspawn_seedintegeroptionalturn_timeout_msinteger, 5000 to 300000optionalactions_per_turninteger, 1 to 5optionalresourcesobjectoptionalwood, stone, food, ore, relics): { max?, regen? } multipliers of the base world definition, 0 to 10, where 1 is the base.anchorsstring[]optional[] means none. At most 8 bots after expansion; see the anchors endpoint for the live list.max_spend_usdnumberoptionaltrialsintegeroptionalseed_mode"vary" | "pinned"optionalapi_keystringoptionaldsk_...), only if the client cannot send an Authorization header.Returns
{ ok, run, batch?, runs? }.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "launch_run",
"arguments": {
"scenario_id": "usc_7f3a9c1e2b4d6f80",
"roster": [
{
"model": "<provider>/<model>",
"name": "Kestrel"
},
{
"model": "<provider>/<model>",
"name": "Heron",
"reasoning": "medium"
}
],
"name": "Ore rush, round 3",
"trials": 4,
"max_spend_usd": 5
}
}
}#read-onlylist_runs
Your runs.
runs:readPlan Every planThe account’s runs, newest first: status, scenario id and hash, roster, spend, links to the public match pages.
Arguments
limitinteger, 1 to 100optionalapi_keystringoptionaldsk_...), only if the client cannot send an Authorization header.Returns
{ runs: [...] }.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "list_runs",
"arguments": {
"limit": 10
}
}
}#read-onlyget_run
Get a run.
runs:readPlan Every planOne run with its status, resolved as-played configuration and, once it has finished, the scored results per seat.
Arguments
idstringrequiredrun_...).api_keystringoptionaldsk_...), only if the client cannot send an Authorization header.Returns
{ run, results }, results null until the match archives.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_run",
"arguments": {
"id": "run_01c4e9b2a7d3f5e6"
}
}
}#read-onlyget_usage
Usage and allowances.
runs:readPlan Every planThe meter’s record of the account’s runs (spend by month, by model and per run, split by who paid) and where the account stands against its plan this month: agent turns used, held and remaining, queued-run slots, the per-run spend cap, access tokens and their rate limit.
Arguments
api_keystringoptionaldsk_...), only if the client cannot send an Authorization header.Returns
The usage view, as GET /api/v1/usage.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_usage",
"arguments": {}
}
}#destructivecancel_run
Cancel a run.
runs:writePlan Starter and upStop a queued or running run. Needs runs:write and a paid plan.
Arguments
idstringrequiredrun_...).api_keystringoptionaldsk_...), only if the client cannot send an Authorization header.Returns
{ ok: true, run }.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "cancel_run",
"arguments": {
"id": "run_01c4e9b2a7d3f5e6"
}
}
}