Skip to content

MCP server

The Model Context Protocol door at mcp.cuvo.co, the tools behind it, and how an agent authenticates.

The MCP server is the same API, addressed the way an agent addresses things. It is a thin client of /v1: every tool is one published operation, made with your own credential, subject to the same scopes, modes, grants and audit.

https://mcp.cuvo.co/mcp

Streamable HTTP, stateless, on GET, POST and DELETE. There is no separate SSE endpoint.

Authenticating

The door takes the same bearer credential as the REST API: an API key, or an OAuth access token. See Authentication.

{
  "mcpServers": {
    "cuvo": {
      "url": "https://mcp.cuvo.co/mcp",
      "headers": { "Authorization": "Bearer cuvo_sk_test_…" }
    }
  }
}

An MCP client that has never seen Cuvo can discover the rest. An unauthenticated request answers 401 with WWW-Authenticate: Bearer resource_metadata="https://mcp.cuvo.co/.well-known/oauth-protected-resource", which points at the authorization server. Dynamic client registration is open and unauthenticated, with PKCE required, because an MCP client has no other way in. A registered client holds nothing until a portal member consents to it.

A user-consented token is always a test mode principal. An agent that signed a person in picks the integration and the scopes at the consent screen and reaches the sandbox. Live data needs a live key or a live client, exactly as it does through the API.

Every credential used here must hold organization:read. That is the scope that names which organization the principal acts for, the door resolves one before any tool runs, and a credential without it cannot open a session at all. Everything beyond that is admitted per tool: each tool's own scope is checked against the credential's effective scopes, and its modes against the credential's mode.

The tools

Forty-six, in three groups.

build, for writing the integration. docs_search searches these guides and spec_lookup reads the published contract; both answer from this service, spend no credential and need no scope. The other fourteen are the simulated clinician and pharmacy: test_approve_case, test_decline_case, test_request_info, test_clinician_message, test_ship_order, test_deliver_order, test_block_order, test_fail_order, test_start_visit, test_complete_visit, test_no_show_visit, test_trigger_webhook, test_set_autopilot, test_reset. All need test:manage and are refused to a live credential.

operate, for running it: usage_get, invoices_list (billing:read), webhook_endpoints_list, webhook_deliveries_list, events_redeliver (webhooks:manage), events_list (events:read).

runtime, the consult itself: organization_get, catalog_medications, catalog_pharmacies, catalog_states, patients_create, patients_list, patients_get, consents_create, consents_list, cases_create, cases_list, cases_get, cases_cancel, cases_release_hold, messages_list, messages_send, orders_list, orders_get, visits_slots, visits_create, visits_get, visits_cancel, visits_reschedule, visits_join_link. Each carries the scope its operation carries, and all of them work in both modes.

tools/list returns only the tools your credential's scopes and mode admit, so an agent is never offered something it will be refused. A call outside scope is a JSON-RPC error, and it is audited like any other refusal.

Every tool carries MCP annotations, derived from the operation it names rather than written beside it. readOnlyHint is true for a tool that only reads, idempotentHint is true when calling a tool twice is calling it once, destructiveHint is true for a tool that cancels, decides, deletes or redelivers, and openWorldHint is false on all of them, because every tool talks to this one API. They are hints, for a client deciding how much confirmation to ask a person for. What a credential may actually do is still its scopes and its mode, checked here and again at the API door.

A list tool answers the same envelope the REST list does: data plus has_more, with limit and starting_after to page it. See Pagination.

The resources

The guides and the contract are also MCP resources, so a client can attach one to a conversation rather than spend a tool call on it.

  • cuvo://docs/<slug> is one guide, as markdown, under the title and summary the developer portal gives it. This page is cuvo://docs/mcp.
  • cuvo://openapi/v1.yaml is the published contract, the same bytes /docs/openapi.yaml serves.

resources/list returns every one of them to every session. They need no scope of their own: the organization:read your credential already holds to open the door is the whole requirement. A read is audited the way a tool call is, with the URI where the tool name goes, and a URI nothing is published at is a JSON-RPC error and an audited refusal.

docs_search stays. A resource list is a table of contents, and an agent still has to find the paragraph.

What is deliberately absent

No tool mints, rotates or reveals a credential. No tool creates or deletes a webhook endpoint or rotates a signing secret. Those stay in the developer portal behind a fresh multi-factor challenge, because an agent holding a bearer token should not be able to mint a second one.

test_decide_case is three tools rather than one, because the decision is a discriminated union and a single tool would have meant a nested decision.decision.

Audit

Every request through the door writes the same audit row a REST call writes. A tool call writes a second row naming the tool, and a resource read writes one naming the URI. Nothing an agent does is invisible to the integration that owns the credential; the portal's audit log shows both.

A first session

Ask the agent to call spec_lookup with no arguments for the operation index, docs_search for the guide it needs, then organization_get to confirm which organization and mode it is in. From there patients_create, consents_create, cases_create and test_approve_case drive a whole consult.

The published skill at /skills/cuvo-api/SKILL.md says the same thing in the form an agent loads directly.