Overview
The Sprucely.io MCP APIs expose the dashboard platform to AI assistants and third-party software through the Model Context Protocol (MCP), an open standard for connecting AI models to tools over HTTP. Any MCP-compatible client can create hosted dashboards, list and inspect them, and append data to them programmatically. Dashboards created through the APIs belong to your account and can be shared, embedded or edited further in the service. For embedding the resulting dashboards into your own site, refer to the simple dashboard integration notes. If you use Claude, the fastest way to get started is the Sprucely.io connector.
Connecting
The MCP server is served over Streamable HTTP at:
https://www.sprucely.io/mcpAny MCP client can connect to this endpoint. For example, one command registers the server in Claude Code:
claude mcp add --transport http sprucely https://www.sprucely.io/mcpYou can also call the server directly with JSON-RPC 2.0 over HTTP. Most MCP clients perform the protocol handshake automatically; the request below lists the available tools using an API key:
curl -X POST https://www.sprucely.io/mcp \
-H "Authorization: Bearer spr_mcp_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'Authentication
Requests are authorized with a standard Authorization Bearer header. Two credential types are supported.
API keys - Create API keys in the MCP section of your profile. Keys start with spr_mcp_, are shown only once at creation, and can be revoked at any time. Use them for scripts, servers and headless MCP clients.
OAuth 2.1 - Interactive MCP clients can instead let users sign in with their Sprucely.io account through OAuth 2.1. The authorization server supports the authorization code flow with PKCE (required), refresh tokens and dynamic client registration, so compatible clients configure themselves automatically from the resource metadata:
https://www.sprucely.io/.well-known/oauth-protected-resourceAvailable scopes: openid, offline_access, dashboards:read and dashboards:write. Scopes are enforced per tool: create_dashboard, add_data and set_sharing require dashboards:write and return an error without it. The remaining tools need only dashboards:read.
For clients that require manual configuration, the authorization server endpoints are:
https://www.sprucely.io/oauth/.well-known/openid-configuration- authorization server metadatahttps://www.sprucely.io/oauth/auth- authorization endpoint (PKCE required)https://www.sprucely.io/oauth/token- token endpointhttps://www.sprucely.io/oauth/reg- dynamic client registrationhttps://www.sprucely.io/oauth/jwks- signing keys (JWKS)https://www.sprucely.io/oauth/token/revocation- token revocationhttps://www.sprucely.io/oauth/token/introspection- token introspection
Tools
The MCP server exposes eight tools: create_dashboard, get_dashboard, list_dashboards, add_data, get_dataset_schema, set_sharing, get_embed_code and storage_status.
The dataset and dashboard parameters below are not an MCP-specific format - they are exactly the same dataset/dashboard JSON documented on the Data Format page and used everywhere else in the service (the standalone embeddable runtime’s import API, AI-generated layouts, and the dashboard editor), so anything written against that reference applies here too.
create_dashboard - Creates a hosted dashboard from an inline dataset and a widget tree, and returns view and embed links. Parameters:
name- the dashboard title (required)dataset- the dataset to visualize:name,headers(column names),types(one database type per header -VARCHAR,BOOLEAN,INTEGER,BIGINT,FLOAT,DOUBLE,DATEorTIMESTAMP) andentries(rows, each an array of values in header order), up to 100 columns and 50,000 raw, row-level entries per call (required). Entries must not be pre-aggregated: send one row per underlying record, and let each chart’sdataFunctioncompute counts, sums, averages and so on at render time.dashboard- 1 to 40 top-level widgets, top to bottom (required). Each widget is adash_chart(a chart type -area,bar,cell,dot,hexbinorline- column mappings forx,y,dandr, an optionaldataFunctionand an optional title), adash_table, adash_textblock, adash_spacer, or adash_stacker_hor/dash_stacker_vercontainer. Stackers nest recursively - a stacker’s own children can include further stackers - to arrange widgets into rows and columns of arbitrary depth. Which column kinds (categorical, continuous or date/time) each chart type’sx/y/d/rdimensions accept, and how time-bucketing a date column withseasonX/seasonYturns it categorical, is documented on each field via tools/list and on the Data Format page.theme- optional dashboard-wide colors (background, text, primary/subtle/secondary accents); any widget can still override its own color or backgroundshared- whether the dashboard is viewable by anyone with the link (optional, default false). New dashboards are private until you set this to true or callset_sharingafterwards.
The result carries the new dashboard’s dashboardId and datasetId (needed by add_data, get_dataset_schema, set_sharing and get_embed_code afterwards), plus its url, viewUrl and iframe embed snippet. Example - create a dashboard with a bar chart and a dashboard-wide accent color:
curl -X POST https://www.sprucely.io/mcp \
-H "Authorization: Bearer spr_mcp_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "create_dashboard",
"arguments": {
"name": "Regional Sales",
"dataset": {
"name": "Sales",
"headers": ["region", "revenue"],
"types": ["VARCHAR", "DOUBLE"],
"entries": [
["North", 1250.5],
["South", 980.25],
["East", 1420.0]
]
},
"dashboard": {
"type": "dash",
"children": [
{ "type": "dash_chart", "chartType": "bar", "x": "region", "d": "revenue", "dataFunction": "sum", "title": "Revenue by region" }
]
},
"theme": { "themePrimary": "#6E2BDC" }
}
}
}'
# Result (result.structuredContent in the JSON-RPC response):
{
"dashboardId": "dash_a1b2c3d4",
"datasetId": "data_e5f6g7h8",
"name": "Regional Sales",
"shared": false,
"rows": 3,
"columns": 2,
"url": "https://www.sprucely.io/service/dashboards/embed/123/dash_a1b2c3d4",
"viewUrl": "https://www.sprucely.io/service/dashboards/view/123/dash_a1b2c3d4",
"iframe": "<iframe src=\"https://www.sprucely.io/service/dashboards/embed/123/dash_a1b2c3d4\" style=\"width:100%;height:640px;border:0\" title=\"Sprucely dashboard\" loading=\"lazy\"></iframe>"
}get_dashboard - Returns a dashboard’s name, sharing status, dataset IDs and links. Parameter: dashboardId (required). Example:
curl -X POST https://www.sprucely.io/mcp \
-H "Authorization: Bearer spr_mcp_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "get_dashboard",
"arguments": {
"dashboardId": "dash_a1b2c3d4"
}
}
}'
# Result (result.structuredContent in the JSON-RPC response):
{
"dashboardId": "dash_a1b2c3d4",
"name": "Regional Sales",
"shared": false,
"archived": false,
"datasetIds": ["data_e5f6g7h8"],
"source": "mcp",
"updatedAt": "2026-07-29T10:15:00.000Z",
"url": "https://www.sprucely.io/service/dashboards/embed/123/dash_a1b2c3d4",
"viewUrl": "https://www.sprucely.io/service/dashboards/view/123/dash_a1b2c3d4",
"iframe": "<iframe src=\"https://www.sprucely.io/service/dashboards/embed/123/dash_a1b2c3d4\" style=\"width:100%;height:640px;border:0\" title=\"Sprucely dashboard\" loading=\"lazy\"></iframe>"
}list_dashboards - Lists the dashboards in your account, ordered by last update, each with its own datasetIds so a dataset can be passed straight to get_dataset_schema or add_data without a separate get_dashboard call. Parameters: limit and offset for paging, query for a case-insensitive name filter (all optional). Example:
curl -X POST https://www.sprucely.io/mcp \
-H "Authorization: Bearer spr_mcp_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "list_dashboards",
"arguments": {
"limit": 10,
"query": "sales"
}
}
}'
# Result (result.structuredContent in the JSON-RPC response):
{
"total": 1,
"limit": 10,
"offset": 0,
"hasMore": false,
"dashboards": [
{
"dashboardId": "dash_a1b2c3d4",
"name": "Regional Sales",
"shared": false,
"datasetIds": ["data_e5f6g7h8"],
"source": "mcp",
"updatedAt": "2026-07-29T10:15:00.000Z",
"url": "https://www.sprucely.io/service/dashboards/embed/123/dash_a1b2c3d4",
"viewUrl": "https://www.sprucely.io/service/dashboards/view/123/dash_a1b2c3d4",
"iframe": "<iframe src=\"https://www.sprucely.io/service/dashboards/embed/123/dash_a1b2c3d4\" style=\"width:100%;height:640px;border:0\" title=\"Sprucely dashboard\" loading=\"lazy\"></iframe>"
}
]
}add_data - Appends entries to an existing dashboard’s dataset; every dashboard using the dataset refreshes automatically. Parameters:
dashboardId- the dashboard to append to (required)entries- rows aligned positionally to the dataset’s headers, up to 50,000 per call (required) - the same raw, row-level rule ascreate_dashboardapplies. Callget_dataset_schemafirst if you don’t already know the dashboard’s exact column names/order. Use it to load large datasets in batches or to keep dashboards up to date.run_id- an optional idempotency key for this specific batch (optional). If a previousadd_datacall with the samedashboardIdandrun_idalready succeeded, calling again returns that prior result instead of appending the rows a second time, so a batch can be retried safely after a timeout or dropped connection - use a newrun_idper distinct batch (e.g. a UUID or a hash of its contents).
Example:
curl -X POST https://www.sprucely.io/mcp \
-H "Authorization: Bearer spr_mcp_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "add_data",
"arguments": {
"dashboardId": "dash_a1b2c3d4",
"entries": [
["West", 875.0]
],
"run_id": "batch-2026-07-29T10:20:00Z"
}
}
}'
# Result (result.structuredContent in the JSON-RPC response):
{
"dashboardId": "dash_a1b2c3d4",
"datasetId": "data_e5f6g7h8",
"addedRows": 1,
"url": "https://www.sprucely.io/service/dashboards/embed/123/dash_a1b2c3d4",
"viewUrl": "https://www.sprucely.io/service/dashboards/view/123/dash_a1b2c3d4",
"iframe": "<iframe src=\"https://www.sprucely.io/service/dashboards/embed/123/dash_a1b2c3d4\" style=\"width:100%;height:640px;border:0\" title=\"Sprucely dashboard\" loading=\"lazy\"></iframe>"
}
# Calling again later with the same dashboardId + run_id (e.g. after a dropped
# connection) is safe: the rows are not appended twice, and the response also
# carries "duplicate": true instead of appending again.get_dataset_schema - Returns a dataset’s column names and types. Use it before calling add_data (or building a new chart against the same dataset) whenever the exact column names/order/types aren’t already known, e.g. in a new session or when another agent created the dashboard. Parameter: datasetId (required). Example:
curl -X POST https://www.sprucely.io/mcp \
-H "Authorization: Bearer spr_mcp_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 6,
"method": "tools/call",
"params": {
"name": "get_dataset_schema",
"arguments": {
"datasetId": "data_e5f6g7h8"
}
}
}'
# Result (result.structuredContent in the JSON-RPC response):
{
"datasetId": "data_e5f6g7h8",
"headers": ["region", "revenue"],
"types": ["VARCHAR", "DOUBLE"]
}set_sharing - Turns a dashboard’s public link on or off. Turning it off revokes access for every earlier revision of the dashboard too, so a link that was shared in the past stops working. Parameters: dashboardId and shared (both required). Example:
curl -X POST https://www.sprucely.io/mcp \
-H "Authorization: Bearer spr_mcp_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "set_sharing",
"arguments": {
"dashboardId": "dash_a1b2c3d4",
"shared": true
}
}
}'
# Result (result.structuredContent in the JSON-RPC response):
{
"dashboardId": "dash_a1b2c3d4",
"shared": true,
"url": "https://www.sprucely.io/service/dashboards/embed/123/dash_a1b2c3d4",
"viewUrl": "https://www.sprucely.io/service/dashboards/view/123/dash_a1b2c3d4",
"iframe": "<iframe src=\"https://www.sprucely.io/service/dashboards/embed/123/dash_a1b2c3d4\" style=\"width:100%;height:640px;border:0\" title=\"Sprucely dashboard\" loading=\"lazy\"></iframe>"
}get_embed_code - Returns the shareable view link and an iframe embed snippet for a dashboard - the same link/embed code create_dashboard and set_sharing already return, for when they are needed again on their own. The link only works for others while the dashboard is shared. Parameter: dashboardId (required). Example:
curl -X POST https://www.sprucely.io/mcp \
-H "Authorization: Bearer spr_mcp_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 8,
"method": "tools/call",
"params": {
"name": "get_embed_code",
"arguments": {
"dashboardId": "dash_a1b2c3d4"
}
}
}'
# Result (result.structuredContent in the JSON-RPC response):
{
"dashboardId": "dash_a1b2c3d4",
"shared": true,
"url": "https://www.sprucely.io/service/dashboards/embed/123/dash_a1b2c3d4",
"viewUrl": "https://www.sprucely.io/service/dashboards/view/123/dash_a1b2c3d4",
"iframe": "<iframe src=\"https://www.sprucely.io/service/dashboards/embed/123/dash_a1b2c3d4\" style=\"width:100%;height:640px;border:0\" title=\"Sprucely dashboard\" loading=\"lazy\"></iframe>"
}storage_status - Reports how much storage the account has used and how much remains, useful before a create_dashboard or add_data call large enough to risk the plan’s storage limit. No parameters. Example:
curl -X POST https://www.sprucely.io/mcp \
-H "Authorization: Bearer spr_mcp_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 9,
"method": "tools/call",
"params": {
"name": "storage_status",
"arguments": {}
}
}'
# Result (result.structuredContent in the JSON-RPC response):
{
"used": 5242880,
"limit": 104857600,
"percent": 5,
"plan": "Regular",
"over": 0,
"blocked": false
}Limits and Errors
- The MCP endpoint accepts 25 requests per minute per client and request bodies up to 25 MB - load larger datasets in
add_databatches. - Datasets accept up to 100 columns and 50,000 rows per call, and a dashboard accepts up to 40 widgets at each level of nesting. Stored dashboards count toward your plan’s storage quota - call
storage_statusto check remaining headroom before a largecreate_dashboardoradd_datacall. - Standard HTTP status codes are used: 401 for missing or invalid credentials (with a WWW-Authenticate challenge pointing at the resource metadata), 403 for revoked or insufficient credentials, 413 for oversized requests and 429 when rate limited.