Skip to content

External Capabilities

Anything that isn't code in the repository — an events API, a real-persona pool — is a declared capability, not hidden wiring. Capabilities are indexed in one committed, public file; endpoints and keys live somewhere else entirely; and every study records, by name, which ones it actually used.

The capabilities registry

resources/capabilities.yaml is the public index. It is committed and open-source on purpose: an entry advertises that a capability exists, when to reach for it, and how to proceed without it.

field meaning
name unique id. For kind: mcp it must equal the server name in .mcp.json
kind mcp | http_client | builtin
provides one line: what it gives a study
stages which sv-* skills consult it (capability_view filters on this)
use_when the trigger condition a skill matches the study against
confirm true = calling it spends the collaborator's budget or sends data out → ask first
cost how the call is metered (see the nuance below)
available_when static check: {mcp_server: <name>} or {env: [VARS…]}
health the live probe to run right before relying on it
howto how a skill applies it, including materialization + provenance rules
fallback what to do when it is unavailable — the offline / open-source path
attach how someone with access registers the private endpoint

The build skills read this through skills.sv_workspace.capability_view(stage=…), which returns each entry verbatim plus two resolved fields:

resolved field type meaning
available bool did available_when pass in this environment?
availability_note str why — e.g. which env var or MCP server is missing

confirm and cost are different signals

confirm: true means the call spends the collaborator's budget or sends data out, so the workflow stops and asks. cost describes metering. They are independent. The bundled event_tool has confirm: false and a cost: it is cheap and read-only, so the workflow just uses it and reports at the stage gate — but on the hosted service every fetch is still metered against your quota. Never read confirm: false as "free".

Three invariants hold for every entry:

  • Endpoints and keys are never in the registry. kind: mcp resolves through the gitignored .mcp.json; kind: http_client through env vars in the gitignored .env.
  • A study pins what it used by NAME in its own resources.json (McpServerDecl) — reproducible provenance, no private endpoint leaking into a committed artifact.
  • Adding a service is one registry entry and zero skill edits. Each skill carries one stable, generic capability-check hook; the skill text never mentions a specific service.

Configuring services: .env is the single source

Service keys live in one gitignored file, socioverse-beta/.env (start from .env.example). .mcp.json is generated from it:

python scripts/gen_mcp_json.py

The renderer reads SV_EVENT_API_URL / SV_EVENT_API_KEY and SV_USER_POOL_MCP_URL / SV_USER_POOL_MCP_KEY and writes the corresponding mcpServers entries. Edit .env, re-run the script — never hand-edit .mcp.json, and never commit either file. See Installation for the setup path.

event_tool — real-world event & macro context for E

How event_tool anchors E: 20 structured sources → the MCP's three tools → monthly environment snapshots, with grounding provenance throughout

kind http_client
stages sv-init, sv-build-environment
provides structured macro / news / market context for E (FRED, census, monthly news archives)
confirm false — but metered; see the nuance above
configured by SV_EVENT_API_URL, SV_EVENT_API_KEY, optional SV_EVENT_LOCAL_CACHE

sv-init's grounding bootstrap defers structured facts to this service; sv-build-environment materializes them into the study.

Three things people get wrong

  1. SV_EVENT_API_URL is the full MCP endpoint, path included — e.g. http://host:9997/event_mcp, not a bare host. The production service speaks JSON-RPC over the MCP wire (tools/call get_data) with an Authorization: Bearer header. No MCP SDK is required.
  2. The health probe is at the host root, /healthz, off the MCP path. The client fail-fasts on it before doing any per-source fetching.
  3. months= is mandatory. The production service has no query parser — the agent layer was deliberately dropped. Always pass an explicit months= list, and usually sources= too. query is carried for provenance only.

The API

from socioverse import materialize_external_events, month_range

path, ev = materialize_external_events(
    study_dir,
    query="US inflation and consumer sentiment, 2024",   # provenance only
    months=month_range("2024-01", "2024-12"),            # REQUIRED
    sources=["fred", "nyt"],
)

materialize_external_events(...) writes environment/external_events.json and returns (path, bundle). month_range(start, end) builds an inclusive "YYYY-MM" list (capped at 48).

Bundle fields worth knowing:

field meaning
provider remote | local_cache | unavailable
available property: provider != "unavailable"
results the payload, keyed "<source>_<YYYY-MM>"
requested_months / requested_sources what you asked for
errors per-tier failure records — read these when a tier degraded
summary, web_evidence legacy. Do not author from these.

Author broadcasts from ev.results, never from ev.summary

summary (and web_evidence) carry another model's prose. Injecting that text into E means the behavior you observe is partly a response to a third-party LLM's framing of the world, not to the world — it contaminates the attribution of B = f(P, E) and quietly invalidates the study. Read the typed numbers out of ev.results and write the broadcast yourself.

The fallback chain

The client degrades one tier at a time and never raises:

  1. remote — the Event service, when SV_EVENT_API_URL is set and /healthz is ok;
  2. local_cacheSV_EVENT_LOCAL_CACHE, laid out <source>/<YYYY-MM>.json, for offline work;
  3. unavailable — at which point you are tier 3: plain web search by hand, hand-authored broadcasts, and every number recorded in the grounding sidecar with via="web_search" plus url and accessed date.

A study built without the service is still grounded — the ledger just records the humbler basis. See Grounding & Provenance for how those facts are recorded.

user_pool_survey_mcp — real platform personas for P

How the user pool anchors P: a real-platform pool, sampling to target marginals, parallel LLM role-play answering — served as an MCP

kind mcp
stages sv-build-population, sv-report
provides real X/Twitter and 小红书 user personas sampled to target marginals, plus LLM questionnaire answering over them
confirm true — each call spends the collaborator's LLM budget and sends the questionnaire out
configured by SV_USER_POOL_MCP_URL + SV_USER_POOL_MCP_KEY (bearer auth) → .mcp.json

Use it when P should be grounded in real platform users rather than synthesized ones, or when the study wants a cross-sectional questionnaire baseline next to the longitudinal panel.

Build-time only, through the in-session MCP tools:

  1. Health-check with get_user_pool_simulation_capabilities (get_user_pool_index_status for the index).
  2. Call run_user_pool_distribution_simulation(platform_id, distributions, user_count, questionnaire, seed, max_workers)max_workers ≤ 128. Platform ids: x_mode_1 (X/Twitter), rednote_v2 (小红书).

allow_virtual_users defaults to true

If a target distribution cell has no matching real user, the service silently substitutes VIRTUAL_* personas to fill it. If you need real users, pass allow_virtual_users=false and inspect the returned sample for gaps — a coverage hole is a finding about your population, not something to paper over.

Materializing the result:

  • write the returned output_jsonl into population/ artifacts (personas + roster) with the P-side seam provider_ref="mcp.socioverse_pool", or into a report baseline table;
  • record one grounding fact per load-bearing distribution, via="provider", source = platform + query;
  • keep the returned llm_model in the provenance — the answers are model-dependent.

Cost scales with user_count

A distribution simulation runs one LLM questionnaire answer per simulated respondent. Cost is roughly user_count × per-respondent rate, and on the hosted service it is metered against your sim-plane quota — see Quotas & billing. Because confirm: true, state the expected spend (the user_count, and that it draws down the balance) and get an explicit yes before calling.

Fallback: synthesize personas under the grounding-first rules (searched anchors, or declared assumptions), or load a user-supplied file (provider_ref="file.personas"), and note in the provenance that the pool was unavailable.

See also