MCP Servers
8 operations under /v1/mcp-servers (GET POST PATCH DELETE): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.
Base URL: https://api.codespar.dev
Every operation below requires a Bearer token. See Authentication.
GET /v1/mcp-servers
https://api.codespar.dev/v1/mcp-serversList this project's MCP servers
Every server in the calling credential's organization AND project, newest first. There is no pagination and no filter: the whole set comes back in one response.
The rows carry no tools array. Read one server to get its priced tools.
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
mcp_servers | array of object | yes | — |
curl -X GET https://api.codespar.dev/v1/mcp-servers \
-H "Authorization: Bearer $CODESPAR_API_KEY"GET /v1/mcp-servers HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEYimport os
import requests
res = requests.get(
"https://api.codespar.dev/v1/mcp-servers",
headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()const res = await fetch("https://api.codespar.dev/v1/mcp-servers", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
},
});
const data = await res.json();const result = await cs.api.get("/v1/mcp-servers");{
"mcp_servers": [
{
"id": "obj_0000000000000000",
"project_id": "prj_0000000000000000",
"slug": "example",
"name": "Example",
"description": "string",
"overview_md": "string",
"category": "string",
"upstream_url": "https://example.com/hook",
"environment": "live",
"payto_kind": "provisioned",
"payto_address": "string",
"consumer_id": "csm_0000000000000000",
"platform_fee_bps": 0,
"tool_count": 1,
"active": true,
"gateway_url": "https://example.com/hook",
"gateway_unavailable_reason": "gateway_origin_not_configured",
"tools": [
{
"id": "obj_0000000000000000",
"tool_name": "Example",
"description": "string",
"pricing_model": "string",
"price": "string",
"price_atomic": "string",
"active": true
}
],
"created_at": "2026-01-15T12:00:00.000Z",
"last_paid_call_at": "2026-01-15T12:00:00.000Z"
}
]
}POST /v1/mcp-servers
https://api.codespar.dev/v1/mcp-serversRegister a monetized MCP server and price its tools
Creates the server, provisions the wallet that will receive its revenue, and writes one priced row per entry in tools. At least one tool is required and at most 200. Each price is a positive USDC decimal string with up to six decimals; a value outside that shape is refused with invalid_body rather than rounded.
WHERE THE MONEY LANDS. consumer_id selects the provisioned CDP wallet that receives settlement. The address is derived for that value, and created if it does not exist yet, so a mistyped consumer_id produces a working server pointing at a DIFFERENT wallet rather than a validation error. Bring-your-own addresses are not offered here.
platform_fee_bps is stored on the row and applied later by settlement. It is not charged by this call and changing it is not possible through the patch operation.
upstream_auth, when given, is encrypted into the vault and never returned by any read on this resource.
TWO GOTCHAS ON THE 400s. A slug already taken by ANY organization returns slug_conflict with status 400, not 409, because the slug namespace is global. And that same code is what a tools array repeating a tool_name produces: the handler classifies a unique-constraint violation by its SQL state alone and cannot tell the two constraints apart, so read slug_conflict as a uniqueness constraint was violated and check both.
503 payto_provisioning_unavailable covers every failure of the wallet step, including a project with no Coinbase CDP credential connected. Nothing is written when it is returned.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
category | string | no | — |
consumer_id | string | yes | — |
description | string | no | — |
name | string | yes | — |
overview_md | string | no | — |
platform_fee_bps | integer | no | — |
slug | string | yes | — |
tools | array of object | yes | — |
upstream_auth | object | no | — |
upstream_url | string (uri) | yes | — |
Responses
| Status | Body | Description |
|---|---|---|
201 | object | OK |
400 | object | invalid_body for a body that failed the schema (details.issues) or a tool price that is not a positive six-decimal USDC amount; invalid_upstream_url for a URL refused by the address guard; slug_conflict for a duplicate slug or a repeated tool name. |
403 | object | Forbidden. The operation requires the admin role in the organization, resolved from the acting user forwarded in x-codespar-user. Known values of error: insufficient_role, bearer_admin_role_missing, bearer_admin_user_not_member, bearer_admin_role_unresolved. The same status and body shape also carry the scope gate's refusals, which reach every route in this subtree including the two reads: forbidden when the credential lacks the scope the route is mapped to, and the two alarms scopes_unresolved and route_scope_unresolved. |
503 | object | The receiving CDP address could not be provisioned. The server was not created; retry after connecting Coinbase CDP for this project. |
Response 201
| Field | Type | Required | Description |
|---|---|---|---|
active | boolean | yes | — |
category | string,null | yes | — |
consumer_id | string,null | yes | — |
created_at | string (date-time) | yes | — |
description | string | yes | Listing card summary. Empty string when none was supplied. |
environment | "live" | "test" | yes | Taken from the calling credential's environment at create time. There is no body field for it. |
gateway_unavailable_reason | "gateway_origin_not_configured" | "gateway_origin_is_production" | yes | Why the gateway URL is null: this deployment is not production and names no gateway origin of its own (gateway_origin_not_configured), or names a production host (gateway_origin_is_production). Null when the URL is present. Production's gateway is never used as a fallback outside production. |
gateway_url | string,null | yes | <gateway origin>/mcp/<slug> (production: https://gw.codespar.dev/mcp/<slug>), rebuilt from the slug on every read rather than stored. Null where this deployment has no gateway; see gateway_unavailable_reason. |
id | string | yes | mcs_ followed by a nanoid. |
last_paid_call_at | string,null (date-time) | no | When the server's last paid call settled over x402, with no window: the latest sale GET /v1/gate/stats attributes to this server, by the same rule. Null when it never sold. A Mode A (mandate) call settles through the consumer wallet and is not a sale here. Present on the list and on the single read; absent from create and patch. |
name | string | yes | — |
overview_md | string | yes | Listing page body, markdown. Empty string when none was supplied. |
payto_address | string | yes | The address that receives USDC for calls settled through the gateway. |
payto_kind | "provisioned" | "split" | yes | Create always writes provisioned, a CodeSpar-derived CDP wallet. split exists in the column but no operation on this surface produces it, and bring-your-own is not offered. |
platform_fee_bps | integer | yes | Basis points of each settled call accrued as a platform fee, 0 to 10000. Stored at create and applied by settlement, not by this resource. |
project_id | string | yes | — |
slug | string | yes | Lowercase letters, digits, underscore or dash, up to 64 characters. The slug namespace is GLOBAL across all tenants, so a slug another organization already holds is refused. |
tool_count | integer | yes | The number of tools supplied at create time. Nothing recomputes it, so deactivating a tool leaves it unchanged and it is not a count of ACTIVE tools. |
tools | array of object | no | Present on create, on the single read, and on the listing patch. ABSENT from the list response, which returns servers without their tools. |
upstream_url | string | yes | — |
curl -X POST https://api.codespar.dev/v1/mcp-servers \
-H "Authorization: Bearer $CODESPAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"slug": "example",
"name": "Example",
"description": "string",
"overview_md": "string",
"category": "string",
"upstream_url": "https://example.com/hook",
"consumer_id": "csm_0000000000000000",
"upstream_auth": {
"header": "string",
"value": "string"
},
"platform_fee_bps": 0,
"tools": [
{
"tool_name": "Example",
"description": "string",
"price": "string"
}
]
}'POST /v1/mcp-servers HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json
{
"slug": "example",
"name": "Example",
"description": "string",
"overview_md": "string",
"category": "string",
"upstream_url": "https://example.com/hook",
"consumer_id": "csm_0000000000000000",
"upstream_auth": {
"header": "string",
"value": "string"
},
"platform_fee_bps": 0,
"tools": [
{
"tool_name": "Example",
"description": "string",
"price": "string"
}
]
}import os
import requests
res = requests.post(
"https://api.codespar.dev/v1/mcp-servers",
headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
json={
"slug": "example",
"name": "Example",
"description": "string",
"overview_md": "string",
"category": "string",
"upstream_url": "https://example.com/hook",
"consumer_id": "csm_0000000000000000",
"upstream_auth": {
"header": "string",
"value": "string"
},
"platform_fee_bps": 0,
"tools": [
{
"tool_name": "Example",
"description": "string",
"price": "string"
}
]
},
)
res.raise_for_status()
data = res.json()const res = await fetch("https://api.codespar.dev/v1/mcp-servers", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"slug": "example",
"name": "Example",
"description": "string",
"overview_md": "string",
"category": "string",
"upstream_url": "https://example.com/hook",
"consumer_id": "csm_0000000000000000",
"upstream_auth": {
"header": "string",
"value": "string"
},
"platform_fee_bps": 0,
"tools": [
{
"tool_name": "Example",
"description": "string",
"price": "string"
}
]
}),
});
const data = await res.json();const r = await cs.api.response("post", "/v1/mcp-servers", {
body: {
slug: "example",
name: "Example",
description: "string",
overview_md: "string",
category: "string",
upstream_url: "https://example.com/hook",
consumer_id: "csm_0000000000000000",
upstream_auth: {
header: "string",
value: "string"
},
platform_fee_bps: 0,
tools: [
{
tool_name: "Example",
description: "string",
price: "string"
}
]
}
});
// r.status is one of the documented statuses (200, 403),
// each with its own body shape in r.data; nothing here throws on 403.
if (r.ok) {
console.log(r.data);
}{
"id": "obj_0000000000000000",
"project_id": "prj_0000000000000000",
"slug": "example",
"name": "Example",
"description": "string",
"overview_md": "string",
"category": "string",
"upstream_url": "https://example.com/hook",
"environment": "live",
"payto_kind": "provisioned",
"payto_address": "string",
"consumer_id": "csm_0000000000000000",
"platform_fee_bps": 0,
"tool_count": 1,
"active": true,
"gateway_url": "https://example.com/hook",
"gateway_unavailable_reason": "gateway_origin_not_configured",
"tools": [
{
"id": "obj_0000000000000000",
"tool_name": "Example",
"description": "string",
"pricing_model": "string",
"price": "string",
"price_atomic": "string",
"active": true
}
],
"created_at": "2026-01-15T12:00:00.000Z",
"last_paid_call_at": "2026-01-15T12:00:00.000Z"
}POST /v1/mcp-servers/platform-fees/sweep
https://api.codespar.dev/v1/mcp-servers/platform-fees/sweepThis moves everything unswept for the calling organization to its provisioned platform wallet
Move accrued platform fees to the organization's wallet now
Settled tool calls on a server with platform_fee_bps above zero accrue a fee owed to the platform partner. This moves everything unswept for the calling organization to its provisioned platform wallet and marks those accruals swept. The body takes no fields.
ONE TRANSFER PER (ENVIRONMENT, SOURCE ACCOUNT) PAIR, NOT PER ACCOUNT. Accruals are grouped by the seller's source account AND the environment they were earned in, and each group is one on-chain transfer: live settles on Base, test on Base Sepolia. The source account name is derived from the organization and the seller's consumer_id and does not carry the environment, so ONE seller selling in both environments produces TWO transfers that repeat the same source in transfers, on two different chains. Do not read transfers as one row per seller, and do not sum a source expecting a single chain's movement.
A BACKGROUND JOB ALREADY DOES THIS, hourly by default. This endpoint is the manual trigger over the same core; it is not the only way fees move, so a zero result usually means the job got there first.
NOT IDEMPOTENT. Each call executes real transfers, so it is not safe to retry blindly. Concurrency is the one case that is handled for you: a per-organization lock is held for the pass, and a second caller arriving while one runs gets 409 sweep_in_progress and moves no money. A call that times out on the client side may still have transferred; re-read the result of the next sweep rather than repeating this one.
PARTIAL SUCCESS IS A 200. A group whose transfer failed appears in transfers with status: "failed" and tx: null, its accruals are left unswept for the next pass, and the response is still 200. swept and total_atomic count the successful transfers only, so compare them against transfers rather than assuming everything moved. total_atomic sums across environments and is therefore not a single chain's amount.
With nothing to sweep the response is swept: 0, total_atomic: "0", an empty transfers, and no partner_wallet.
503 payto_provisioning_unavailable is broader than its name: the handler maps EVERY error thrown by the pass to that one code, so it covers a database failure during the sweep as well as a platform wallet that could not be provisioned.
This operation needs the platform-fees:sweep scope, which the other seven do not.
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
403 | object | Forbidden. The operation requires the admin role in the organization, resolved from the acting user forwarded in x-codespar-user. Known values of error: insufficient_role, bearer_admin_role_missing, bearer_admin_user_not_member, bearer_admin_role_unresolved. The same status and body shape also carry the scope gate's refusals, which reach every route in this subtree including the two reads: forbidden when the credential lacks the scope the route is mapped to, and the two alarms scopes_unresolved and route_scope_unresolved. |
409 | object | Another sweep for this organization holds the lock. Nothing was transferred and nothing was marked swept; try again shortly. |
503 | object | The pass failed. Reported for any thrown error, including a platform wallet that could not be provisioned and a database failure mid-sweep. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
partner_wallet | string | no | Absent when there was nothing to sweep, because no address is resolved in that case. |
swept | integer | yes | How many TRANSFERS succeeded. It is not a count of accrual rows or of servers. |
total_atomic | string | yes | The sum of the successful transfers only, in USDC atomic units. It sums across environments, so it is not an amount that moved on any single chain. |
transfers | array of object | yes | — |
curl -X POST https://api.codespar.dev/v1/mcp-servers/platform-fees/sweep \
-H "Authorization: Bearer $CODESPAR_API_KEY"POST /v1/mcp-servers/platform-fees/sweep HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEYimport os
import requests
res = requests.post(
"https://api.codespar.dev/v1/mcp-servers/platform-fees/sweep",
headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()const res = await fetch("https://api.codespar.dev/v1/mcp-servers/platform-fees/sweep", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
},
});
const data = await res.json();const r = await cs.api.response("post", "/v1/mcp-servers/platform-fees/sweep");
// r.status is one of the documented statuses (200, 403),
// each with its own body shape in r.data; nothing here throws on 403.
if (r.ok) {
console.log(r.data);
}{
"swept": 0,
"total_atomic": "string",
"partner_wallet": "string",
"transfers": [
{
"source": "string",
"amount_atomic": "1000",
"tx": "string",
"status": "swept"
}
]
}POST /v1/mcp-servers/validate
https://api.codespar.dev/v1/mcp-servers/validateConnect to an upstream MCP server and list its tools
A dry run before create: CodeSpar opens a StreamableHTTP MCP session to upstream_url and returns its tools/list, so a seller can see what will be priced. Read only, and nothing is stored. auth, when given, is sent as a single request header to the upstream and is not persisted by this call.
AN UNREACHABLE UPSTREAM IS A 200, NOT AN ERROR. Connection and listing failures are reported in the body as valid: false with error and an empty tools, because the request itself succeeded. Only a malformed body or a rejected URL produce a 400.
upstream_url is checked before any connection is opened, against the address it RESOLVES to and not only its text, so a hostname the caller controls cannot be pointed at a private address. A rejected URL comes back as invalid_upstream_url with details.reason set to url_not_public_http, host_unresolvable or host_resolves_to_blocked_address.
Both the connect and the tool listing are bounded at eight seconds.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
auth | object | no | — |
upstream_url | string (uri) | yes | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | The body did not match the schema (invalid_body, with details.issues carrying the validation issues), or the URL was refused by the address guard (invalid_upstream_url, with details.upstream_url and details.reason). |
403 | object | Forbidden. The operation requires the admin role in the organization, resolved from the acting user forwarded in x-codespar-user. Known values of error: insufficient_role, bearer_admin_role_missing, bearer_admin_user_not_member, bearer_admin_role_unresolved. The same status and body shape also carry the scope gate's refusals, which reach every route in this subtree including the two reads: forbidden when the credential lacks the scope the route is mapped to, and the two alarms scopes_unresolved and route_scope_unresolved. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
count | integer | yes | The length of tools. |
error | string | no | Present only when valid is false. invalid_url, connect_timeout and list_tools_timeout come from the introspection itself; anything else is the upstream error's own message, so treat this as an open string rather than a code enum. |
tools | array of object | yes | Empty when valid is false. |
valid | boolean | yes | — |
curl -X POST https://api.codespar.dev/v1/mcp-servers/validate \
-H "Authorization: Bearer $CODESPAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"upstream_url": "https://example.com/hook",
"auth": {
"header": "string",
"value": "string"
}
}'POST /v1/mcp-servers/validate HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json
{
"upstream_url": "https://example.com/hook",
"auth": {
"header": "string",
"value": "string"
}
}import os
import requests
res = requests.post(
"https://api.codespar.dev/v1/mcp-servers/validate",
headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
json={
"upstream_url": "https://example.com/hook",
"auth": {
"header": "string",
"value": "string"
}
},
)
res.raise_for_status()
data = res.json()const res = await fetch("https://api.codespar.dev/v1/mcp-servers/validate", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"upstream_url": "https://example.com/hook",
"auth": {
"header": "string",
"value": "string"
}
}),
});
const data = await res.json();const r = await cs.api.response("post", "/v1/mcp-servers/validate", {
body: {
upstream_url: "https://example.com/hook",
auth: {
header: "string",
value: "string"
}
}
});
// r.status is one of the documented statuses (200, 403),
// each with its own body shape in r.data; nothing here throws on 403.
if (r.ok) {
console.log(r.data);
}{
"valid": true,
"count": 1,
"tools": [
{
"name": "Example",
"description": "string"
}
],
"error": "string"
}GET /v1/mcp-servers/{id}
https://api.codespar.dev/v1/mcp-servers/{id}Read one MCP server with its priced tools
The server plus every tool row, ordered by tool_name, active and inactive alike.
An id belonging to another organization or another project returns 404, not 403. The lookup carries both predicates in its WHERE clause, so the handler cannot distinguish someone else owns this from no such row, and must not, because telling them apart would confirm the id exists.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | yes | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
404 | object | No such server in this organization and project. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
active | boolean | yes | — |
category | string,null | yes | — |
consumer_id | string,null | yes | — |
created_at | string (date-time) | yes | — |
description | string | yes | Listing card summary. Empty string when none was supplied. |
environment | "live" | "test" | yes | Taken from the calling credential's environment at create time. There is no body field for it. |
gateway_unavailable_reason | "gateway_origin_not_configured" | "gateway_origin_is_production" | yes | Why the gateway URL is null: this deployment is not production and names no gateway origin of its own (gateway_origin_not_configured), or names a production host (gateway_origin_is_production). Null when the URL is present. Production's gateway is never used as a fallback outside production. |
gateway_url | string,null | yes | <gateway origin>/mcp/<slug> (production: https://gw.codespar.dev/mcp/<slug>), rebuilt from the slug on every read rather than stored. Null where this deployment has no gateway; see gateway_unavailable_reason. |
id | string | yes | mcs_ followed by a nanoid. |
last_paid_call_at | string,null (date-time) | no | When the server's last paid call settled over x402, with no window: the latest sale GET /v1/gate/stats attributes to this server, by the same rule. Null when it never sold. A Mode A (mandate) call settles through the consumer wallet and is not a sale here. Present on the list and on the single read; absent from create and patch. |
name | string | yes | — |
overview_md | string | yes | Listing page body, markdown. Empty string when none was supplied. |
payto_address | string | yes | The address that receives USDC for calls settled through the gateway. |
payto_kind | "provisioned" | "split" | yes | Create always writes provisioned, a CodeSpar-derived CDP wallet. split exists in the column but no operation on this surface produces it, and bring-your-own is not offered. |
platform_fee_bps | integer | yes | Basis points of each settled call accrued as a platform fee, 0 to 10000. Stored at create and applied by settlement, not by this resource. |
project_id | string | yes | — |
slug | string | yes | Lowercase letters, digits, underscore or dash, up to 64 characters. The slug namespace is GLOBAL across all tenants, so a slug another organization already holds is refused. |
tool_count | integer | yes | The number of tools supplied at create time. Nothing recomputes it, so deactivating a tool leaves it unchanged and it is not a count of ACTIVE tools. |
tools | array of object | no | Present on create, on the single read, and on the listing patch. ABSENT from the list response, which returns servers without their tools. |
upstream_url | string | yes | — |
curl -X GET https://api.codespar.dev/v1/mcp-servers/{id} \
-H "Authorization: Bearer $CODESPAR_API_KEY"GET /v1/mcp-servers/{id} HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEYimport os
import requests
res = requests.get(
"https://api.codespar.dev/v1/mcp-servers/{id}",
headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()const res = await fetch("https://api.codespar.dev/v1/mcp-servers/{id}", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
},
});
const data = await res.json();const result = await cs.api.get("/v1/mcp-servers/{id}", {
path: {
id: "msrv_0000000000000000"
}
});{
"id": "obj_0000000000000000",
"project_id": "prj_0000000000000000",
"slug": "example",
"name": "Example",
"description": "string",
"overview_md": "string",
"category": "string",
"upstream_url": "https://example.com/hook",
"environment": "live",
"payto_kind": "provisioned",
"payto_address": "string",
"consumer_id": "csm_0000000000000000",
"platform_fee_bps": 0,
"tool_count": 1,
"active": true,
"gateway_url": "https://example.com/hook",
"gateway_unavailable_reason": "gateway_origin_not_configured",
"tools": [
{
"id": "obj_0000000000000000",
"tool_name": "Example",
"description": "string",
"pricing_model": "string",
"price": "string",
"price_atomic": "string",
"active": true
}
],
"created_at": "2026-01-15T12:00:00.000Z",
"last_paid_call_at": "2026-01-15T12:00:00.000Z"
}PATCH /v1/mcp-servers/{id}
https://api.codespar.dev/v1/mcp-servers/{id}Update an MCP server's listing metadata or its active flag
Accepts name, description, overview_md, category and active. At least one must be present; an empty object is refused with invalid_body.
NOTHING ABOUT MONEY OR ROUTING IS PATCHABLE HERE. slug, upstream_url, upstream_auth, consumer_id, payto_address and platform_fee_bps have no field on this operation, and tool prices move through the tool patch below.
Omitted fields keep their stored value. category therefore cannot be CLEARED through this operation: the update coalesces an absent field onto the existing one and the schema does not accept null. description and overview_md can be emptied by sending "".
active: false is what takes the gateway URL out of service; it is not a delete and the slug stays held.
Returns the updated server with its tools. Cross-tenant ids return 404, for the same reason as the read.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | yes | — |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
active | boolean | no | — |
category | string | no | — |
description | string | no | — |
name | string | no | — |
overview_md | string | no | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | The body failed the schema, or was an empty patch. details.issues carries the issues. |
403 | object | Forbidden. The operation requires the admin role in the organization, resolved from the acting user forwarded in x-codespar-user. Known values of error: insufficient_role, bearer_admin_role_missing, bearer_admin_user_not_member, bearer_admin_role_unresolved. The same status and body shape also carry the scope gate's refusals, which reach every route in this subtree including the two reads: forbidden when the credential lacks the scope the route is mapped to, and the two alarms scopes_unresolved and route_scope_unresolved. |
404 | object | No such server in this organization and project. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
active | boolean | yes | — |
category | string,null | yes | — |
consumer_id | string,null | yes | — |
created_at | string (date-time) | yes | — |
description | string | yes | Listing card summary. Empty string when none was supplied. |
environment | "live" | "test" | yes | Taken from the calling credential's environment at create time. There is no body field for it. |
gateway_unavailable_reason | "gateway_origin_not_configured" | "gateway_origin_is_production" | yes | Why the gateway URL is null: this deployment is not production and names no gateway origin of its own (gateway_origin_not_configured), or names a production host (gateway_origin_is_production). Null when the URL is present. Production's gateway is never used as a fallback outside production. |
gateway_url | string,null | yes | <gateway origin>/mcp/<slug> (production: https://gw.codespar.dev/mcp/<slug>), rebuilt from the slug on every read rather than stored. Null where this deployment has no gateway; see gateway_unavailable_reason. |
id | string | yes | mcs_ followed by a nanoid. |
last_paid_call_at | string,null (date-time) | no | When the server's last paid call settled over x402, with no window: the latest sale GET /v1/gate/stats attributes to this server, by the same rule. Null when it never sold. A Mode A (mandate) call settles through the consumer wallet and is not a sale here. Present on the list and on the single read; absent from create and patch. |
name | string | yes | — |
overview_md | string | yes | Listing page body, markdown. Empty string when none was supplied. |
payto_address | string | yes | The address that receives USDC for calls settled through the gateway. |
payto_kind | "provisioned" | "split" | yes | Create always writes provisioned, a CodeSpar-derived CDP wallet. split exists in the column but no operation on this surface produces it, and bring-your-own is not offered. |
platform_fee_bps | integer | yes | Basis points of each settled call accrued as a platform fee, 0 to 10000. Stored at create and applied by settlement, not by this resource. |
project_id | string | yes | — |
slug | string | yes | Lowercase letters, digits, underscore or dash, up to 64 characters. The slug namespace is GLOBAL across all tenants, so a slug another organization already holds is refused. |
tool_count | integer | yes | The number of tools supplied at create time. Nothing recomputes it, so deactivating a tool leaves it unchanged and it is not a count of ACTIVE tools. |
tools | array of object | no | Present on create, on the single read, and on the listing patch. ABSENT from the list response, which returns servers without their tools. |
upstream_url | string | yes | — |
curl -X PATCH https://api.codespar.dev/v1/mcp-servers/{id} \
-H "Authorization: Bearer $CODESPAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Example",
"description": "string",
"overview_md": "string",
"category": "string",
"active": true
}'PATCH /v1/mcp-servers/{id} HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json
{
"name": "Example",
"description": "string",
"overview_md": "string",
"category": "string",
"active": true
}import os
import requests
res = requests.patch(
"https://api.codespar.dev/v1/mcp-servers/{id}",
headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
json={
"name": "Example",
"description": "string",
"overview_md": "string",
"category": "string",
"active": True
},
)
res.raise_for_status()
data = res.json()const res = await fetch("https://api.codespar.dev/v1/mcp-servers/{id}", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Example",
"description": "string",
"overview_md": "string",
"category": "string",
"active": true
}),
});
const data = await res.json();const r = await cs.api.response("patch", "/v1/mcp-servers/{id}", {
path: {
id: "msrv_0000000000000000"
},
body: {
name: "Example",
description: "string",
overview_md: "string",
category: "string",
active: true
}
});
// r.status is one of the documented statuses (200, 403),
// each with its own body shape in r.data; nothing here throws on 403.
if (r.ok) {
console.log(r.data);
}{
"id": "obj_0000000000000000",
"project_id": "prj_0000000000000000",
"slug": "example",
"name": "Example",
"description": "string",
"overview_md": "string",
"category": "string",
"upstream_url": "https://example.com/hook",
"environment": "live",
"payto_kind": "provisioned",
"payto_address": "string",
"consumer_id": "csm_0000000000000000",
"platform_fee_bps": 0,
"tool_count": 1,
"active": true,
"gateway_url": "https://example.com/hook",
"gateway_unavailable_reason": "gateway_origin_not_configured",
"tools": [
{
"id": "obj_0000000000000000",
"tool_name": "Example",
"description": "string",
"pricing_model": "string",
"price": "string",
"price_atomic": "string",
"active": true
}
],
"created_at": "2026-01-15T12:00:00.000Z",
"last_paid_call_at": "2026-01-15T12:00:00.000Z"
}DELETE /v1/mcp-servers/{id}
https://api.codespar.dev/v1/mcp-servers/{id}Delete an MCP server and its priced tools
A HARD delete, not a deactivation: the row is removed and its tool rows cascade with it. There is no undo and no restore, and the gateway URL stops resolving. Use PATCH /v1/mcp-servers/{id} with active: false to take a server out of service while keeping the row and the slug.
The slug returns to the global namespace and can be claimed by any organization afterwards.
The stored upstream credential is not deleted by this call; only the server and its tools are. Returns 204 with no body, or 404 when no row in this organization and project matched.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | yes | — |
Responses
| Status | Body | Description |
|---|---|---|
204 | — | No Content |
403 | object | Forbidden. The operation requires the admin role in the organization, resolved from the acting user forwarded in x-codespar-user. Known values of error: insufficient_role, bearer_admin_role_missing, bearer_admin_user_not_member, bearer_admin_role_unresolved. The same status and body shape also carry the scope gate's refusals, which reach every route in this subtree including the two reads: forbidden when the credential lacks the scope the route is mapped to, and the two alarms scopes_unresolved and route_scope_unresolved. |
404 | object | No such server in this organization and project. |
curl -X DELETE https://api.codespar.dev/v1/mcp-servers/{id} \
-H "Authorization: Bearer $CODESPAR_API_KEY"DELETE /v1/mcp-servers/{id} HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEYimport os
import requests
res = requests.delete(
"https://api.codespar.dev/v1/mcp-servers/{id}",
headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()const res = await fetch("https://api.codespar.dev/v1/mcp-servers/{id}", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
},
});
const data = await res.json();const r = await cs.api.response("delete", "/v1/mcp-servers/{id}", {
path: {
id: "msrv_0000000000000000"
}
});
// r.status is one of the documented statuses (200, 403),
// each with its own body shape in r.data; nothing here throws on 403.
if (r.ok) {
console.log(r.data);
}PATCH /v1/mcp-servers/{id}/tools/{tool}
https://api.codespar.dev/v1/mcp-servers/{id}/tools/{tool}Reprice one tool, or turn it off
Accepts price and active; at least one must be present. {tool} is the TOOL NAME, not an id, so it must be URL-encoded when the name contains a character that would otherwise end the path segment.
A MONEY EDIT THAT TAKES EFFECT ON THE NEXT CALL. price is a positive USDC decimal string with up to six decimals and replaces the tool's price outright; there is no schedule and no grace period. A price outside that shape is refused with invalid_body rather than rounded to something chargeable.
Returns the updated tool alone, not the whole server. tool_count on the server is unaffected by active: false.
404 mcp_tool_not_found is the single answer for three different situations: no such tool on that server, no such server, and a server owned by another organization or project. The update is scoped through the parent server in one statement, so the handler sees only no row matched.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | yes | — |
tool | string | yes | — |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
active | boolean | no | — |
price | string | no | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | The body failed the schema, was an empty patch, or carried a price that is not a positive six-decimal USDC amount. |
403 | object | Forbidden. The operation requires the admin role in the organization, resolved from the acting user forwarded in x-codespar-user. Known values of error: insufficient_role, bearer_admin_role_missing, bearer_admin_user_not_member, bearer_admin_role_unresolved. The same status and body shape also carry the scope gate's refusals, which reach every route in this subtree including the two reads: forbidden when the credential lacks the scope the route is mapped to, and the two alarms scopes_unresolved and route_scope_unresolved. |
404 | object | No tool by that name on a server this organization and project owns. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
active | boolean | yes | An inactive tool stays listed and stays priced. |
description | string | yes | Empty string when none was supplied. |
id | string | yes | mct_ followed by a nanoid. |
input_schema | — | no | The tool's argument JSON Schema, stored as it was received from the upstream's tools/list or from the create body. {} when none was supplied. |
price | string | yes | The per-call price rendered as a trimmed USDC decimal string, derived from price_atomic. |
price_atomic | string | yes | The authoritative per-call price, in USDC atomic units (six decimals), as an integer string. price is a rendering of this value; charge decisions should read this one. |
pricing_model | string | yes | Only flat is written today. The column is a free string with no CHECK constraint, so read it as open rather than as a closed enum. |
tool_name | string | yes | The MCP tool name. Unique within one server, and the key the tool patch addresses. |
curl -X PATCH https://api.codespar.dev/v1/mcp-servers/{id}/tools/{tool} \
-H "Authorization: Bearer $CODESPAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"price": "string",
"active": true
}'PATCH /v1/mcp-servers/{id}/tools/{tool} HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json
{
"price": "string",
"active": true
}import os
import requests
res = requests.patch(
"https://api.codespar.dev/v1/mcp-servers/{id}/tools/{tool}",
headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
json={
"price": "string",
"active": True
},
)
res.raise_for_status()
data = res.json()const res = await fetch("https://api.codespar.dev/v1/mcp-servers/{id}/tools/{tool}", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"price": "string",
"active": true
}),
});
const data = await res.json();const r = await cs.api.response("patch", "/v1/mcp-servers/{id}/tools/{tool}", {
path: {
id: "msrv_0000000000000000",
tool: "string"
},
body: {
price: "string",
active: true
}
});
// r.status is one of the documented statuses (200, 403),
// each with its own body shape in r.data; nothing here throws on 403.
if (r.ok) {
console.log(r.data);
}{
"id": "obj_0000000000000000",
"tool_name": "Example",
"description": "string",
"pricing_model": "string",
"price": "string",
"price_atomic": "string",
"active": true
}Connect
1 operation under /v1/connect/start (POST): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.
Servers
4 operations under /v1/servers (GET POST): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.